컴포넌트 라이프사이클 이해하기
이제 리액트 컴포넌트가 화면에 나타나고, 업데이트되며, 최종적으로 사라지는 전체 과정, 즉 컴포넌트 라이프사이클(Lifecycle, 생명 주기) 에 대해 이해해 볼 차례입니다.
컴포넌트 라이프사이클은 마치 사람의 '태어나고(mount), 살아가며(update), 죽는(unmount)' 과정과 같습니다. 리액트 컴포넌트도 이와 유사한 생명 주기를 가지며, 각 단계에서 특정 작업을 수행할 수 있도록 훅(Hook)을 제공합니다. 이 라이프사이클을 이해하는 것은 컴포넌트의 동작 방식을 더 깊이 이해하고, 필요한 시점에 적절한 로직을 실행하는 데 필수적입니다.
컴포넌트의 주요 라이프사이클 단계
리액트 컴포넌트는 크게 세 가지 주요 생명 주기 단계를 거칩니다.
마운트(Mounting): 컴포넌트 생성 및 DOM 삽입
- 컴포넌트가 처음으로 생성되어 실제 DOM에 삽입되는 단계입니다.
- 예시: 페이지 로딩 시 컴포넌트가 화면에 나타나는 순간
업데이트(Updating): 컴포넌트 재렌더링
- 컴포넌트의
props
나state
가 변경되어 UI가 다시 그려지는 단계입니다. - 예시:
useState
를 통해 상태가 변경되거나, 부모로부터 새로운props
를 받을 때
언마운트(Unmounting): 컴포넌트 제거 및 DOM에서 삭제
- 컴포넌트가 더 이상 필요 없어져 실제 DOM에서 제거되는 단계입니다.
- 예시: 페이지 이동 시 컴포넌트가 화면에서 사라지는 순간
함수형 컴포넌트의 useEffect
훅
클래스형 컴포넌트에는 componentDidMount
, componentDidUpdate
, componentWillUnmount
와 같은 생명 주기 메서드들이 존재했습니다. 하지만 함수형 컴포넌트에서는 이 모든 라이프사이클 작업을 useEffect
훅 하나로 처리할 수 있습니다.
useEffect
훅은 컴포넌트가 렌더링된 이후에 특정 작업을 수행하도록 하는 훅입니다. 주로 부수 효과(Side Effect) 를 처리할 때 사용됩니다.
부수 효과(Side Effect)란?
함수 내에서 함수의 입력(인자) 외에 다른 외부에 영향을 주거나, 외부로부터 영향을 받는 작업을 말합니다. 리액트 컴포넌트에서의 부수 효과는 다음과 같은 것들이 있습니다.
- 데이터 가져오기 (API 호출)
- 구독(Subscription) 설정 및 해제
- DOM 직접 조작 (드물지만 필요할 때)
- 타이머 설정 및 해제 (
setTimeout
,setInterval
)
useEffect
의 기본 구조는 다음과 같습니다.
import React, { useEffect } from 'react';
useEffect(() => {
// 컴포넌트가 렌더링된 후에 실행될 코드 (부수 효과)
return () => {
// 컴포넌트가 언마운트되거나, 다음 이펙트가 실행되기 전에 실행될 '클린업(Clean-up)' 함수
// 구독 해제, 타이머 제거 등 정리 작업
};
}, [의존성 배열]); // (선택 사항) 의존성 배열: 이 배열의 값이 변경될 때만 이펙트가 다시 실행됩니다.
useEffect
의 두 번째 인자인 의존성 배열(Dependency Array) 은 useEffect
가 언제 다시 실행될지를 제어하는 데 매우 중요합니다.
useEffect
의 다양한 활용 패턴
의존성 배열의 값에 따라 useEffect
는 마운트, 업데이트, 언마운트 시점에 다르게 동작합니다.
마운트 시에만 실행 (빈 의존성 배열 []
)
컴포넌트가 처음 마운트될 때만 한 번 실행되고, 이후에는 다시 실행되지 않도록 하려면 의존성 배열을 빈 배열([]
) 로 전달합니다. 이는 클래스형 컴포넌트의 componentDidMount
와 유사합니다.
예제: 페이지 제목 변경 및 초기 데이터 로딩
// src/components/MountedComponent.js
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;
useEffect
내부의 코드는 컴포넌트가 처음 마운트될 때 딱 한 번 실행됩니다.return () => { ... }
내의 코드는 컴포넌트가 언마운트될 때(예:MountedComponent
가 화면에서 사라질 때) 실행되는 클린업 함수입니다. 타이머 해제, 구독 해제 등 뒷정리 작업을 여기에 작성합니다.
의존성이 변경될 때마다 실행
컴포넌트의 특정 state
나 props
값이 변경될 때마다 useEffect
를 다시 실행하고 싶다면, 해당 state
나 props
를 의존성 배열에 포함합니다.
예제: 카운터 값 변경 시 메시지 업데이트
// src/components/CounterWithEffect.js
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]
를 넣었습니다. 이는count
state
의 값이 변경될 때마다 이useEffect
내부의 코드를 다시 실행하라는 의미입니다.console.log
를 통해count
가 변경될 때마다 로그가 찍히는 것을 확인할 수 있습니다.
매 렌더링마다 실행 (의존성 배열 생략)
의존성 배열을 아예 생략하면 useEffect
는 컴포넌트가 렌더링될 때마다 (props나 state가 변경될 때마다 포함) 실행됩니다.
// src/components/AlwaysRunEffect.js
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
가 변경 $\rightarrow$useEffect
재실행 $\rightarrow$value
변경 $\rightarrow$ ... 무한 반복)
클린업(Clean-up) 함수 활용
useEffect
는 선택적으로 return
문 안에 클린업 함수를 반환할 수 있습니다. 이 클린업 함수는 다음 두 가지 시점에 실행됩니다.
- 컴포넌트가 언마운트될 때 (화면에서 사라질 때)
- 이전 이펙트가 다시 실행되기 직전 (의존성 배열의 값이 변경되어 이펙트가 재실행될 때)
클린업 함수는 메모리 누수를 방지하거나 불필요한 네트워크 요청 등을 막는 데 사용됩니다. 주로 구독 해제, 타이머 해제, 이벤트 리스너 제거 등의 뒷정리 작업에 활용됩니다.
예제: 타이머 설정 및 해제
// src/components/TimerComponent.js
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
로 타이머를 시작하고, 반환된interval
ID를 변수에 저장합니다.- 클린업 함수에서
clearInterval(interval)
을 호출하여 타이머를 해제합니다. 이 컴포넌트가 화면에서 사라지면 타이머가 자동으로 멈춥니다. - 이
TimerComponent
를App.js
에 추가하고, 조건부 렌더링으로 이 컴포넌트를 나타났다 사라지게 하면 클린업 함수가 실행되는 것을 볼 수 있습니다. (예: 토글 버튼으로TimerComponent
의 렌더링 여부 제어)
모든 컴포넌트 한 곳에서 테스트
App.js
파일을 수정하여 위에서 만든 컴포넌트들을 모두 불러와 렌더링하고, 콘솔을 보면서 useEffect
의 동작 방식을 직접 확인해 보세요. 특히 MountedComponent
나 TimerComponent
처럼 조건부 렌더링으로 렌더링 여부를 제어하면 마운트/언마운트 시의 useEffect
동작을 명확히 볼 수 있습니다.
// src/App.js
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;
버튼을 클릭하여 컴포넌트의 렌더링 여부를 변경하면서 개발자 도구의 콘솔을 유심히 살펴보면 useEffect
와 클린업 함수의 실행 시점을 명확히 이해할 수 있을 것입니다.
3장 4절 "컴포넌트 라이프사이클 이해하기"는 여기까지입니다. 컴포넌트의 세 가지 주요 생명 주기 단계와 함수형 컴포넌트에서 이 모든 것을 다룰 수 있는 useEffect
훅의 기본 사용법과 다양한 의존성 배열 패턴, 그리고 클린업 함수의 중요성에 대해 상세하게 설명했습니다.
이제 여러분은 컴포넌트가 화면에 나타나고 사라지는 시점에 필요한 작업을 수행할 수 있는 능력을 갖추게 되었습니다. 다음 장에서는 useRef
훅을 이용하여 DOM 요소에 직접 접근하는 방법과 그 활용 사례에 대해 알아보겠습니다.