컴포넌트 라이프사이클
컴포넌트의 마운트·업데이트·언마운트 단계를 구분하고 useEffect의 의존성과 정리 함수 실행 시점을 익힙니다.
이제 리액트 컴포넌트가 화면에 나타나고, 업데이트되며, 최종적으로 사라지는 전체 과정, 즉 컴포넌트 라이프사이클(Lifecycle, 생명 주기)에 대해 이해해 볼 차례입니다.
컴포넌트 라이프사이클은 마치 사람이 태어나고(mount), 살아가며(update), 죽는(unmount) 과정과 같습니다.
리액트 컴포넌트도 이와 유사한 생명 주기를 가지며, 각 단계에서 특정 작업을 수행할 수 있도록 훅(Hook)을 제공합니다.
이 라이프사이클을 이해하는 것은 컴포넌트의 동작 방식을 더 깊이 이해하고, 필요한 시점에 적절한 로직을 실행하는 데 필수적입니다.
컴포넌트의 주요 라이프사이클 단계
리액트 컴포넌트는 크게 세 가지 주요 생명 주기 단계를 거칩니다.
- 컴포넌트가 처음으로 생성되어 실제 DOM에 삽입되는 단계입니다.
- 예시: 페이지 로딩 시 컴포넌트가 화면에 나타나는 순간
- 컴포넌트의
props나state가 변경되어 UI가 다시 그려지는 단계입니다. - 예시:
useState를 통해 상태가 변경되거나, 부모로부터 새로운props를 받을 때
- 컴포넌트가 더 이상 필요 없어져 실제 DOM에서 제거되는 단계입니다.
- 예시: 페이지 이동 시 컴포넌트가 화면에서 사라지는 순간
함수형 컴포넌트의 useEffect 훅
클래스형 컴포넌트에는 componentDidMount, componentDidUpdate, componentWillUnmount와 같은 생명 주기 메서드들이 존재했습니다.
함수형 컴포넌트에서는 useEffect 훅으로 타이머, 구독, 브라우저 API처럼 외부 시스템과의 동기화를 시작하고 정리할 수 있습니다. 모든 생명 주기 작업을 이 훅 하나로 대체한다는 뜻은 아닙니다.
useEffect 훅의 setup은 React가 화면에 변경을 반영한 커밋 이후 실행됩니다. 의존성이 같으면 재실행을 건너뛸 수 있으므로 컴포넌트 함수를 호출할 때마다 무조건 setup을 실행하는 것은 아닙니다.
주로 부수 효과(Side Effect)를 처리할 때 사용됩니다.
부수 효과(Side Effect)란?함수 내에서 입력(인자)을 처리하는 것 외에 외부에 영향을 주거나, 외부로부터 영향을 받는 작업을 말합니다.
리액트 컴포넌트에서의 부수 효과는 다음과 같은 것들이 있습니다.
- 데이터 가져오기 (API 호출)
- 구독(Subscription) 설정 및 해제
- DOM 직접 조작 (드물지만 필요할 때)
- 타이머 설정 및 해제 (
setTimeout,setInterval)
useEffect의 기본 구조는 다음과 같습니다.
import React, { useEffect } from 'react';
function Example({ dependency }) {
useEffect(() => {
// 컴포넌트가 렌더링된 후에 실행될 코드 (부수 효과)
return () => {
// 컴포넌트가 언마운트되거나 다음 이펙트가 실행되기 전에 정리합니다.
};
}, [dependency]);
return null;
}useEffect의 두 번째 인자인 의존성 배열(Dependency Array)은 useEffect가 언제 다시 실행될지를 제어하는 데 매우 중요합니다.
useEffect의 다양한 활용 패턴
의존성 배열의 값에 따라 useEffect는 마운트, 업데이트, 언마운트 시점에 다르게 동작합니다.
마운트 시에만 실행 (빈 의존성 배열 [])
빈 의존성 배열([])은 반응형 의존 값의 변경에 따른 재동기화를 하지 않는다는 뜻입니다. 개발 Strict Mode에서는 setup→cleanup→setup 점검이 추가될 수 있고 재마운트하면 다시 실행됩니다.
이는 클래스형 컴포넌트의 componentDidMount와 유사합니다.
Vite 프로젝트에서는 아래 JSX 컴포넌트를 .jsx로 저장합니다. 마지막 App.js 예제도 기존 App.jsx에 적용합니다.
import React, { useState, useEffect } from 'react';
function MountedComponent() {
const [data, setData] = useState(null);
useEffect(() => {
// 이 코드는 컴포넌트가 처음 화면에 나타날 때 (마운트될 때) 한 번만 실행됩니다.
console.log('MountedComponent가 마운트되었습니다!');
// 페이지 제목 변경 (브라우저 탭 제목)
document.title = 'React 앱 - 마운트됨';
// (가상) API에서 데이터 가져오기 (마운트 시 초기 데이터 로드)
const fetchData = async () => {
console.log('데이터를 불러오는 중...');
await new Promise(resolve => setTimeout(resolve, 2000)); // 2초 지연 시뮬레이션
setData('성공적으로 로드된 데이터입니다!');
console.log('데이터 로드 완료!');
};
fetchData();
// (선택적) 클린업 함수: 컴포넌트가 언마운트될 때 실행됩니다.
return () => {
console.log('MountedComponent가 언마운트될 예정입니다.');
document.title = 'React 앱'; // 제목을 원래대로 복원
};
}, []); // (1) 빈 의존성 배열: 마운트 시 한 번만 실행
return (
<div style={{ border: '1px solid #ff7f50', padding: '20px', margin: '20px', borderRadius: '8px' }}>
<h2>마운트 시 동작하는 컴포넌트</h2>
<p>{data ? data : '데이터 로드 중...'}</p>
</div>
);
}
export default MountedComponent;- 이 예제의 “한 번” 주석은 일반적인 최초 마운트 설명입니다. 개발 Strict Mode의 추가 점검까지 한 번으로 제한하지 않습니다. MountedComponent의 cleanup은 제목만 복원하며 진행 중인 지연 Promise를 취소하지 않습니다.
return () => { ... }내의 코드는 컴포넌트가 언마운트될 때(예:MountedComponent가 화면에서 사라질 때) 실행되는 클린업 함수입니다. 타이머 해제, 구독 해제 등 뒷정리 작업을 여기에 작성합니다.
의존성이 변경될 때마다 실행
컴포넌트의 특정 state나 props 값이 변경될 때마다 useEffect를 다시 실행하고 싶다면, 해당 state나 props를 의존성 배열에 포함합니다.
아래 코드는 Effect 실행 시점을 보여주기 위해 파생 메시지를 state로 복사합니다. 실제 UI에서는 count로부터 렌더 중 메시지를 계산해 불필요한 추가 렌더를 줄이는 편이 적합합니다.
import React, { useState, useEffect } from 'react';
function CounterWithEffect() {
const [count, setCount] = useState(0);
const [message, setMessage] = useState('');
// (1) count state가 변경될 때마다 이펙트 실행
useEffect(() => {
console.log(`카운트가 ${count}로 변경되었습니다.`);
setMessage(`현재 카운트 값은 ${count} 입니다.`);
// (선택적) 클린업: 이전 메시지 초기화
return () => {
console.log('이전 카운트 이펙트 정리');
// setMessage(''); // 필요하다면 메시지 초기화 로직
};
}, [count]); // (2) 의존성 배열에 count 추가
return (
<div style={{ border: '1px solid #4CAF50', padding: '20px', margin: '20px', borderRadius: '8px' }}>
<h2>카운터 (useEffect 활용)</h2>
<p>{message}</p>
<p>현재 값: {count}</p>
<button onClick={() => setCount(prevCount => prevCount + 1)}>
증가
</button>
<button onClick={() => setCount(0)} style={{ marginLeft: '10px' }}>
초기화
</button>
</div>
);
}
export default CounterWithEffect;useEffect의 의존성 배열에[count]를 넣었습니다. 이는countstate의 값이 변경될 때마다 이useEffect내부의 코드를 다시 실행하라는 의미입니다.console.log를 통해count가 변경될 때마다 로그가 찍히는 것을 확인할 수 있습니다.
매 렌더링마다 실행 (의존성 배열 생략)
의존성 배열을 아예 생략하면 useEffect는 컴포넌트의 렌더 결과가 커밋될 때마다 실행됩니다.
import React, { useState, useEffect } from 'react';
function AlwaysRunEffect() {
const [value, setValue] = useState(0);
useEffect(() => {
// 이 코드는 컴포넌트가 렌더링(리렌더링)될 때마다 실행됩니다.
// 무한 루프에 빠지지 않도록 주의해야 합니다. (state 업데이트를 여기서 직접하면 안 됨)
console.log('컴포넌트가 렌더링될 때마다 실행!', value);
}); // (1) 의존성 배열 생략
return (
<div style={{ border: '1px solid #8e44ad', padding: '20px', margin: '20px', borderRadius: '8px' }}>
<h2>항상 실행되는 useEffect</h2>
<p>값: {value}</p>
<button onClick={() => setValue(value + 1)}>값 증가</button>
</div>
);
}
export default AlwaysRunEffect;- 이 패턴은 흔히 사용되지는 않습니다. 주로 DOM 직접 조작이나 외부 라이브러리 연동 등, 컴포넌트의 모든 렌더링 주기마다 특정 작업을 수행해야 할 때 사용됩니다.
- 주의: 의존성 배열을 생략하고
useEffect내부에서state를 업데이트하면 무한 루프에 빠질 수 있습니다. (예:setValue(value + 1)을useEffect내부에서 직접 호출하면value가 변경 ->useEffect재실행 ->value변경 -> ... 무한 반복)
클린업(Clean-up) 함수 활용
useEffect는 선택적으로 return 문 안에 클린업 함수를 반환할 수 있습니다.
이 클린업 함수는 다음 두 가지 시점에 실행됩니다.
컴포넌트가 언마운트될 때 (화면에서 사라질 때)
이전 이펙트가 다시 실행되기 직전 (의존성 배열의 값이 변경되어 이펙트가 재실행될 때)
클린업 함수는 메모리 누수를 방지하거나 불필요한 네트워크 요청 등을 막는 데 사용됩니다.
주로 구독 해제, 타이머 해제, 이벤트 리스너 제거 등의 뒷정리 작업에 활용됩니다.
예제: 타이머 설정 및 해제import React, { useState, useEffect } from 'react';
function TimerComponent() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
console.log('타이머 시작');
// 1초마다 seconds state를 증가시키는 타이머 설정
const interval = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
// (1) 클린업 함수 반환: 컴포넌트가 언마운트되거나 이펙트 재실행 전에 타이머 해제
return () => {
console.log('타이머 클린업');
clearInterval(interval); // (2) 타이머 해제
};
}, []); // 빈 의존성 배열: 컴포넌트 마운트 시 한 번만 타이머 설정
return (
<div style={{ border: '1px solid #17a2b8', padding: '20px', margin: '20px', borderRadius: '8px' }}>
<h2>타이머 컴포넌트</h2>
<p>현재 시간: {seconds}초</p>
<p>이 컴포넌트가 화면에서 사라지면 타이머도 멈춥니다.</p>
</div>
);
}
export default TimerComponent;setInterval로 타이머를 시작하고, 반환된intervalID를 변수에 저장합니다.- 클린업 함수에서
clearInterval(interval)을 호출하여 타이머를 해제합니다. 아래App.js의 조건부 렌더링이 이 컴포넌트를 트리에서 제거할 때 타이머가 멈춥니다. CSS로 숨기거나 컴포넌트가null을 반환하는 것만으로 언마운트를 뜻하지는 않습니다. - 이
TimerComponent를App.js에 추가하고, 조건부 렌더링으로 이 컴포넌트가 나타났다가 사라지게 하면 클린업 함수가 실행되는 것을 볼 수 있습니다. (예: 토글 버튼으로TimerComponent의 렌더링 여부 제어)
모든 컴포넌트 한곳에서 테스트
App.js 파일을 수정하여 위에서 만든 컴포넌트들을 모두 불러와 렌더링하고, 콘솔을 보면서 useEffect의 동작 방식을 직접 확인해 보세요.
특히 MountedComponent나 TimerComponent처럼 조건부 렌더링으로 렌더링 여부를 제어하면 마운트/언마운트 시의 useEffect 동작을 명확히 볼 수 있습니다.
import React, { useState } from 'react';
import './App.css';
import MountedComponent from './components/MountedComponent';
import CounterWithEffect from './components/CounterWithEffect';
import AlwaysRunEffect from './components/AlwaysRunEffect';
import TimerComponent from './components/TimerComponent'; // 추가
function App() {
const [showMounted, setShowMounted] = useState(true);
const [showTimer, setShowTimer] = useState(true); // TimerComponent 제어용
return (
<div className="App">
<h1>React 컴포넌트 라이프사이클과 useEffect</h1>
<button onClick={() => setShowMounted(!showMounted)} style={{ margin: '10px', padding: '10px 20px' }}>
MountedComponent {showMounted ? '숨기기' : '보이기'}
</button>
{showMounted && <MountedComponent />} {/* 조건부 렌더링 */}
<hr />
<CounterWithEffect />
<hr />
<AlwaysRunEffect />
<hr />
<button onClick={() => setShowTimer(!showTimer)} style={{ margin: '10px', padding: '10px 20px' }}>
TimerComponent {showTimer ? '숨기기' : '보이기'}
</button>
{showTimer && <TimerComponent />} {/* 조건부 렌더링 */}
</div>
);
}
export default App;이벤트와 Effect를 나누어 따라가기
useEffect의 추가 개발 점검이 없는 실행을 기준으로 CounterWithEffect의 처음 count는 0입니다. 첫 커밋 뒤 setup이 count 0을 기록하고 안내 메시지를 갱신합니다. 메시지 갱신으로 다시 렌더해도 의존성인 count는 그대로 0이므로 setup을 다시 실행하지 않습니다. 증가 버튼을 한 번 눌러 count가 1이 되면, 이전 count 0의 cleanup 뒤에 count 1의 setup이 실행됩니다.
개발 Strict Mode에서는 최초 setup → cleanup → setup 점검을 추가로 볼 수 있습니다. 따라서 첫 로그 개수만 외워 판단하지 말고, 어느 count를 사용한 setup과 cleanup인지 짝을 지어 확인합니다. 원래 MountedComponent의 cleanup은 페이지 제목을 돌릴 뿐이며, 이미 시작한 2초 지연 Promise를 취소하지 않습니다. 타이머 해제와 다른 종류의 작업 정리는 각각 구현해야 합니다.
독립 연습
- count가 1일 때 초기화하여 0으로 바꾸면 어느 count로 실행된 setup을 정리하나요? 새 setup은 어떤 count를 읽나요? 이미 0일 때 다시 초기화한 경우와 구별하세요.
TimerComponent가 3초를 표시할 때 숨기기 버튼으로 트리에서 제거하고 다시 보입니다. 새 타이머는 어느 값에서 시작하나요? 표시된 시간이 늘 때마다 기존 interval을 다시 만드는지도 설명하세요.
의존성과 상태 수명 확인
- count 1로 실행된 setup에 대응하는 cleanup 뒤에 count 0의 setup입니다. 원래 cleanup 로그에는 count 값이 없으므로 전후 setup 로그와 짝지어 읽습니다. 이미 0이면 count 의존성에 변화가 없어 해당 이유로 cleanup/setup 쌍을 다시 실행하지 않습니다.
- 새로 마운트된 컴포넌트의
useState(0)부터 시작합니다.[]인 Effect는 seconds의 변화에 의존하지 않으며, interval 콜백의 함수형 updater가 직전 seconds를 증가시킵니다. 이전 인스턴스를 제거할 때 clearInterval로 정리하므로 새 interval과 함께 계속 돌게 두지 않습니다.
React의 Effect 설명은 외부 시스템 동기화, 의존성에 따른 재실행, cleanup과 개발 점검을 구분합니다.
라이프사이클을 이해할 때는 언제 실행되는지만 외우기보다, 어떤 외부 자원을 시작하고 언제 정리해야 하는지 함께 봐야 합니다.
Effect setup과 cleanup 순서 — 상태 추적
interval 생성·seconds callback·부모 트리 제거·clearInterval에서 자원 존재와 표시가 달라진다. [] setup이 매 증가마다 실행되지 않음을 보여 줄 가치가 있다.
EFFECT: SETUP & CLEANUP
Effect를 교체하거나 제거할 때 이전 setup의 자원을 정리합니다
Effect는 커밋 후 외부 동기화를 시작합니다. 본문의 TimerComponent는 타이머를 만들고 정리하며, CounterWithEffect는 count 변경 시 이전 cleanup 뒤 새 setup을 실행합니다.
TimerComponent · []
setup: interval 생성
tick: seconds의 이전 값 +1
unmount: clearInterval(interval)초가 바뀌어도 [] Effect를 매초 교체하지는 않습니다. 타이머의 updater가 다음 seconds를 계산합니다.
CounterWithEffect · [count]
count 0 커밋 → setup(0)
count 1 커밋 → cleanup(0)
→ setup(1)
unmount → cleanup(1)의존성은 Object.is로 비교합니다. 값이 같으면 해당 변경에 따른 재동기화를 건너뜁니다.
배열 생략 / 개발 Strict Mode
배열을 생략하면 매 커밋 후 setup, 필요 시 이전 cleanup이 실행됩니다. 개발 Strict Mode는 최초 setup→cleanup→setup 점검을 추가할 수 있어 []가 절대 한 번이라는 뜻은 아닙니다.
Effect에 넣을 작업의 경계
외부 연결·타이머는 setup과 cleanup을 짝지어 다룹니다. count에서 메시지를 계산하는 일은 렌더에, 클릭 작업은 핸들러에 둘 수 있습니다. MountedComponent의 cleanup은 제목만 복원하고 지연 Promise를 취소하지 않습니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | TimerComponent의 마운트 후 setup에서 interval 생성→콜백에 따른 seconds 표시 갱신→부모 트리 제거 시 cleanup에서 interval 해제의 노드 3개·간선 2개다. []이므로 seconds 갱신마다 setup을 재실행하지 않는다. 숨김 CSS나 null 출력만으로 제거된다고 가정하지 않는다. |
| 전체 관계·조건 설명 | TimerComponent의 마운트 후 setup에서 interval 생성→콜백에 따른 seconds 표시 갱신→부모 트리 제거 시 cleanup에서 interval 해제의 노드 3개·간선 2개다. []이므로 seconds 갱신마다 setup을 재실행하지 않는다. 숨김 CSS나 null 출력만으로 제거된다고 가정하지 않는다. 모바일은 같은 노드와 방향을 다른 배치로 보여 줍니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 마운트 setup | interval: 존재 / deps: [] | Effect가 interval을 만들고 현재 seconds를 표시한다 | setup은 seconds 갱신마다 반복되지 않는다 |
| callback tick | seconds: 이전+1 / interval: 같은 timer | updater가 seconds를 증가시키고 화면을 갱신한다 | 화면 갱신과 자원 재생성은 다르다 |
| 트리 제거 | interval: 해제 / component: 트리에서 없음 | 부모가 실제로 제거하면 cleanup이 clearInterval을 호출한다 | CSS 숨김이나 null 출력만으로 unmount를 가정하지 않는다 |
Effect setup과 cleanup 순서 — 상태 추적
interval 생성·seconds callback·부모 트리 제거·clearInterval에서 자원 존재와 표시가 달라진다. [] setup이 매 증가마다 실행되지 않음을 보여 줄 가치가 있다.
EFFECT: SETUP & CLEANUP
Effect를 교체하거나 제거할 때 이전 setup의 자원을 정리합니다
Effect는 커밋 후 외부 동기화를 시작합니다. 본문의 TimerComponent는 타이머를 만들고 정리하며, CounterWithEffect는 count 변경 시 이전 cleanup 뒤 새 setup을 실행합니다.
TimerComponent · []
setup: interval 생성
tick: seconds의 이전 값 +1
unmount: clearInterval(interval)초가 바뀌어도 [] Effect를 매초 교체하지는 않습니다. 타이머의 updater가 다음 seconds를 계산합니다.
CounterWithEffect · [count]
count 0 커밋 → setup(0)
count 1 커밋 → cleanup(0)
→ setup(1)
unmount → cleanup(1)의존성은 Object.is로 비교합니다. 값이 같으면 해당 변경에 따른 재동기화를 건너뜁니다.
배열 생략 / 개발 Strict Mode
배열을 생략하면 매 커밋 후 setup, 필요 시 이전 cleanup이 실행됩니다. 개발 Strict Mode는 최초 setup→cleanup→setup 점검을 추가할 수 있어 []가 절대 한 번이라는 뜻은 아닙니다.
Effect에 넣을 작업의 경계
외부 연결·타이머는 setup과 cleanup을 짝지어 다룹니다. count에서 메시지를 계산하는 일은 렌더에, 클릭 작업은 핸들러에 둘 수 있습니다. MountedComponent의 cleanup은 제목만 복원하고 지연 Promise를 취소하지 않습니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | TimerComponent의 마운트 후 setup에서 interval 생성→콜백에 따른 seconds 표시 갱신→부모 트리 제거 시 cleanup에서 interval 해제의 노드 3개·간선 2개다. []이므로 seconds 갱신마다 setup을 재실행하지 않는다. 숨김 CSS나 null 출력만으로 제거된다고 가정하지 않는다. |
| 전체 관계·조건 설명 | TimerComponent의 마운트 후 setup에서 interval 생성→콜백에 따른 seconds 표시 갱신→부모 트리 제거 시 cleanup에서 interval 해제의 노드 3개·간선 2개다. []이므로 seconds 갱신마다 setup을 재실행하지 않는다. 숨김 CSS나 null 출력만으로 제거된다고 가정하지 않는다. 모바일은 같은 노드와 방향을 다른 배치로 보여 줍니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 마운트 setup | interval: 존재 / deps: [] | Effect가 interval을 만들고 현재 seconds를 표시한다 | setup은 seconds 갱신마다 반복되지 않는다 |
| callback tick | seconds: 이전+1 / interval: 같은 timer | updater가 seconds를 증가시키고 화면을 갱신한다 | 화면 갱신과 자원 재생성은 다르다 |
| 트리 제거 | interval: 해제 / component: 트리에서 없음 | 부모가 실제로 제거하면 cleanup이 clearInterval을 호출한다 | CSS 숨김이나 null 출력만으로 unmount를 가정하지 않는다 |
3장 4절 컴포넌트 라이프사이클 이해하기는 여기까지입니다.
컴포넌트의 세 가지 주요 생명 주기 단계와 외부 시스템을 동기화하는 useEffect 훅, 의존성 배열, 클린업 함수의 역할을 살펴보았습니다.
이제는 컴포넌트가 화면에 나타나고 사라지는 시점에 필요한 작업을 수행할 수 있게 되었습니다.
다음 절에서는 지금까지 배운 개념을 묶어 간단한 할 일 목록 앱 만들기 실습을 진행하겠습니다.