
React 공식 문서에서는 You Might Not Need an Effect글에서 useEffect의 무분별한 사용성을 경고한다.
나는 useEffect를 잘 사용하고 있을까?
얼마 안 되는 경력 중 useEffect와 싸운 날들이 많다.
그 중 최근에 useEffect를 사용하면서 겪은 아하! 모먼트를 적어본다.
기존 코드에 useEffect 의존성 배열에 상태값이 두 개 있었다.
view가 그려진 후 한 번만 동작하는 기능이 필요했다.
그래서 useEffect가 적합했고, 나는 의존성 배열이 비어있는 useEffect를 추가했다.
useEffect(() => {
// ...
}, [])
추가하고나니 마음에 안들었다.
왜 나는 이 useEffect가 마음에 안들었을까?

리렌더링 악몽
과거에 React를 제대로 학습하지 못하고 무자비하게 사용한 useEffect로 연쇄 리렌더링 악몽을 겪었다.
코드는 너무 길고 비즈니스 로직과 상태, useEffect가 강결합되어 코드 파악하는데 한 세월을 보냈다. useEffect가 난무하는 상황을 진정시키고 이벤트 핸들러로 어떻게든 해결하려고 했던 추억(?)이 있다.
초기 렌더링 사용 목적이 아니라면 useEffect 의존성 배열에 추적하는 상태 값을 명시해줘야 한다.
만약 이 때 사용하는 상태 값이 여러개라면?
의존성 배열의 모든 상태값이 각각 다른 비즈니스 로직에 연관이 되어있다면?
useEffect(() => {
// a 상태값 연관 로직
// b 상태값 연관 로직
// c 상태값 연관 로직
// ...
}, [a, b, c, d, ...])
물론 위의 경우, 관심사를 분리하면 문제가 되지 않는다.
하지만 모든 개발자가 그렇게 작업하진 않는다.
React에서 경고한 것처럼 상태 변화시 리렌더링 === useEffect 라고 생각하는 개발자들도 많다.
위와 같은 악몽으로,
useEffect를 여러번 사용한 찝찝함이 나를 괴롭혔다.
useEffect를 여러번 사용하는 것을 지양할 필요는 없다.
React는 useEffect를 외부 환경과 동기화하려고 만들었다. api서버, 브라우저, 라이브러리 등등.. 물론 기존의 클래스 컴포넌트의 불편함을 보완하려고 한 부분도 있다.
React가 이렇게 잘 만들어줬는데 잘 써줘야지. 근데 그래도 찝찝해.
그래서 동료에게 코드 리뷰를 받았다.
찝찝함을 덜어낼 수 있는 방법 몇 가지를 전수받았다.
useEffect말고 useRef로 특정 조건을 추가한 방법이다.
장점: useEffect 쓰지 않아도 된다.
단점: 비동기 처리에 필요한 로직은 예상치 않은 동작이 있을 수 있다. 동작 후 cleanup이 불가능하다.
function Component() {
const isFirstRender = useRef(true);
// 렌더링 될 때마다 실행되지만, 조건문으로 제어
if (isFirstRender.current) {
isFirstRender.current = false;
// 초기 렌더링 로직
...
}
return <div>Content</div>;
}
useRef나 useEffect를 그대로 활용하고 관심사를 분리하여 재사용성을 높힌 방법이다.
장점: 내부 로직과 데이터가 캡슐화 된다. 테스트에 용이하다.
단점: 내부 의존성 추적이 어렵다. 코드가 드러나지 않아 의도가 불명확하다.
export function useFirstRenderEffect(callback: () => void) {
const hasRun = useRef(false);
if (typeof window !== 'undefined' && !hasRun.current) {
callback();
hasRun.current = true;
}
}
// 사용
function ProductPage() {
useFirstRenderEffect(() => {
// 초기 렌더링 로직
...
});
return <div>{contents}</div>;
}
useEffect를 그대로 가져와서 선언적 컴포넌트로 만드는 방법이다.
장점: React 철학과 구조가 맞아 선언적이다.
단점: 컴포넌트인데 UI를 그리지 않는다. 의미 없는 노드를 생성한다. 훅이 더 나을지도..
function Mounted({ callback }: { callback: () => void }) {
useEffect(() => {
// 초기 렌더링 로직
...
},[])
return null
}
// 사용
return (
<>
<Mounted callback={function} /> // 명시적으로 사용
<Header />
<Body />
<Footer />
</>
제시한 방법 중 선택해서 사용하면 내 찝찝함을 덜어낼 수 있을 것이라 생각했다.
하지만 고민하고 좀 더 하고나서, 아무것도 채택하지 않았다.

세 번째 방법이 제일 매력적이게 다가왔다.
Provider처럼 UI를 반환하지 않는 래퍼 컴포넌트들도 있으니 충분히 사용할 만 했고 다른 컴포넌트에서 재사용하는 로직이다보니 명시적인게 좋아보였다.
하지만 나는 다시 고민했다.
작업 중인 컴포넌트에는 이미 커스텀훅으로 관심사 분리가 어느 정도 되어있었다.
useEffect가 몇 개 더 있다고 내부 상태들이 의존성에 얽혀 유지보수가 어려워 질 것 같지 않다.
내가 선호하지 않는 이유는 뭘까? 가독성 떄문에? 그저 지나간 과거의 악몽때문에?
대안들을 살펴보고 다시 보니 굳이? 라는 생각도 스쳐지나갔다.
그래서 초기 렌더링시 특정 동작을 하는 useEffect를 그 자리에 두었다.
해당 코드를 고치는 일은 없었다.
미래에 useEffect가 더 복잡한 로직과 의존성 배열로 골치 아파지는 일이 생긴다면, 그때는 선언적 컴포넌트를 활용해보자.
필요할 때 아래와 같이 여러 곳에서 활용할 수 있을 것이다.
useEffect.... 저도 사용하다가 예측하기 어려운 버그를 한두번 겪은게 아니라 최대한 사용안하고 있어요..
팀원들이랑 설계하다가 effect 써야되나 하면 다들 한숨부터 쉬심.. ㅋㅋㅋ
그래도 사용해야되는 경우는 있고 복잡한 effect 동작이라고하면 저도 항상 두번째, 세번째 방법을 사용하고 있어요!