며칠 전 코딩을 하면서 내 예상대로 렌더링이 되지 않는 경우가 있었다. 그리고 디버깅하는 과정에서
그동안 내가 잘못 예상하고 있었던 useEffect
의 실행 순서에 대해 알게 됐다.
useEffect
에 동작에 대해서 다시 한번 더 생각하는 기회가 됐다.
아래의 모든 글은 App
, OuterBox
, InnerBox
로 구성돼있다.
컴포넌트 구조는 App
컴포넌트부터 최상단 컴포넌트이다 App > OuterBox > InnerBox
export default function App() {
return (
<div className="App">
<h1>useEffect 순서 테스트</h1>
<OuterBox />
</div>
);
}
const OuterBox: FC = () => {
return (
<>
<h2>Outer BOX</h2>
<InnerBox />
</>
);
};
const InnerBox: FC = () => {
return <h2>Inner Box</h2>
};
결과부터 말하면 하위에 있는 컴포넌트 먼저 실행된다.
아래 코드를 보고 한번 결과를 예상해 보자.
지금까지 나는 아무생각없이 1 → 2 → 3 순서로 실행될 줄 알았다.
function App() {
useEffect(() => {
console.log(1);
}, []);
return ...
}
const OuterBox: FC = () => {
useEffect(() => {
console.log(2);
}, []);
return ...
};
const InnerBox: FC = () => {
useEffect(() => {
console.log(3);
}, []);
return ...
};
//실행결과
3
2
1
생각해 보면 당연했다.
useEffect
는 컴포넌트가 렌더링이 된 후에 실행되는 것이다. App
이 render
되기 위해서는 OuterBox
가 먼저 렌더링이 되어야 되고, OuterBox
가 완전히 렌더링 되기 위해서는 InnerBox
가 렌더링이 되어야 한다.
그러면 우리는 이제 알게 됐다. 하위에 있는 컴포넌트의 useEffect
가 먼저 실행되는구나
위의 코드에서 약간의 비동기 처리와 Suspense를 활용해 로딩 처리를 추가해 보겠다.
아래 코드에서 InnerBox
의 const repoStars = useRecoilValue(getStars);
는 단순히 깃허브 star 개수를 가져오는 비동기 요청이라고 생각하면 된다.
OuterBox
에서 로딩 처리를 위해 InnerBox
를 Suspense
로 감싸줬다.
아래 코드를 보고 한번 결과를 예상 해보자.
function App() {
useEffect(() => {
console.log(1);
}, []);
return ...
}
const OuterBox: FC = () => {
useEffect(() => {
console.log(2);
}, []);
return (
<>
<h2>Outer BOX</h2>
<Suspense fallback={<div>loading...</div>}>
<InnerBox />
</Suspense>
</>
);
};
const InnerBox: FC = () => {
//깃허브 stars 수를 갖고오는 비동기 요청
const repoStars = useRecoilValue(getStars);
useEffect(() => {
console.log(3);
}, []);
return ...
};
//실행결과
2
1
3
3번이 가장 마지막에 실행된다.
가장 하위 컴포넌트임에도 불구하고 가장 마지막에 실행되는 이유는 무엇일까??
useEffect
는 컴포넌트의 렌더링이 완료가 되면 실행되기 때문에 3이 가장 마지막에 실행된 것이다.
**InnerBox
에서 비동기 요청을 할 때 Suspense
한테 렌더링을 interrupt
** 당하기 때문에 OuterBox
, App
이 먼저 실행 되고 비동기 요청이 완료된 시점에 InnerBox
가 렌더링이 되면서 3이 출력된 것이다.
결국 기억할 것은 useEffect
는 컴포넌트의 렌더링이 끝나면 실행된다.
const InnerBox: FC = () => {
console.log(5);
// 비동기 요청
const repoStars = useRecoilValue(getStars);
console.log(4);
useEffect(() => {
console.log(3);
});
...
};
//실행결과
5
2
1
4
3
당연한 결과일 수 있겠지만 나는 조금 헷갈렸다. (5가 먼저 출력이 될까..? 라는 생각을 했다. )
이 결과로 인해 Suspense
는 비동기 요청을 만나는 그 순간 interrupt
해 간다는 것을 알 수 있다.
마지막으로 Suspense를 활용하지 않고 컴포넌트 내에서 로딩 처리했을 때의 결과를 알아보자.
마지막으로 Suspense
를 활용하지 않고 내부에서 로딩 처리를 했다.
recoil
의 useRecoilValueLoadable
를 활용해서 InnerBox
안에서 로딩 처리를 해줬다.
아래 코드를 보고 한번 결과를 예상해 보자.
// App, OuterBox는 위에와 같고 Suspense만 지워줬다.
// (Suspense를 지워도 결과는 같았다.)
const InnerBox: FC = () => {
const repoStarsLodable = useRecoilValueLoadable(getStars);
console.log(4);
useEffect(() => {
console.log(3);
});
return (
<>
{repoStarsLodable.state === "loading" && <div>loading...</div>}
{repoStarsLodable.state === "hasValue" && (
<>
<h2>Inner Box</h2>
<h3>내 레포 star 개수는 {repoStarsLodable.contents}</h3>
</>
)}
</>
);
};
//실행결과
4 //
3 // loading...일 때 출력되는 로그
2
1
4 //
3 // 로딩 후 출력되는 로그
InnerBox 내부에서 로딩까지 하면서 로딩 처리될 때도 출력이 되고 로딩이 끝나고 나서 4,3이 한 번 더 실행되는 결과를 볼 수 있다.
사실 useEffect
의 동작과정만 제대로 생각하면 바로 알 수 있는 것들이었다...
그래도 이제 알았으니 됐다!
덕분에 알아가네요! 잘 읽고갑니다!!