함수형 컴포넌트
함수형 컴포넌트에서의 생명주기에 대해 알아보겠습니다. 함수형 컴포넌트는 클래스 컴포넌트와 달리 생명주기 메서드를 직접적으로 사용하지 않습니다. React 16.8 버전 이전에는 함수형 컴포넌트에서 state와 생명주기 기능을 사용하기 어려웠습니다. 그러나 React 16.8 버전부터 Hook이 도입되어 클래스를 작성하지 않고도 함수형 컴포넌트에서 state와 생명주기와 유사한 기능을 사용할 수 있게 되었습니다.
함수형 컴포넌트의 생명주기를 이해하기 위해서는 Hook의 동작과 실행 순서를 이해하는 것이 중요합니다. 이를 통해 컴포넌트의 렌더링 과정과 부수 효과(side effects)를 더 효과적으로 관리할 수 있습니다.
1. Hook Flow
아래에서 설명할 함수형 컴포넌트의 흐름도를 React Hook Flow라고 부르겠습니다. 이는 Hook을 기반으로 렌더링되는 과정을 나타냅니다. 단, 이는 공식 명칭이 아니라 이해를 돕기 위한 용어임을 유의해주세요.

1.1 컴포넌트 생애주기
컴포넌트의 생명주기는 크게 세 단계로 나눌 수 있습니다: 마운트(mount), 업데이트(update), 언마운트(unmount)입니다. 마운트는 컴포넌트가 처음 DOM에 렌더링될 때를 의미합니다. 업데이트는 마운트 완료 이후 state나 props의 변화가 생겼을 때 React가 이를 감지하고 컴포넌트를 재렌더링하는 과정을 의미합니다. 언마운트는 DOM에서 컴포넌트가 제거되는 시점을 의미합니다.
각 생명주기 단계에 따른 동작은 다음과 같습니다.
1.1.1 마운트 (Mount)
Run lazy initializersrenderReact updates DOMRun LayoutEffectsBrowser paints screenRun Effects
1.1.2 업데이트(Update)
renderReact updates DOMCleanup LayoutEffectsRun LayoutEffectsBrowser paints screenCleanup EffectsRun Effects
1.1.3 언마운트(Unmount)
Cleanup LayoutEffectsCleanup Effects
1.2 동작 상세 설명
흐름별 상세 동작을 살펴보겠습니다.
- Run lazy initializers
최초 마운트 시에 단 1번 실행되는 동작으로, 컴포넌트가 만들어질 때 props와 state 등의 값을 초기화합니다.
- Render
엘리먼트를 생성하고 가상 DOM을 구성합니다. 이 과정에서 컴포넌트 함수가 호출되고, 반환된 JSX가 React 엘리먼트로 변환됩니다. 이는 렌더 단계의 일부로, 순수하고 부수 효과가 없어야 합니다.
- React updates DOM
이 단계는 커밋 단계의 일부입니다. 가상 DOM과 실제 DOM을 비교하여 필요한 부분만 실제 DOM에 업데이트합니다. 이 과정은 효율적인 DOM 조작을 위해 최적화되어 있습니다.
- Cleanup LayoutEffects
업데이트와 언마운트 과정에서 실행됩니다. 업데이트 시에는 새로운 LayoutEffect 실행 전에, 언마운트 시에는 컴포넌트 제거 전에 진행됩니다. 이전 렌더의 LayoutEffect 정리 함수가 이 시점에 실행됩니다.
- Run LayoutEffects
마운트와 업데이트 과정에서 DOM 변경 직후, 그러나 브라우저가 화면을 그리기 전에 동기적으로 실행됩니다. useLayoutEffect Hook이 이 시점에 실행됩니다. DOM 측정이 필요한 경우나 DOM 변경이 화면에 반영되기 전에 수행해야 하는 작업에 사용됩니다.
- Browser paints screen
브라우저가 실제 DOM을 화면에 그립니다. 이 단계에서 사용자는 변경된 UI를 볼 수 있게 됩니다.
- Cleanup Effects
업데이트와 언마운트 과정에서 실행됩니다. 새로운 Effect 실행 전이나 컴포넌트 제거 전에 이전 Effect의 정리(cleanup) 함수를 실행합니다. 이를 통해 이전 효과로 인한 부수 효과를 정리할 수 있습니다.
- Run Effects
마운트와 업데이트 과정에서 컴포넌트가 브라우저에 그려진 후에 비동기적으로 실행됩니다. useEffect Hook이 이 시점에 실행됩니다.
Hook Flow를 이해하면 함수형 컴포넌트에서 부수 효과를 더 효과적으로 관리하고, 성능 최적화를 할 수 있습니다.
2. useEffect와 useLayoutEffect의 차이
앞서 살펴본 Hook Flow를 보면 LayoutEffects와 Effects의 동작이 페인트를 기준으로 실행되는 시점에 차이가 있다는 것을 알 수 있습니다. 즉, useLayoutEffect와 useEffect 훅의 실행 시점이 다릅니다. 이 두 훅의 차이점을 좀 더 자세히 알아보겠습니다.
2.1 useEffect 실행 시점
Hook Flow에서 Cleanup Effects와 Run Effects는 브라우저가 화면을 그린 이후에 동작합니다. 즉, useEffect는 브라우저가 DOM을 업데이트하고 화면을 다시 그린 후에 비동기적으로 실행됩니다. 이로 인해 가끔 useState로 생성한 state 값이 초기값이나 이전 state 값으로 화면에 잠시 표시되었다가, useEffect 내의 setState로 설정한 새로운 값으로 업데이트되면서 화면이 깜빡거리는 현상(flickering)이 발생할 수 있습니다.
예시 코드를 통해 깜빡거리는 현상을 확인해보겠습니다. 먼저 Vite를 사용하여 프로젝트를 생성합니다.
npm create vite@latest my-app -- --template react
cd my-app # my-app 프로젝트로 이동
npm install # 의존성 설치
App.jsx
import React, { useEffect, useState } from 'react';
function App() {
const [value, setValue] = useState(0);
useEffect(() => {
// 2초 후 state 값을 업데이트
setTimeout(() => {
setValue((prevValue) => prevValue + 1000);
}, 2000);
}, []);
return (
<div style={{ margin: '10px' }}>
<h1>깜빡거리는 현상이 보이시나요?</h1>
<p>{value}</p>
</div>
);
}
export default App;
useEffect의 콜백 함수는 의존성 배열이 비어있어 컴포넌트가 마운트될 때 한 번만 실행됩니다. value state의 초기값은 0입니다. useEffect는 렌더링과 페인트 이후에 실행되므로 첫 화면에는 value가 0으로 표시됩니다. setTimeout 함수를 이용하여 value의 값 업데이트가 2초 지연 후에 발생하도록 하였습니다. 따라서 초기값 0에서 1000으로 바뀌는 것을 확인할 수 있습니다.
| 첫 화면 | 2초 후 화면 |
|---|---|
![]() | ![]() |
실제 상황에서도 데이터를 가져오거나 복잡한 계산을 통해 상태를 업데이트하면서 깜빡거림 현상이 발생할 수 있습니다. 이러한 깜빡거리는 문제를 해결하기 위해 useLayoutEffect를 사용할 수 있습니다.
2.2 useLayoutEffect 실행 시점
useLayoutEffect 훅은 렌더링이 발생하고 브라우저가 화면을 그리기 전에 실행됩니다. useLayoutEffect는 동기적으로 동작하기 때문에 useLayoutEffect 콜백 함수가 모두 실행된 후 페인트가 진행됩니다. 따라서 useLayoutEffect 내에서 시간이 오래 걸리는 작업을 수행하면 화면 렌더링이 지연될 수 있습니다.
App.jsx
import React, { useLayoutEffect, useState } from 'react';
function App() {
const [value, setValue] = useState(0);
useLayoutEffect(() => {
setValue((prevValue) => prevValue + 1000);
}, []);
return (
<div style={{ margin: '10px' }}>
<h1>깜빡거리는 현상이 보이시나요?</h1>
<p>{value}</p>
</div>
);
}
export default App;
useLayoutEffect의 콜백 함수는 의존성 배열이 비어있어 컴포넌트가 마운트될 때 한 번만 실행됩니다. value의 값 업데이트는 브라우저에 화면이 그려지기 전에 동작하기 때문에 사용자는 초기값 0을 볼 수 없습니다. 화면에는 업데이트된 값인 1000이 바로 표시되어 깜빡거림 현상이 발생하지 않습니다.
위 예시에서 useLayoutEffect가 어떻게 깜빡거림 문제를 해결하는지 확인할 수 있습니다. 하지만 실제 애플리케이션에서는 useLayoutEffect 내에서 복잡한 계산이나 API 호출 등을 수행할 경우 화면 렌더링이 지연될 수 있으므로, 사용 시 주의가 필요합니다. 대부분의 경우 useEffect로 충분하며, 레이아웃 계산이나 DOM 측정과 같이 렌더링 직후 즉시 실행해야 하는 경우에만 useLayoutEffect를 사용하는 것이 좋습니다.
3. Hook Flow 확인하기
예제를 통해 콘솔창의 로그를 확인하면서 Hook Flow를 확인해보겠습니다.
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
App.jsx
import React, { useEffect, useLayoutEffect, useState } from 'react';
function HookFlow() {
const [count, setCount] = useState(0);
const onClick = () => {
setCount((prevCount) => prevCount + 1);
};
useLayoutEffect(() => {
console.log('LayoutEffect 1: 마운트 시 브라우저에 그려주기 전 실행');
return () => {
console.log('LayoutEffect Cleanup 1: 언마운트 시 실행');
};
}, []);
useLayoutEffect(() => {
console.log(
'LayoutEffect 2: 마운트/업데이트 시마다 브라우저에 그려주기 전 실행',
);
return () => {
console.log(
'LayoutEffect Cleanup 2: 업데이트 직전 또는 언마운트 시 실행',
);
};
});
useLayoutEffect(() => {
console.log(
'LayoutEffect 3: 마운트 시 또는 num 변경으로 업데이트 시 브라우저에 그려주기 전 실행',
);
return () => {
console.log(
'LayoutEffect Cleanup 3: num 변경으로 인한 업데이트 직전 또는 언마운트 시 실행',
);
};
}, [count]);
// useLayoutEffect가 모두 끝난 후에야 브라우저에 그려주기 시작합니다.
useEffect(() => {
console.log('Effect 1: 마운트 시 브라우저에 그려진 후 실행');
return () => {
console.log('Effect Cleanup 1: 언마운트 시 실행');
};
}, []);
useEffect(() => {
console.log('Effect 2: 마운트/업데이트 시마다 브라우저에 그려진 후 실행');
return () => {
console.log('Effect Cleanup 2: 업데이트 직전 또는 언마운트 시 실행');
};
});
useEffect(() => {
console.log(
'Effect 3: 마운트 시 또는 num 변경으로 업데이트 시 브라우저에 그려진 후 실행',
);
return () => {
console.log(
'Effect Cleanup 3: num 변경으로 인한 업데이트 직전 또는 언마운트 시 실행',
);
};
}, [count]);
return <button onClick={onClick}>{count}</button>;
}
function App() {
const [isVisible, setIsVisible] = useState(true);
const handleClick = () => {
setIsVisible(!isVisible);
};
return (
<div style={{ margin: '20px' }}>
<button onClick={handleClick}>
{isVisible ? '언마운트시키기' : '마운트시키기'}
</button>
<br />
{isVisible && <HookFlow />}
</div>
);
}
export default App;
App 컴포넌트에는 HookFlow 컴포넌트를 마운트하거나 언마운트할 수 있는 버튼이 있습니다. HookFlow 컴포넌트를 마운트시키면 count를 1씩 증가시키는 숫자 버튼이 나탑니다. 언마운트시키면 숫자 버튼이 사라집니다.
3.1 최초 마운트 시
isVisble state는 초기값이 true이므로 처음 App컴포넌트가 마운트될 때 HookFlow 컴포넌트도 마운트됩니다.
콘솔창으로 로그를 확인하여 Hook Flow를 확인해보겠습니다.
useLayoutEffect와 useEffect가 모두 실행된 것을 확인할 수 있습니다. useLayoutEffect가 실행된 후 useEffect가 실행됩니다. 이는 useEffect 함수를 useLayoutEffect 함수보다 먼저 작성하여도 콘솔 창에서는 useLayoutEffect의 메세지가 먼저 출력되고 useEffect 함수 메세지가 출력되는 것으로 알 수 있습니다.
3.2 업데이트 시
숫자 버튼을 누르면 count 값이 1씩 증가하면서 업데이트가 일어납니다. 로그를 확인하여 보겠습니다.
의존성 배열이 비어있어 마운트 시에만 실행되는 첫 번째 useLayoutEffect와 첫 번째 useEffect가 업데이트 시에는 실행되지 않았다는 것을 알 수 있습니다. LayoutEffect와 Effect가 진행되기 전에 클린업 함수가 실행되는 것도 확인할 수 있습니다. 클린업 함수가 실행되고 콜백함수가 실행됩니다.
3.3 언마운트 시
언마운트 시키기 버튼을 클릭하여 HookFlow 컴포넌트를 언마운트 시켜보겠습니다.
| 첫 화면 | 2초 후 화면 |
|---|---|
![]() | ![]() |
콘솔창을 열어 로그를 확인해보겠습니다.
모든 useLayoutEffect의 클린업 함수와 useEffect의 클린업 함수가 동작한 것을 알 수 있습니다.
React Hook이 라이프 사이클 메서드는 아닙니다. 클래스 컴포넌트에서는 라이프 사이클 메서드를 사용할 수 있었지만, 함수형 컴포넌트는 그러지 못합니다. 대신 Hook을 통해 컴포넌트의 생명주기를 쉽게 이해하고 유사한 기능을 사용할 수 있습니다.



