리액트의 대표적인 hooke 중 하나인 useEffect. 하지만, 최근 면접에서 실행시점을 모호하게 알고 있어 질문에 명쾌하게 대답하지 못하였다. 이번 시간에는 useEffect 실행 시점을 딥다이브한 내용을 다루어 보겠습니다.
본론으로 들어가기에 앞서,알아두어야하는 개념이 있다.
생명주기(Lifecycle)란 컴포넌트가 생성되고 변경되고 소멸될 때 까지 일련의 과정을 의미한다. 생명주기(Lifecycle)에서 사용되는 용어를 짚고 넘어가야 useEffect의 실행시점을 완벽하게 이해할 수 있고, useEffect를 남용하게 될 경우 발생하는 문제점을 정확하게 이해할 수 있다.
→ 컴포넌트가 웹브라우저에 추가되어 처음 그려질 때
→ 컴포넌트가 화면에서 다시 그려질 때(리렌더링될 때)
→ 컴포넌트가 웹브라우저에서 제거될 때
리액트 훅을 적용한 컴포넌트(함수형 컴포넌트)의 생명주기는 다음과 같다.
useEffect
는 함수형 컴포넌트에서 사이드 이펙트(side effect)를 처리하기 위해 사용되는 가장 대표적인 React Hook이다.
useEffect
는 컴포넌트가 마운트되거나 업데이트될 때 특정 작업을 실행하고, 언마운트될 때 정리(clean-up) 작업도 함께 수행할 수 있게 해주기 때문에 사이드 이펙트(side effect)를 다룰 때 사용된다.
useEffect(() => {
// 1️⃣ 실행할 작업 (mount 또는 update 시)
return () => {
// 2️⃣ 정리 함수 (unmount 시)
};
}, [deps]);
import { useState, useEffect } from "react";
export default function App() {
console.log("App 렌더링1");
const [count, setCount] = useState(0);
useEffect(() => {
console.log("useEffect 실행");
const intervalId = setInterval(() => {
console.log("인터벌 동작중");
setCount((prev) => prev + 1);
}, 1000);
return () => {
console.log("클린업 실행");
clearInterval(intervalId);
};
}, [count]);
console.log("App 렌더링2");
return (
<div>
<p>{count}</p>
</div>
);
}
App 렌더링 1 -> App 렌더링2 -> useEffect순으로 실행되는 것을 확인 할 수 있다.
즉, useEffect는 렌더링이 끝난 후 실행되는 것을 알 수 있다. 다시 말해, useEffect는 화면에 렌더링이 반영될 때까지 코드 실행을 “지연”시킨다.
위의 코드에서는 의존성 배열에 count를 넣어주었으니 count가 변경 될때마다 cleanup함수
가 실행될 것을 예상할 수 있다.
하지만, 여기서 주의해야될 부분이 있다. 위의 콘솔을 확인하면 알 수 있듯이 cleanup함수
는 리렌더링 이후(App 렌더링 1 → App 렌더링2 → 클린업 실행)에 실행된다는 것이다.
즉, 컴포넌트가 리렌더링 될 때, 재평가 => 언마운트 => 업데이트 순으로 진행된다는 것이다. cleanup함수
가 unmount 시 실행된다는 말이 헷갈린다면, useEffect가 재실행되기 직전에 cleanup함수
가 실행된다고 생각하면 될 것 같다.
import { useEffect } from "react";
const Inner = () => {
useEffect(() => {
console.log("Inner Mount");
}, []);
return <div> </div>;
};
const Outer = () => {
useEffect(() => {
console.log("Outer Mount");
}, []);
return (
<div>
<Inner />
</div>
);
};
function App() {
useEffect(() => {
console.log("App mount");
}, []);
return (
<div className="App">
<Outer />
</div>
);
}
export default App;
해당 코드는 App → Outer → Inner 구조로 컴포넌트가 중첩되어있다.
그런데 콘솔을 확인해보면, 렌더링은 topdown 방식으로 순차적으로 실행되는데 useEffect
는 가장 아랫쪽에 있는 Inner부터 역순으로 실행되는 것을 확인할 수 있다.
useEffect는 브라우저가 paint를 완료한 후 실행되는데, React는 내부적으로 깊이 우선 순회(DFS) 방식으로 렌더링을 진행하고, effect도 비슷하게 자식부터 부모 순서로 등록된 effect들을 실행한다.
즉, 가장 안쪽의 Inner의 effect가 가장 먼저 실행되고, 그 다음 Outer, 마지막으로 App순으로 실행한다.
useLayoutEffect
는 렌더링 되고 DOM이 업데이트된 직후, 브라우저가 실제로 화면에 그리기(paint) 전에실행되는 Hook이다. 이를 통해 레이아웃을 측정하거나, DOM을 즉시 조작할 필요가 있을 때 활용되곤 한다.
useEffect
→ paint 이후(비동기)
useLayoutEffect
→ paint 이전(동기)
useLayoutEffect
는 동기적이므로 무거운 작업을 하는 경우에는 화면 렌더링을 지연시킬 수 있으므로 주의가 필요하다.
import { useEffect, useLayoutEffect, useState } from "react";
import "./style.css";
export default function App() {
console.log("App Render Start");
const [isMount, setIsMount] = useState(false);
const handleClick = () => {
console.log("======= TOGGLE =======");
setIsMount((value) => !value);
};
return (
<div>
<button onClick={handleClick}>{isMount ? "UnMount" : "Mount"}</button>
{isMount && <Parent />}
</div>
);
}
function Parent() {
console.log("Parent Render Start");
useLayoutEffect(() => {
console.log("Parent useLayoutEffect");
return () => {
console.log("Parent useLayoutEffect cleanup");
};
}, []);
useEffect(() => {
console.log("Parent useEffect");
return () => {
console.log("Parent useEffect cleanup");
};
}, []);
return (
<div ref={(v) => console.log("Parent Ref Assign", v)} className="App">
<Child />
<div></div>
</div>
);
}
function Child() {
console.log("Child Render Start");
useLayoutEffect(() => {
console.log("Child useLayoutEffect");
return () => {
console.log("Child useLayoutEffect cleanup");
};
}, []);
useEffect(() => {
console.log("Child useEffect");
return () => {
console.log("Child useEffect cleanup");
};
}, []);
return <div ref={(v) => console.log("Child Ref Assign", v)}>Child</div>;
}
Child useEffect 실행 → "Child useEffect"
Parent useEffect 실행 → "Parent useEffect"
여기서 주의해야되는 점은 React는 부모 → 자식 순서로 트리를 순회하면서 DOM을 unmount하므로, 부모의 layoutEffect cleanup이 먼저 실행된다는 점이다.
Child useEffect 정리 → "Child useEffect cleanup"
Parent useEffect 정리 → "Parent useEffect cleanup"
위의 실행순서에 알 수 있듯이, useLayoutEffect cleanup
은 해당 컴포넌트의 DOM이 제거되기 바로 직전에 실행된다는 것을 알 수 있다.
useLayoutEffect
는 paint 직전에 실행되고, dom에서 제거되기 직전에 cleanup실행. → 동기
useEffect
는 컴포넌트 렌더링 이후(paint 이후)실행되고, 컴포넌트가 언마운트 or 렌더링이 일어난 이후 의존성이 변경되기 직전에 cleanup이 실행된다. → 비동기
mount시에는 child → parent순으로 실행되지만, ummout시에는 parent → child순으로 실행된다.