const App = () => {
cosnt { data } = useFetch({ url: "/jack.json" });
console.log("App rendering");
return (
<div>{ JSON.stringify(data) }</div>
);
}
↑ App 컴포넌는 useFetch 커스텀훅으로 받아온 data를 보여주고 있습니다.
export const useFetch = (options) => {
const [ data, setData ] = useState(null);
useEffect(() => {
console.log("useFetch useEffect");
fetch(option.url)
.then(resp => resp.json())
.then(json => setData(json));
});
return { data };
}
↑ useFetch는 커스텀훅으로써 fetch API를 통해 받아온 데이터를 반환해주고 있습니다.
↑ 코드실행 시 외와 같이 App 컴포넌트가 무한 렌더링되는 상황을 볼 수 있다.
uesEffect 무한루프 원인
- 함수형 컴포넌트가 렌더링된다는 것은 함수가 호출되어 실행되는 것과 같다.
- 리액트는 state, props가 업데이트될때 컴포넌트를 렌더링한다.
- 의존성 배열이 없다.
위 코드의 실행과정을 살펴보면...
1. App 컴포넌트 렌더링 → useFetch 실행 → fetch 함수 호출 → 최초 결과값은 null → App 컴포넌트에 data 변수에 결과값 할당. 그리고 나머지 코드실행 → fetch 함수에서 promise 결과값 도착 → useFetch 안에 있는 state 업데이트 → App 컴포넌트 호출 → useFetch 실행 ...
결국 커스텀훅의 state값이 변경됨에 따라 커스텀훅을 사용하는 컴포넌트가 반복 호출되는 현상이다. 그리고 useEffect에는 의존성 배열이 없어 비교하는 과정없이 무조건 실행된 것이다.
export const useFetch = (options) => {
const [ data, setData ] = useState(null);
useEffect(() => {
console.log("useFetch useEffect");
fetch(option.url)
.then(resp => resp.json())
.then(json => setData(json));
}, []); // 두번째 인자로 빈배열 추가
return { data };
}
의존성 배열(Dependency array)
- useEffect는 클래스형 컴포넌트에서 볼 수 있는 componentDidMount, componentDidUpdate, componentWillMount 등 라이프 싸이클을 함수형 컴포넌트에서도 똑같이 사용될 수 있도록 개발된 훅이다.
이때 useEffect의 두번째 인자로 배열이 들어갈 수 있는데 이를 의존성 배열이라고 한다.
- Dependency란 사전적의미로 "의존"이다. 즉, useEffect는 무엇인가에 의존한다는 것이다.
- 배열안에는 useEffect가 의존할 대상(element)을 넣는다. 그리고 useEffect는 의존할 대상을 캐싱하고 useEffect가 실행될때 마다(컴포넌트가 렌더링될때 마다) 그 대상의 이전 상태와 이후 상태를 비교하고 상태가 다를 경우 useEffect 첫번째 콜백함수를 실행한다. 이때 비교는 얕은 비교를 한다. (Shallow comparison | 원시값과 객체의 다른 비교방식 | 참고링크)
useEffect 사용상황 CASE#1
const App = () => {
const [ url, setUrl ] = useState(null)
cosnt { data } = useFetch({ url });
console.log("App rendering");
return (
<>
<div>{ JSON.stringify(data) }</div>
<button onClick={() => setUrl("/jack.json")}>jack</button>
<button onClick={() => setUrl("/sally.json")}>sally</button>
</>
);
}
export const useFetch = (options) => {
const [ data, setData ] = useState(null);
useEffect(() => {
console.log("useFetch useEffect");
fetch(option.url)
.then(resp => resp.json())
.then(json => setData(json));
}, [options]);
return { data };
}
위 코드는 jack, sally 버튼을 클릭 시 useFetch를 통해 jack.json, sally.json의 데이터를 가져오기 위함이다.
하지만 위와 같이 다시 무한루프가 발생한다. 그 이유는 useEffect의 의존성 배열의 요소인 options 때문이다.
options는 App 컴포넌트에서 객체로 전달된다. 비록 url의 값이 "/jack.json"이어도 객채이기 때문에 원시값처럼 비교하는 것과는 다르게 객체의 참조값(메모리의 주소)을 비교하게 된것이다. 즉, useEffect의 입장에서는 같은 값이 아니라는 판단을 하고 블로그글 첫번째 상황이 반복되는 것이다.
해결방법#1: useMemo
const App = () => {
const [ url, setUrl ] = useState(null)
const myOptions = useMemo(() => ({ url }), [url]);
cosnt { data } = useFetch(myOptions);
console.log("App rendering");
return (
<>
<div>{ JSON.stringify(data) }</div>
<button onClick={() => setUrl("/jack.json")}>jack</button>
<button onClick={() => setUrl("/sally.json")}>sally</button>
</>
);
}
바로 useMemo 훅을 사용하는 것이다. useMemo는 값을 메모리에 저장시켜 주기때문에 useEffect가 비교대상을 비교할때 정확하고 똑같은 메모리 주소를 참조하여 비교하기 때문에 정상적으로 작동되는 것을 볼 수 있다.
해결방법#2: 비교대상을 원시값으로 전달하기
const App = () => {
const [ url, setUrl ] = useState(null)
cosnt { data } = useFetch({ url });
console.log("App rendering");
return (
<>
<div>{ JSON.stringify(data) }</div>
<button onClick={() => setUrl("/jack.json")}>jack</button>
<button onClick={() => setUrl("/sally.json")}>sally</button>
</>
);
}
export const useFetch = (options) => {
const [ data, setData ] = useState(null);
useEffect(() => {
console.log("useFetch useEffect");
if(option.url){
fetch(option.url)
.then(resp => resp.json())
.then(json => setData(json));
}
}, [option.url]); // string
return { data };
}
useEffect 사용상황 CASE#2
const App = () => {
const [ url, setUrl ] = useState(null)
cosnt { data } = useFetch({
url,
onSuccess: () => console.log("success")
});
console.log("App rendering");
return (
<>
<div>{ JSON.stringify(data) }</div>
<button onClick={() => setUrl("/jack.json")}>jack</button>
<button onClick={() => setUrl("/sally.json")}>sally</button>
</>
);
}
export const useFetch = (options) => {
const [ data, setData ] = useState(null);
useEffect(() => {
console.log("useFetch useEffect");
if(option.url){
fetch(option.url)
.then(resp => resp.json())
.then(json => {
options?.onSuccess();
setData(json);
});
}
}, [option.url, option.onSuccess]); // string, 함수
return { data };
}
이유는 useEffect 사용상황 CASE#1과 같다.
해결방법#1: useRef
export const useFetch = (options) => {
const [ data, setData ] = useState(null);
const savedOnSuccess = useRef(options.onSuccess); // 함수를 useRef 초기값으로 넣는다
useEffect(() => {
console.log("useFetch useEffect");
if(option.url){
fetch(option.url)
.then(resp => resp.json())
.then(json => {
savedOnSuccess.current?.(json);
setData(json);
});
}
}, [option.url]); // string
return { data };
}
해결방법#2: 커스텀훅 + useLayoutEffect
- 매개변수가 함수일 것을 대비해 전용 콜백 커스텀훅을 만든다.
const useCallbackRef = (callback) => {
const callbackRef = useRef(callback);
useLayoutEffect(() => {
callbackRef.current = callback;
}, [callback]);
return callbackRef;
};
export const useFetch = (options) => {
const [data, setData] = useState(null);
const savedOnSuccess = useCallbackRef(options.onSuccess);
useEffect(() => {
console.log("useFetch useEffect ");
if (options.url) {
let isCancelled = false;
fetch(options.url)
.then((response) => response.json())
.then((json) => {
if (!isCancelled) {
savedOnSuccess.current?.(json);
setData(json);
}
});
return () => {
isCancelled = true;
};
}
}, [options.url]);
return {
data,
};
};
중요!!
만약 사용자가 jack.json을 호출하고 아직 jack.json의 결과값을 가져오지 않은 상태에서 sally.json을 호출할 경우 에러가 발생한다. 이러한 API 호출간 race 현상을 막기 위해 useEffect의 클린업 작업이 필요하다.
'React.js' 카테고리의 다른 글
React Virtual DOM 비교 원리와 얕은 비교(feat. 자바스크립트 비교알고리즘 Object.is( )) (0) | 2022.10.15 |
---|---|
[심층분석] useState (1) | 2022.10.14 |
리액트의 작동원리 (0) | 2022.10.06 |
[애니메이션] GSAP 적용 (0) | 2022.08.12 |
깨달음 (리액트 비동기) (0) | 2022.08.11 |
댓글