exhaustive-deps란?
function Example({ someProp }) {
function doSomething() {
console.log(someProp);
}
useEffect(() => {
doSomething();
}, []); // 🔴 이것은 안전하지 않다 (`someProp`을 사용하는`doSomething`을 호출)
// 그러면 'doSomething'를 useEffect 안에 넣어라
}
React Hook useEffect has missing dependencies: ‘…’, … Either include them or remove the dependency array.
해당 데이터들이 다 바뀌었는데 useEffect 내부의 데이터는 다 최신 상태여야 한다는게 리액트의 철학 때문에
useEffect가 안돌아가면 얘네들은 오래된 데이터가 될텐데 괜찮은가?하고 에러 내보내는 것이다.
eslint-plugin-react-hooks 패키지의 일부로 exhaustive-deps ESLint 규칙을 제공하는데,
ESLint로 인해 의존성을 철저하게(exhaustive) 다 적었는지 검사를 해준다.
useEffect의 의존성을 비워두면 처음 한 번만 렌더링되기 때문에 처음의 데이터만 기억하고 있는 상태임. → 데이터 불일치
그런데 의존성을 다 넣어주게 되면 무한루프가 도는 경우가 있다.
왜 무한루프가 도는 것인지?
내가 넣은 의존성이 사실은 실행될 때마다 ‘New’로 변하고 있다.
- 렌더링: 리액트가 컴포넌트를 그린다. 이때 getSummary 함수가 새로 생성된다.(내용은 같지만 메모리 주소가 바뀜)
- useEffect 감지: getSummary가 바뀌었네? → 재실행
- 함수 실행: getSummary 안에서 데이터 가져와서 setData(결과) 호출
- 상태 변경: setData가 호출되었으니 리액트다 다시 리렌더링
- 무한 반복 …
이름이 같은 함수인데 왜 재실행되는 것일까?
리액트 입장에서는 컴포넌트가 돌 때마다 이름만 같은 다른 함수라고 판단(자바스크립트의 객체/함수 특성 때문)
-
자바스크립트의 참조값 특성
자바스크립트에서는 함수나 객체는 메모리 주소를 갖는다.
- 숫자나 문자열(원시 타입): 1은 언제나 1, “Young”는 언제나 “Young”이다.
- 함수나 객체(참조 타입): 겉모양이 똑같이 생겼어도 메모리에 저장된 위치(주소)가 다르면 완전히 다른 객체로 본다.
// 겉보기엔 똑같은 함수지만 const func1 = () => console.log("안녕"); const func2 = () => console.log("안녕"); console.log(func1 === func2); // 결과는 false! (주소가 다르니까요) -
컴포넌트 렌더링 = 함수 재실행 (지역함수)
리액트 컴포넌트도 함수인데, 상태가 바뀌어 렌더링이 일어났다는 것은 이 함수가 재실행되었다는 것!
- App 이라는 큰 함수 실행
- 그 안의 const로 지역 함수 재선언
- 새로운 메모리 공간 할당하여 함수 저장
- 이전 렌더링이랑 달라진 메모리에 저장된 주소로 인해 재실행된다.
-
리액트의 비교 방식
리액트는 useEffect 의존성 배열 검사시, 엄격한 비교(===) 사용
- 주소가 바뀌면 데이터가 바뀐 거로 판단.
해결 방법
useEffect, useLayoutEffect, useMemo, useCallback , useImperativeHandle
위와 같은 훅에서 마지막 인수로 종속성 목록을 지정하는데, 콜백 내에서 사용되는 모든 값을 포함하고 리액트 데이터 흐름에 참여되어야 한다.
state, prop, 함수 등등
-
종속되는 거 다 적어주기
function Example({ someProp }) { function doSomething() { console.log(someProp); } useEffect(() => { doSomething(); }, [someProp]); } -
useEffect안으로 이동 시키기function Example({ someProp }) { useEffect(() => { // 밖에 있던 함수를 안으로 이사 시켰어요! function doSomething() { console.log(someProp); } doSomething(); }, [someProp]); // 이제 someProp이 바뀔 때마다 이 함수가 최신 데이터를 들고 실행돼요. }함수가 useEffect 밖에 있으면 그 함수가 내부에서 뭘 쓰는지 리액트가 알 수 없다.
안에 있으면 someProp를 쓴다는걸 알수 있다. -
useCallback으로 포장 시키기const doSomething = useCallback(() => { console.log(someProp); }, [someProp]); // someProp이 바뀔 때만 함수가 새로 만들어짐 useEffect(() => { doSomething(); }, [doSomething]); // 함수의 '정체성'이 바뀌었을 때만 실행!그럼에도 함수를 밖에 유지하고 싶다면?(
useCallback)useCallback안에 함수 자체를 의존성 배열에 넣을 수 있게 포장하기!진짜 중요한 데이터가 바뀔 때만 새로 만들고, 평소에는 재활용되어진다.
→ 리액트가 메모리에 이 함수의 주소를 저장해두고, 리렌더링해도 새로 주소를 만들지 않고 그 주소를 그대로 사용!!) -
경고 무시하기 (권장 안함)
// eslint-disable-next-line의존성 종속을 하지 않는 것 자체가 “리액트의 엔진 작동 원리를 망가뜨리는 것”
- 오래된 데이터(유령 버그)
- 리액트의 자동 동기화 무시
결론
useEffect는 동기화 장치이다. → 데이터가 변하면 이펙트도 반응해야 한다.
이펙트 안에서 쓰는 모든 “리액트 값(props, state, 함수)”는 의존성 배열에 반드시 포함하기
클로저 문제로 인해 옛날 데이터를 보여주게 되는 버그, 무한 루프 막을 수 있다.
참고 내용