리액트 컴포넌트 생명주기(Lifecycle) 한눈에 파악하기

리액트 개발자라면 반드시 이해해야 할 핵심 개념 중 하나가 바로 **생명주기(Lifecycle)**입니다. 컴포넌트가 브라우저에 나타나고, 업데이트되고, 사라지는 일련의 과정을 이해하면 보다 효율적인 앱을 설계할 수 있습니다.
과거에는 클래스형 컴포넌트의 메서드를 통해 이를 관리했지만, 현재는 React Hooks를 이용한 함수형 컴포넌트가 표준이 되었습니다. 이 글에서는 두 방식을 비교하며 생명주기를 완벽하게 정리해 보겠습니다.
1. 생명주기의 3단계 요약
리액트 컴포넌트의 생명주기는 크게 세 단계로 나뉩니다.
- Mounting (마운트): 컴포넌트가 DOM에 삽입될 때 (탄생)
- Updating (업데이트): Props나 State가 변경되어 재렌더링될 때 (성장)
- Unmounting (언마운트): 컴포넌트가 DOM에서 제거될 때 (소멸)
2. 클래스형 vs 함수형 비교 차트
단계클래스형 메서드함수형 Hook (useEffect)
| 마운트 | componentDidMount | useEffect(() => { ... }, []) |
| 업데이트 | componentDidUpdate | useEffect(() => { ... }, [의존성]) |
| 언마운트 | componentWillUnmount | useEffect(() => { return () => { ... } }) |
3. 단계별 상세 설명 및 실무 예제
① Mounting: 컴포넌트가 화면에 나타날 때
이 단계는 주로 외부 API 호출, 라이브러리 연동(D3, Google Maps 등), 혹은 setTimeout 같은 타이머를 설정할 때 사용됩니다.
[함수형 예제: API 데이터 불러오기]
import React, { useState, useEffect } from 'react';
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
// 마운트 시점에 딱 한 번 실행됨
console.log("컴포넌트가 마운트되었습니다.");
fetch('[https://api.example.com/user/1](https://api.example.com/user/1)')
.then(res => res.json())
.then(data => setUser(data));
}, []); // 빈 배열([])은 마운트 시에만 실행하라는 의미입니다.
return (
안녕하세요, {user.name}님!
:로딩 중...
} );
}
② Updating: 데이터가 변경되어 다시 그려질 때
특정 상태(state)나 속성(props)이 변경될 때 동작을 수행하고 싶다면 이 단계를 활용합니다.
[함수형 예제: 검색어 변경에 따른 재조회]
useEffect(() => {
if (searchTerm) {
console.log(`검색어가 "${searchTerm}"으로 변경되어 결과를 업데이트합니다.`);
// 검색 로직 실행
}
}, [searchTerm]); // searchTerm이 바뀔 때마다 실행됩니다.
③ Unmounting: 컴포넌트가 사라질 때
메모리 누수를 방지하기 위해 사용했던 이벤트 리스너를 제거하거나 타이머를 멈추는 '정리(Cleanup)' 작업이 필요합니다.
[함수형 예제: 타이머 해제]
import React, { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Cleanup 함수: 언마운트 직전에 실행됨
return () => {
console.log("타이머를 종료하고 메모리를 정리합니다.");
clearInterval(intervalId);
};
}, []);
return <div>경과 시간: {seconds}초</div>;
}
4. 복합적인 흐름 이해하기
실무에서는 하나의 useEffect 안에서 마운트와 언마운트 로직을 동시에 처리하는 경우가 많습니다.
[종합 예제: 윈도우 리사이즈 이벤트]
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
// 1. 마운트 시: 이벤트 등록
window.addEventListener('resize', handleResize);
console.log("리스너 등록");
// 2. 언마운트 시: 이벤트 제거
return () => {
window.removeEventListener('resize', handleResize);
console.log("리스너 해제");
};
}, []); // 의존성 배열이 비어있으므로 생성/소멸 시에만 작동
return <h2>현재 창 너비: {width}px</h2>;
}
5. 생명주기 관리 시 주의사항
- 의존성 배열 관리: useEffect의 두 번째 인자인 배열을 잘못 관리하면 무한 루프에 빠지거나 업데이트가 반영되지 않을 수 있습니다. 내부에 사용된 모든 상태값과 함수를 체크하세요.
- 불필요한 렌더링 방지: 매 렌더링마다 생명주기 로직이 실행되지 않도록 useMemo나 useCallback과 함께 사용하여 최적화하는 것이 좋습니다.
- Cleanup의 중요성: 외부 라이브러리를 연결했다면 반드시 언마운트 시점에 연결을 끊어주어야 성능 저하를 막을 수 있습니다.
리액트의 생명주기는 컴포넌트의 라이프 사이클을 개발자가 제어할 수 있게 해주는 강력한 도구입니다. 클래스형의 복잡한 메서드 이름보다는 useEffect 하나로 통합된 현대적인 리액트 방식을 익히는 것이 생산성 향상에 큰 도움이 됩니다.