본문 바로가기
코딩이야기𖦹/React

📌 React의 useEffect와 생명주기 비교 정리

by Dev디자인 2025. 4. 10.

React를 학습하면서 가장 혼란스러운 개념 중 하나는 바로

useEffect와 클래스형 컴포넌트의 생명주기 함수들이다.

useEffect의 다양한 사용 패턴을 알아보고,

클래스형 컴포넌트의 생명주기 함수와 어떻게 대응되는지 정리해보고자 한다.

🧩 사전지식: React 컴포넌트는 언제 렌더링되는가?

React는 상태(state)가 바뀔 때마다 다시 렌더링된다.

다시 말해, setStateuseStateset함수가 호출되면 해당 컴포넌트는 다시 실행된다.

즉, 다음과 같은 흐름을 가진다:

  1. 컴포넌트가 처음 화면에 나타날 때마운트(Mount)
  2. 상태가 바뀌어 다시 렌더링될 때 → 업데이트(Update)
  3. 컴포넌트가 사라질 때언마운트(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-serverdb.json을 활용하면 백엔드 없이도 실제 API처럼 개발할 수 있다.
  • 클래스형 컴포넌트의 생명주기 메서드와 매칭해보면 useEffect의 의미가 더 명확하게 다가온다.

최근댓글

최근글

skin by © 2024 ttuttak