며칠 전 코딩을 하면서 내 예상대로 렌더링이 되지 않는 경우가 있었다. 그리고 디버깅하는 과정에서
그동안 내가 잘못 예상하고 있었던 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의 동작과정만 제대로 생각하면 바로 알 수 있는 것들이었다...
그래도 이제 알았으니 됐다!
덕분에 알아가네요! 잘 읽고갑니다!!