리액트 훅의 useEffect를 사용할 때 꼭 알아야 할 네 가지를 공유하려고 한다.
리액트 훅을 사용할 때 복수의 useEffect 함수를 사용할 수 있다. 하지만 할 수 있다고 다 좋은 것은 아니다. 클린 코드의 관점에서는 함수는 한 가지 목적을 가지고 있어야 하기 때문이다. (글을 쓴다면 한 문장에 한 개의 아이디어만 전달해야 하는 이유와 같다)
useEffect를 작고 단순한 단일 목적의 함수로 분리한다면 의도치 않은 이펙트 함수의 실행을 방지할 수 있다. 물론 디펜던시 배열도 사용해야 한다.
우선, useEffect(그리고 setTimeout)를 이용해 재귀적인 카운터를 구현하는 나쁜 코드를 작성해보자. 내부에서는 서로 관련이 없는 varA
와 varB
변수를 사용한다.
우선 나쁜 코드를 먼저 작성한다.
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>
);
}
보다시피, 한번 상태 변경으로 varA
와 varB
두 상태 변수에 업데이트가 발생하게 된다. 그래서 이 훅은 정상적으로 동작하지 않는다.
예제는 짧은 코드기 때문에 문제점이 명확하게 보이지만 더 많은 코드와 더 많은 변수를 갖고 있는 긴 함수였다면 문제를 발견하기 힘들 것이다. 그러니 옳은 방향으로 useEffect를 분리해 수정해보자.
이런 경우라면 코드는 아래와 같이 작업해야 한다.
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의 잘못된 사용에 쉽게 대해 이해시키고자 만들어진 코드다. 보통 변수가 이전 상태에 의존을 갖고 있다면 setVarA(varA => varA + 1)같은 방법을 이용해야 한다.(이 노트를 추가하도록 도움을 준 @Michael Landis 에게 감사의 뜻을 표한다)*
상단의 예제를 다시 살펴보자. 만약 varA
와 varB
가 완전히 독립적이라면 어땠을까?
커스텀 훅으로 만든다면 두 상태 변수를 완전히 독립적으로 만들 수 있다. 그리고 이 방법은 각 함수들이 어떤 변수를 사용하는지도 쉽게 파악할 수 있다.
자 커스텀 훅을 만들어 보자
function App() {
const [varA, setVarA] = useVarA();
const [varB, setVarB] = useVarB();
return (
<span>
Var A: {varA}, Var B: {varB}
</span>
);
}
function useVarA() {
const [varA, setVarA] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => setVarA(varA + 1), 1000);
return () => clearTimeout(timeout);
}, [varA]);
return [varA, setVarA];
}
function useVarB() {
const [varB, setVarB] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => setVarB(varB + 2), 2000);
return () => clearTimeout(timeout);
}, [varB]);
return [varB, setVarB];
}
이제 각 변수들은 고유의 훅을 갖게 되었다. 훨씬 더 유지 보수가 쉽고 읽기도 쉽다.
setTimeout을 사용했던 예제에서 다음의 코드를 살펴보자.
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>;
}
특정한 이유로 카운터를 최대 5개까지로 제한한다고 해보자. 이를 구현하는 옳은 방법과 잘못된 방법을 각각 살펴보자.
먼저 잘못된 방법부터 살펴보자.
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
은 조건적으로 실행되는 반면 clearTimeout
은 varA
가 변경될 때마다 매번 실행된다. 큰 문제는 없겠지만 정상적인 작업은 아니다.
useEffect
를 조건적으로 사용하는 이상적인 방법은 함수의 초기에 바로 반환해버리는 것이다. 아래와 같이 말이다.
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>;
}
이 방법은 Material UI에서도(그리고 다른 많은 곳에서) 사용하고 있으며 의도치 않은 useEffect
의 실행을 막을 수 있다.
ESLint를 사용하고 있다면 아마도 exhaustive-deps 규칙의 경고를 본 적이 있을 것이다.
매우 중요한 규칙이다. 앱이 점점 커질질수록 useEffect
에는 더 많은 디펜던시가 추가될 수 있다. 모든 디펜던시의 변화를 감지해 깨진 클로저가 만들어지는 것을 피하려면 모든 디펜던시를 디펜던시 배열에 추가해야 한다.(이 주제에 관한 공식 문서)
다시 한번 setTimeout
을 사용했던 예제에서 setTimeout
을 단 한 번만 실행해 varA
를 증가시킨다고 해보자.
아마 아래와 같은 잘못된 예제의 코드를 작성할 수도 있다.
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>;
}
위 코드가 비록 의도했던 대로 동작한다 하더라도 잠깐 생각해보자. "만약 코드가 더 커진다면?" 혹은 "위의 코드를 변경해야 한다면?" 어떨까 말이다.
그런 상황을 위해서, 사용되는 모든 변수들을 디펜던시 배열에 추가해야 한다. 그래야 테스트하기 수월해지고 발생할 수 있는 문제를 쉽게 탐지할 수 있다. (깨진 프롭이나 클로저 등)
옳은 방법은 아래와 같다.
function App() {
const [varA, setVarA] = useState(0);
useEffect(() => {
if (varA > 0) return;
const timeout = setTimeout(() => setVarA(varA + 1), 1000);
return () => clearTimeout(timeout);
}, [varA]); // 이렇게 해야한다. 모든 디펜더시가 배열에 들어가 있다.
return <span>Var A: {varA}</span>;
}
이상이다. 질문이나 제안사항이 있다면, 댓글로 남겨주길 바란다. 꼭 확인하겠다.