[React] useEffect 실행 순서

장동균·2024년 5월 12일
0

useEffect의 실행 순서가 항상 헷갈려서 이를 정리한다.


Mounting

컴포넌트의 인스턴스가 생성되어 DOM 상에 삽입되는 것. (리액트에서는 컴포넌트를 특정 영역에 끼워넣는 행위를 말한다.)

때문에 마운트된 후란 표현은 리액트 컴포넌트가 웹 페이지의 DOM에 삽입되고 난 이후를 의미한다. 이는 컴포넌트의 생성과 초기 렌더링이 완료되었고, 이제 화면에 실제로 보여지는 상태에 도달했음을 나타낸다. 마운트된 이후 컴포넌트는 사용자와의 상호작용이 가능해지며, DOM에서 직접 조작할 수 있다.

마운트 과정에서의 주요 단계들

  1. 렌더링(Rendering): 컴포넌트의 render 메서드나 함수형 컴포넌트에서 반환(return)하는 JSX가 호출되어 가상 DOM을 구성합니다.
  2. 재조정(Reconciliation): 리액트는 기존의 가상 DOM과 새로운 가상 DOM을 비교하여 실제 DOM에 반영해야 할 변경 사항을 결정합니다.
  3. 커밋(Commit): 변경 사항이 실제 DOM에 반영되는 단계입니다. 이 시점이 바로 컴포넌트가 마운트된 시점입니다.

Updating

이미 mount 되어 DOM에 존재하는 컴포넌트를 rerendering 하여 업데이트 하는것.

Unmounting

컴포넌트가 DOM에서 제거되는 것


useEffect의 실행 시점은 mount가 완전히 끝난 이후이다. 즉 컴포넌트가 실제 DOM에 반영된 후, 비동기적으로 실행된다.

왜 마운트 이후에 useEffect가 실행되는가?

이러한 설계는 useEffect가 실행될 때 DOM이 최신 상태임을 보장하기 위함이다. 예를 들어, DOM 요소에 접근하거나 수정해야 하는 경우, useEffect 내에서 이러한 작업을 안전하게 수행할 수 있다. 마운트 과정 중에 실행될 경우, DOM이 완전히 준비되지 않았을 수 있기 때문에 오류가 발생할 가능성이 있다.

따라서, useEffect는 컴포넌트가 마운트되고 나서 "정리된 상태"에서 실행되도록 설계되어 있으며, 이는 리액트의 성능 최적화 및 안정성을 유지하는 데 기여한다.


import { useEffect } from 'react';

export default const App = () => {
  console.log('1');

  useEffect(() => {
    console.log('after mount');
  }, []);

  console.log('2');

  return <div className='App' />
}

// 출력
// 1
// 2
// after mount

mount 중 '1'과 '2'가 찍힌다.

mount가 종료되고 나서 'after mount'가 찍힌다.


useEffect에는 클린업 함수가 존재한다. 클린업 함수는 기본적으로 컴포넌트의 언마운트 시점에 동작한다. 이는 주로 이벤트 리스너를 제거하거나, 구독을 해제하거나, 진행 중인 네트워크 요청을 취소하는 등의 작업을 수행하기 위해 사용한다. 이 기능은 리소스 누수를 방지하고, 메모리 관리를 최적화하는데 중요하다.

import { useEffect } from 'react';

export default const App = () => {
  console.log('1');

  useEffect(() => {
    console.log('after mount');
    
    return () => console.log('unmount');
  }, []);

  console.log('2');

  return <div className='App' />;
}

// 출력
// 1
// 2
// after mount
// 컴포넌트가 제거되는 동작 실행
// unmount

언마운트 시점 이외에도 클린업 함수를 실행시키는 방법이 존재한다. 이는 의존성 배열에 값이 추가된 경우이다. useEffect에 제공된 의존성 배열 내의 어떤 값이 변경되면, 이전에 실행된 useEffect의 클린업 함수가 먼저 실행된다. (실행 시점의 값들을 기억한다.) 그 후, useEffect의 메인 함수가 다시 실행되어 새로운 부수 효과를 적용한다. 이는 이전의 부수 효과로 인해 발생할 수 있는 영향을 제거하고, 새로운 상태나 속성에 기반한 적절한 부수 효과를 적용하기 위함이다.

import { useEffect, useState } from 'react';

