-
[React] useEffect 심화 학습Front-End(Web)/React - 프레임워크(React, Next) 2021. 4. 17. 16:28반응형
🧐 서론
프로젝트나 과제에서 useEffect Hooks를 밥먹듯이 사용하였고, 이를 이해하기 보다는 레거시 코드와, dependency array(이하 deps) 대입 테스트를 통해 구현에 초점을 두기만 했었다.
최근, 면접에서 useEffect 관련한 이슈들이 많았고, 렌더링 최적화 등 고려해야 될 조건이나 작동원리를 제대로 모르고 썼다고 느꼈다.
(실제 복수의 면접에서 이러한 부분을 지적받기도 함.)
앞으로 많은 프레임워크를 학습하기 전에 React를 다지는 목적으로 React Docs에 대한 공부를 전반적으로 진행할 예정이다.
Docs 구성순서로 진행하기 앞서, useEffect에 대한 심화학습을 선행해보고자 한다!
💙 useEffect 기본
- React Hooks - useEffect() 포스팅 : abangpa1ace.tistory.com/16?category=905014
useEffect는 React Hooks API의 일종으로, 컴포넌트가 렌더링 될 때마다 특정 작업을 실행하기 위한 Hook이다.
- 백엔드 서버에서 변경된 REST API 데이터를 패치
- props, state 등 상태값의 변동을 확인하기 위함
- Timers(setTimeout, setInterval) 을 운용하기 위함
- 위 조건들에 따라, 혹은 필요시 DOM 변화를 주기 위함
이 useEffect 의 등장으로 함수 컴포넌트는 LifeCycle의 componentDidMount, componentDidUpdate, componentWillUnmount 를 '유사하게' 구현할 수 있게 되었다.
단, 말 그대로 이 LifeCycle 을 모방한 개념이기 때문에, 이를 구현하기 위해선 추가적인 조작이 필요하다.
- 기본 문법
useEffect(callback function, [dependency array])
1) Callback function
useEffect의 첫 번째 인자이자 effect 함수라고도 칭한다. 매 렌더링 시 수행하고자 하는 작업을 콜백함수 형태로 인자로 전달한다.
2) Dependency Array
useEffect의 두 번째 인자이다. deps는 필수요소가 아니므로 생략해도 되나, 이 deps의 모습에 따라 useEffect의 성질이 달라진다.
- undefined : 모든 렌더링, 리렌더링 시 effect 함수 실행
- [] (빈 배열) : 최초(마운트)에만 effect 함수 실행
- [variable] : deps 배열 내 값들이 바뀌는 경우에도 effect 함수 실행. 통상 effect 함수와 관련된 값이나 상태(props, state) 해당.
3) Cleanup function
effect 함수가 반환(return)하는 함수를 cleanup 함수라고 한다. 이 함수는, deps가 비었다면 언마운트 시, 값이 있다면 해당 값의 업데이트 직전에 실행된다.
그렇기에, 이벤트 리스너 삭제나 클로저 해제 등의 역할을 주로 담당하기 때문에 Clean-up 이라고 칭해지곤 한다.
* Rules of Hooks : 1) Hooks는 다른 블록(조건문, 반복문 등)에 포함되지 않는 상위레벨 작성, 2) Hooks는 함수 컴포넌트에서만 운용
💙 useEffect 사용 시 명심하자!
1. 단일 목적의 useEffect
useEffect도 함수 컴포넌트 내에서 사용되는 특정 구조의 '함수'이다. 그렇기에, 클린코드를 위해 이 역시도 하나의 목적을 가져야 한다.
function App() { const [varA, setVarA] = useState(0); const [varB, setVarB] = useState(0); // 이렇게 하면 안된다! useEffect(() => { const timeoutA = setTimeout(() => setVarA(varA + 1), 1000); const timeoutB = setTimeout(() => setVarB(varB + 2), 2000); return () => { clearTimeout(timeoutA); clearTimeout(timeoutB); }; }, [varA, varB]); return ( <span> Var A: {varA}, Var B: {varB} </span> ); }
나쁜 예시의 좋은 예이다. (??🤨🤨) 하나의 useEffect에서 2개의 타이머가 돌 뿐 아니라, 2개의 state를 deps로 받고 있다.
이를 실제로 실행시키면 varA 값만 증가하는, 우리가 의도하지 않은 방식으로 동작한다. (만약 varB가 dpes 첫 인자면 반대가 되리라..)
function App() { const [varA, setVarA] = useState(0); const [varB, setVarB] = useState(0); // 옳은 방법 useEffect(() => { const timeout = setTimeout(() => setVarA(varA + 1), 1000); return () => clearTimeout(timeout); }, [varA]); useEffect(() => { const timeout = setTimeout(() => setVarB(varB + 2), 2000); return () => clearTimeout(timeout); }, [varB]); return ( <span> Var A: {varA}, Var B: {varB} </span> ); }
위 코드는 이렇게 개선될 수 있겠다. 가독성도 훨씬 좋아졌을 뿐 아니라, 각 useEffect가 가지는 목적성이 명확해졌다.
* useState 와 useEffect(Timer) 의 안전성 증가
위 countup 과 같은 로직들은 이전 state에 의존성을 가진다. 이런 경우엔, setter 함수를 아래와 같이 작성하는 게 안전성이 확보된다.
setState((prevState) => prevState + 1);
2. Custom Hooks 사용 권장
function App() { const [varA, setVarA] = useVars(1000); const [varB, setVarB] = useVars(2000); return ( <span> Var A: {varA}, Var B: {varB} </span> ); } function useVars(stepTime) { const [var, setVar] = useState(0); useEffect(() => { const timeout = setTimeout(() => setVar((var) => var + 1), stepTime); return () => clearTimeout(timeout); }, [var]); return [var, setVar]; }
반복되는 로직을 Custom Hooks 화 하면 당연히 코드반복도 줄어들며 유지보수가 쉬워지게 된다.
위 예제는 TOAST UI 출처에서 발췌했지만, 반복을 최소화하기 위해 나는 A,B 2개의 커스텀 훅을 하나로 병합해보았다.
3. 조건부 useEffect의 옳은 방법
setInterval을 종료하는 조건부 로직을 최근에 구현하면서, if문을 타이머 안과 밖에 거는 것에 대해 신중할 필요가 있음을 느꼈다.
function App() { const [varA, setVarA] = useState(0); // 이렇게 하면 안된다. useEffect(() => { let timeout; if (varA < 5) { timeout = setTimeout(() => setVarA(varA + 1), 1000); } return () => clearTimeout(timeout); }, [varA]); return <span>Var A: {varA}</span>; }
이는 동작 자체에는 문제가 없었다. 하지만, setTimeout은 varA에 따라 조건적으로 실행되나, cleanup 함수는 varA가 변경될 때마다 실행된다는 문제점이 발생한다.
function App() { const [varA, setVarA] = useState(0); useEffect(() => { if (varA >= 5) return; const timeout = setTimeout(() => setVarA(varA + 1), 1000); return () => clearTimeout(timeout); }, [varA]); return <span>Var A: {varA}</span>; }
이처럼, if문을 최초에 걸어주면서 초기에 바로 반환한다면, 의도치 않은 timeout의 메모리 소모나 cleanup 함수실행을 방지할 수 있다.
4. useEffect 내의 모든 변수를 deps에 추가
ESLint에서 자주 표출한 exhaustive-deps 규칙 경고와 관련된 내용이다.
useEffect의 역할이 커질수록 내부에서 활용되는 값이 증가할거고, 이 모든 디펜던시를 deps에 추가해야 깨진 클로저가 잔존하지 않게 된다.
function App() { const [varA, setVarA] = useState(0); useEffect(() => { const timeout = setTimeout(() => setVarA(varA + 1), 1000); return () => clearTimeout(timeout); }, []); // 피해야한다: varA 가 디펜던시 배열에 없다. return <span>Var A: {varA}</span>; }
이렇게 빈 deps는 당연히 업데이트를 감지할 수 없으며, varA와 관련된 effect 함수 로직은 불용된 채 외부 state만 참조하는 깨진 클로저로 메모리에 남아있게 된다.
그렇기에, useEffect와 관련된 상태값을 deps에 추가하는 것은 중요하며, 반대로 이 useEffect가 어느 상태에 의존성을 가지는지도 명시해준다.
useEffect를 단순한 업데이트 용도로 사용해온 나로서는, 정말 고려해야 할 부분이 많다는 걸 느낀 기회였다.
어찌 보면 useEffect 뿐만이 아닌, 모든 개발관련 개념에 대해 기본과 Docs에 충실해야 한다는 점 역시 명심하게 되었다.
useEffect에 관련된 이슈들(가령 공식문서 유싱이나 FAQ)까지 차차 정리하면서 이 글을 useEffect 안내서로 만들어보겠다!!
[출처]
- React 공식문서 - Using the Effect Hook : ko.reactjs.org/docs/hooks-effect.html
- overreacted 님의 블로그 : overreacted.io/ko/a-complete-guide-to-useeffect/
- TOAST UI 포스팅 : ui.toast.com/weekly-pick/ko_20200916
반응형'Front-End(Web) > React - 프레임워크(React, Next)' 카테고리의 다른 글
[React] 공식문서 학습(문서) : 주요 개념 (0) 2021.04.28 [React] 공식문서 학습(문서) : 설치 (0) 2021.04.23 [React.js] SyntheticEvent(합성 이벤트) (0) 2021.03.15 [React.js] Event Handling(이벤트 처리) (0) 2021.03.15 [React.js] Custom Hooks (Rules of Hooks) (0) 2021.03.11