React를 학습하면서 가장 혼란스러운 개념 중 하나는 바로
useEffect와 클래스형 컴포넌트의 생명주기 함수들이다.
useEffect의 다양한 사용 패턴을 알아보고,
클래스형 컴포넌트의 생명주기 함수와 어떻게 대응되는지 정리해보고자 한다.
🧩 사전지식: React 컴포넌트는 언제 렌더링되는가?
React는 상태(state)가 바뀔 때마다 다시 렌더링된다.
다시 말해, setState나 useState의 set함수가 호출되면 해당 컴포넌트는 다시 실행된다.
즉, 다음과 같은 흐름을 가진다:
- 컴포넌트가 처음 화면에 나타날 때 → 마운트(Mount)
- 상태가 바뀌어 다시 렌더링될 때 → 업데이트(Update)
- 컴포넌트가 사라질 때 → 언마운트(Unmount)
이러한 흐름은 함수형 컴포넌트든, 클래스형 컴포넌트든 동일하다. 다만, 그 흐름을 표현하는 방식이 다를 뿐이다.
✅ 1번: 클래스형 컴포넌트 생명주기
클래스형 컴포넌트에서는 다음과 같은 생명주기 메서드를 사용하여 컴포넌트의 특정 시점에 맞는 작업을 수행할 수 있다.
class Counter extends Component {
constructor() {
super();
this.state = { counter: 1 }
console.log('constructor')
}
componentDidMount() {
console.log('DidMount')
}
componentDidUpdate() {
console.log('DidUpdate')
}
componentWillUnmount() {
console.log('componentWillUnmount')
}
render() {
console.log("render")
return (
<>
<div>counter : {this.state.counter}</div>
<button onClick={() => this.setState({
counter: this.state.counter + 1 })}>+1</button>
</>
)
}
}
실행 시점 | 생명주기 | 메서드설명 |
컴포넌트가 처음 나타날 때 | componentDidMount() | DOM이 마운트된 후 실행되며, 초기 데이터 요청이나 타이머 설정 등에 사용됨 |
상태 또는 props가 바뀌고 다시 렌더링될 때 |
componentDidUpdate() | 이전 상태와 비교하거나, 특정 prop이 바뀐 경우에만 작업을 수행할 때 조건문과 함께 사용됨 |
컴포넌트가 화면에서 제거될 때 | componentWillUnmount() | 이벤트 리스너 제거, 타이머 정리 등 클린업 작업을 할 때 사용됨 |
✅ 2번: 함수형 컴포넌트 생명주기 (useEffect)
함수형 컴포넌트에서는 위의 생명주기 메서드와 동일한 역할을 useEffect 하나로 처리한다.
useEffect는 렌더링 이후 특정 작업을 수행하거나, 언마운트 시 정리 작업을 실행할 수 있다.
function Counter () {
const [counter, setCounter] = useState(1)
const [counter2, setCounter2] = useState(100)
useEffect(() => { console.log('맨 처음 렌더링 될 때') }, [])
useEffect(() => { console.log('리렌더링...') })
useEffect(() => { console.log('counter의 값이 변할 때') }, [counter])
useEffect(() => { console.log('counter2의 값이 변할 때') }, [counter2])
useEffect(() => {
return () => {
console.log("returned function")
}
}, [])
return (
<>
<div>counter : {counter}</div>
<button onClick={() => setCounter(counter + 1)}>+1</button>
<div>counter2 : {counter2}</div>
<button onClick={() => setCounter2(counter2 - 1)}>-1</button>
</>
)
}
실행 조건 | useEffect 구문 | 실제 용도 및 예시 |
컴포넌트가 처음 마운트될 때 1회만 실행 | useEffect(() => {...}, []) | 초기 데이터 불러오기, 이벤트 등록, 타이머 설정 등 |
컴포넌트가 리렌더링될 때마다 실행 | useEffect(() => {...}) | 디버깅, DOM 속성 추적 등 모든 렌더링 상황에 반응해야 할 때 |
특정 상태 값이 변경될 때만 실행 | useEffect(() => {...}, [state]) | 해당 state가 바뀔 때만 실행됨. 조건부 작업(ex: 필터링, 정렬) 등에 활용 |
언마운트 시 실행 (정리 작업) | useEffect(() => { return () => {...} }, []) | 이벤트 제거, 타이머 정리, 외부 구독 종료 등 클린업 작업에 사용 |
✅ 3번: React 생명주기 함수의 필요성 (왜 써야 할까?)
단순히 '언제 실행되느냐'보다 중요한 것은 '왜 필요한가'이다.
React 컴포넌트는 단순히 화면만 그리는 게 아니라 다양한 외부 작업을 함께 처리한다.
아래는 생명주기 함수(useEffect 포함) 네 가지 대표적인 상황이다.
1️⃣ 서버에서 데이터를 받아오는 경우
컴포넌트가 처음 나타났을 때 데이터를 불러와야 하는 상황이 많다.
예를 들어 게시글 목록, 유저 정보, 뉴스 기사 등은 서버에서 가져와야 한다.
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data))
}, [])
✅ 빈 배열 []을 넣으면 마운트 시 1회만 실행되어, 중복 요청 없이 데이터 초기화가 가능하다.
2️⃣ 이벤트 핸들러를 사용하는 경우
윈도우 리사이즈, 스크롤 감지, 키보드 입력 등 브라우저 이벤트를 감지하려면 이벤트 핸들러를 등록해야 한다.
하지만 컴포넌트가 사라질 때 이벤트를 꼭 제거해야 메모리 누수를 막을 수 있다.
useEffect(() => {
const handleResize = () => console.log(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
✅ 이벤트 연결과 해제를 동시에 처리할 수 있다는 점에서 useEffect는 매우 강력하다.
3️⃣ 타이머 함수 사용하는 경우
컴포넌트가 일정 시간마다 무언가를 반복해야 할 때, setInterval 또는 setTimeout 같은 타이머 함수를 사용한다. 이때도 컴포넌트가 언마운트되면 타이머를 반드시 제거해주어야 한다. 그렇지 않으면 컴포넌트가 사라졌음에도 계속 콘솔이 찍히거나, 메모리 누수 같은 문제가 발생할 수 있다.
useEffect(() => {
const timer = setInterval(() => {
console.log('1초마다 실행')
}, 1000)
return () => clearInterval(timer)
}, [])
✅ 실전에서는 채팅 새로고침, 실시간 시계, 주기적인 알림 확인 등에 사용된다.
4️⃣ 클린업(Cleanup) 함수란?
useEffect는 컴포넌트가 사라지거나 특정 상태가 바뀌기 전에 정리(clean-up) 할 작업을 수행할 수 있다.
이 정리 작업은 useEffect 안에서 return () => { ... } 형태로 작성하며, 다음 두 상황에서 자동 실행된다:
- 컴포넌트가 언마운트(사라질)될 때
- useEffect가 다시 실행되기 전, 이전 effect를 정리할 때
예를 들어, 외부에서 이벤트를 연결했거나, 타이머를 사용했거나, 서버 연결(WebSocket 등)을 해두었을 때 이 정리 작업은 꼭 필요하다.
useEffect(() => {
console.log('컴포넌트가 실행됨')
return () => {
console.log('컴포넌트가 사라질 때 정리')
}
}, [])
✅ 정리(clean-up)를 하지 않으면, 예기치 않은 동작이나 성능 문제, 메모리 누수가 발생할 수 있다.
🗃 useEffect + fetch + db.json
useEffect를 활용하여 해당 가짜 서버에 요청을 보내고 데이터를 받아오는 방식은 다음과 같다
useEffect(() => {
fetch("http://localhost:3000/data")
.then((res) => res.json())
.then((res) => setData(res))
}, [])
💾 db.json이란?
db.json은 JSON 형식의 파일로, 우리가 사용할 테스트용 데이터를 담는 곳이다. 예를 들어 아래와 같이 작성할 수 있다
{
"data": [
{ "id": 1, "content": "첫 번째 데이터" },
{ "id": 2, "content": "두 번째 데이터" }
]
}
이 파일은 마치 서버의 데이터베이스처럼 동작한다. json-server가 이 파일을 읽어 API 형태로 응답해주기 때문이다.
🛠 실행 방법
아래 명령어를 프로젝트 루트 디렉토리에서 실행하면 된다:
npx json-server --watch db.json --port 3000
이 명령어는 다음과 같은 작업을 수행한다:
- db.json을 읽고
- 내부 데이터를 REST API 형식으로 제공하며
- http://localhost:3000/data로 요청 시, data 배열을 JSON으로 응답함
🔄 전체 흐름 다시 정리
1. App 컴포넌트가 처음 렌더링됨
2. useEffect 안의 fetch 코드 실행
3. fetch가 json-server로부터 데이터 요청 (http://localhost:3000/data)
4. json-server가 db.json 파일에서 데이터를 꺼내 응답
5. setData()로 상태 업데이트 → 화면에 목록 출력됨
🧠 왜 useEffect 안에서 fetch를 해야 할까?
React 컴포넌트는 렌더링될 때마다 함수가 실행되므로,
fetch 요청을 컴포넌트 함수 안에 직접 넣으면 렌더링마다 계속 요청이 발생하는 문제가 생긴다.
따라서 useEffect(() => {...}, [])처럼 마운트 시 딱 한 번만 요청을 보내도록 해야 한다.
또한, 비동기 요청이므로 네트워크 응답이 돌아오는 동안 컴포넌트는 계속 실행될 수 있다.
이때 상태가 바뀌기 전에 언마운트되는 경우를 대비하여 정리(clean-up) 로직을 추가하는 것도 좋은 습관이다.
✅ 마무리 요약
- useEffect는 컴포넌트 렌더링 이후 특정 작업을 수행할 수 있게 한다.
- 의존성 배열에 따라 실행 시점이 달라진다.
- 생명주기 함수는 다음과 같은 실무 상황에서 필수적으로 필요하다:
- 데이터 요청
- 이벤트 등록/해제
- 타이머 실행 및 제거
- 외부 연결 정리 (클린업)
- json-server와 db.json을 활용하면 백엔드 없이도 실제 API처럼 개발할 수 있다.
- 클래스형 컴포넌트의 생명주기 메서드와 매칭해보면 useEffect의 의미가 더 명확하게 다가온다.
'코딩이야기𖦹 > React' 카테고리의 다른 글
📌 Tailwind CSS 진짜 편한가? (0) | 2025.04.17 |
---|---|
📌 styled-components 정리! (2) | 2025.04.16 |
📌 SCSS : 변수, 중첩, @mixin 정리! (0) | 2025.04.15 |
📌 React Router를 활용한 동물 정보 웹 페이지 만들기를 통한 개념 정리 (0) | 2025.04.10 |
📌 SPA랑 MPA, CSR이랑 SSR 개념 한 번에 정리 해봄 (0) | 2025.04.07 |