리액트는 UI를 컴포넌트라는 독립적이고 재사용 가능한 블록으로 구성한다. 각각의 컴포넌트는 사용자 인터페이스의 특정 부분을 정의하며, 자체적인 상태(state)와 속성(props)을 기반으로 렌더링한다.
컴포넌트는 "처음 화면에 나타날 때", "업데이트할 때", "화면에서 사라질 때" 다양한 동작이 필요하다. 이런 흐름을 리액트는 라이프사이클(Lifecycle)이라고 부른다.
라이프사이클을 이해하면 불필요한 렌더링을 막고 최적화된 사용자 경험을 제공할 수 있다.
과거 리액트는 클래스형 컴포넌트에서 라이프사이클 메서드를 사용했지만, 현재는 함수형 컴포넌트 + 훅(Hooks)를 사용해 라이프사이클을 관리한다.
useEffectuseLayoutEffectuseRef컴포넌트가 처음 화면에 나타나는 시점이다. useEffect를 사용해 컴포넌트가 마운트될 때 실행할 코드를 작성할 수 있다. 의존성 배열을 빈 배열 []로 설정하면 처음 한 번만 실행된다.
import { useEffect } from "react";
function Example() {
useEffect(() => {
console.log("컴포넌트가 화면에 나타났습니다!");
// 빈 배열을 넣으면 Mount 시에만 실행
}, []);
}
컴포넌트가 이미 마운트된 후, state나 props 변경되어 리렌더링이 일어날 때 발생한다. useEffect에 특정 값을 의존성 배열에 넣으면, 그 값이 바뀔 때마다 해당 코드가 다시 실행된다.
import { useEffect, useState } from "react";
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`count가 ${count}로 업데이트되었습니다.`);
// count가 변할 때마다 실행
}, [count]);
}
컴포넌트가 더 이상 필요없어 화면에서 사라질 때 발생한다. useEffect 안에서 return 함수를 작성하면, 언마운트 시 자동으로 호출된다. 이때 필요한 정리 작업을 해주면 되는데, 정리를 하지 않으면 메모리 누수가 발생하거나 사라진 컴포넌트가 계속 작업을 시도하는 문제가 발생할 수 있다.
import { useEffect } from "react";
function Example() {
useEffect(() => {
const timer = setInterval(() => {
console.log("타이머 작동 중...");
}, 1000);
return () => {
clearInterval(timer);
console.log("컴포넌트가 사라졌습니다. 타이머 정리 완료!");
};
}, []);
}
함수형 컴포넌트의 표준 라이프사이클 훅으로, 비동기 로직을 처리할 때 자주 사용한다.
useEffect(() => {
// 실행할 코드
return () => {
// 정리(cleanup)할 코드
};
}, [의존성 배열]);
[]: 마운트될 때만 실행[value]: value가 변할 때마다 실행배열 없음: 매 렌더링 때마다 실행 (비권장)렌더링과 무관하게 값을 저장할 수 있는 훅이다. 상태(state)처럼 값을 유지하지만, 값이 변해도 리렌더링을 발생시키지 않는다. 이전 값을 기억할 때 사용한다.
import { useEffect, useRef, useState } from "react";
function Example() {
const [count, setCount] = useState(0);
const prevCountRef = useRef(0);
useEffect(() => {
prevCountRef.current = count;
}, [count]);
return (
<div>
<p>현재 값: {count}</p>
<p>이전 값: {prevCountRef.current}</p>
<button onClick={() => setCount((c) => c + 1)}>증가</button>
</div>
);
}
useEffect 내부에서 상태를 변경하면 컴포넌트가 다시 렌더링된다. 만약 적절한 조건 없이 상태를 수정하면 무한 루프가 발생할 수 있으니 주의해야 한다.
useEffect(() => {
setCount(count + 1);
// 매 렌더링마다 count 증가 → 무한 렌더링 발생
});
useEffect(() => {
if (count === 0) {
setCount(1);
}
}, [count]);
useEffect의 의존성 배열을 올바르게 관리해야 원하는 타이밍에 실행된다. 필요한 변수를 빠뜨리면 오래된 데이터를 사용할 위험이 있고, 불필요한 변수를 넣으면 불필요한 렌더링이 반복될 수 있다.
컴포넌트가 언마운트될 때 정리 작업을 하지 않으면 타이머, 이벤트 리스너, 네트워크 요청 등이 계속 남아 메모리 누수를 유발할 수 있다.
useEffect(() => {
const timer = setInterval(() => {
console.log("running...");
}, 1000);
return () => {
clearInterval(timer); // 언마운트 시 타이머 해제
};
}, []);