export default const App = () => {
  const [input, setInput] = useState('');
  
  console.log('1');

  useEffect(() => {
    console.log('after mount');
    // 클린업 함수 시점의 input 값 확인 필요
    return () => console.log('unmount');
  }, [input]);

  console.log('2');

  return (
    <div className='App'>
      <input
        value={input}
        onChange={(e) => {
          setInput(e.target.value);
        }}></input>
    </div>
  );
}

// 출력
// 1
// 2
// after mount (input의 value: '')

// input에 '1' 입력 (업데이트 발생)
// 1
// 2
// unmount (input의 value: '')
// after mount (input의 value: '1')

정리하면 다음과 같다.

useEffect에서 클린업 함수는 다음 두 가지 주요 시점에 실행된다:

  1. 의존성 배열의 값이 변경될 때: 배열에 포함된 값 중 하나라도 변경되면, 이전의 useEffect 실행에 대한 클린업이 먼저 수행된 후, 새로운 useEffect 함수가 실행됩니다. 이는 이전 부수 효과의 영향을 정리하고 새로운 상태에 맞춰 부수 효과를 다시 설정하기 위함입니다.
  2. 컴포넌트가 언마운트될 때: 컴포넌트가 DOM에서 제거될 때, 클린업 함수가 실행되어 모든 정리 작업을 수행합니다. 이는 의존성 배열의 내용과 관계없이 항상 수행됩니다.

export default const App = () {
  const [isShow, setIsShow] = useState(true);

  useEffect(() => {
	if (isShow) return;
    return () => console.log('unmount');
  }, []);
	
	return <div>test</div>;
}

다음의 클린업 함수는 실행될 수 있을까?

실행되지 못한다. 클린업 함수가 useEffect의 한가지 기능이라고 생각하여, 클린업 함수 위쪽 문맥과는 별도로 동작한다고 생각할 수 있다. 하지만 실제로는 그렇지 않다. 때문에 클린업 함수를 사용해야하는 경우 early return을 유의해서 사용해야한다.


import { useEffect } from 'react';

const Inner = () => {
  useEffect(() => {
    console.log('Inner Mount');
  }, []);
  
  return <div>Inner</div>;
};

const Outer = () => {
  useEffect(() => {
    console.log('Outer Mount');
  }, []);

  return (
    <div>
      <Inner />
    </div>
  );
};


const App = () => {
  useEffect(() => {
    console.log('App Mount');
  }, []);

  return (
    <div className='App'>
      <Outer />
    </div>
  );
}


export default App;

// 출력
// Inner Mount
// Outer Mount
// App Mount

App => Outer => Inner

컴포넌트의 호출 순서는 다음과 같다. 반면 실제 로그 출력은 이와 정반대이다. 이는 useEffect의 실행 시점이 마운팅 이후이기 때문이다. App 컴포넌트의 마운팅이 완료되려면 Outer 컴포넌트의, Outer 컴포넌트의 마운팅이 완료되려면 Inner 컴포넌트의 마운팅이 완료되어야 한다.
즉, 자식 컴포넌트 => 부모 컴포넌트 순으로 useEffect가 실행된다.


import { useEffect } from 'react';

const One = () => {
  useEffect(() => {
    console.log('One Mount');
  }, []);
  
  return <div>One</div>;
};

const Two = () => {
  useEffect(() => {
    console.log('Two Mount');
  }, []);

  return <div>Two</div>;
};

const App = () => {
  useEffect(() => {
    console.log('App Mount');
  }, []);

  return (
    <div className='App'>
      <One />
      <Two />
    </div>
  );
}

export default App;

// 출력
// One Mount
// Two Mount
// App Mount

자식 컴포넌트가 동일 레벨에서 호출되는 경우의 출력은 다음과 같다.


참고문헌

https://choyeon-dev.tistory.com/m/10

https://velog.io/@soyi47/React-Component%EC%9D%98-Lifecycle

profile
프론트 개발자가 되고 싶어요

3개의 댓글

comment-user-thumbnail
2024년 5월 13일

롤의 실행순서가 항상 헷갈려서 정리한다.
1. 맵스가기
2. 롤키기
3. 로그인하기

답글 달기
comment-user-thumbnail
2024년 5월 13일

롤의 실행순서가 항상 헷갈려서 정리한다.
1. 맵스가기
2. 롤키기
3. 로그인하기

답글 달기
comment-user-thumbnail
2024년 7월 1일

초심잃음?

답글 달기