Skip to content

Latest commit

 

History

History
233 lines (164 loc) · 9.93 KB

3.1.2 useEffect.md

File metadata and controls

233 lines (164 loc) · 9.93 KB

평소에 React에서 자주 사용하는 React의 hook 중에서 useEffect 에 대해서 깊게 다뤄보려고 한다.

useEffect 란?

보통 useEffect라고 물어보면 다음과 같은 답변을 들을 것이다.

  • useEffect는 두 개의 인수를 받는데. 첫 번째는 콜백, 두 번째는 의존성 배열이다. 이 두 번째 의존성 배열의 값이 변경되면 첫 번째 인수인 콜백을 실행한다.

  • 클래스형 컴포넌트의 생명주기 메서드와 비슷한 작동을 구현할 수 있다. 두 번째 의존성 배열에 빈 배열을 넣으면 컴포넌 트가 마운트될 때만 실행된다.

  • useEffect는 클린업 함수를 반환할 수 있는데, 이 클린업 함수는 컴포넌트가 언마운트될 때 실행된다

이러한 useEffect에 대한 정의는 어누 정도 옳지만 완전히 정확하지는 않다. 그리고 알려진 것 처럼 생명주기 메서드를 대체하기 위해 만들어진 hook도 아니다.

useEffect의 정의를 정확하게 내리자면 useEffect는 Application 내 Component의 여러 값들을 활용해 동기적으로 부수 효과를 만드는 매커니즘이다.

그리고 이 부수 효과가 '언제' 일어나는지보다 어떤 상태값과 함께 실행되는지 살펴보는 것이 중요하다.


useEffect는 기본적으로 이 2가지가 대표적인 유형이 있다.

의존성 배열 (dependency array)

첫번째 유형: 하나의 인자만 받는다.

인자는 callback 함수를 받는다.

useEffect(()=>{
  //작업 ...
});

이 유형은 Rendering이 될때마다 callback 함수가 실행이 된다. 즉, 컴포넌트가 맨 처음 화면에 렌더링 될때, 컴포넌트가 다시 렌더링 될때 실행이 된다.

하지만, 한 가지 의문점이 생길 수 있다. 의존성 배열이 없는 useEffect가 매 렌더링마다 실행된다면 그냥 useEffect 없이 써도 되는게 아닐까?

// 1
function Component() {
  console.log('렌더링됨');
}

// 2
function Component() {
  useEffect(() => {
    console.log('렌더링됨');
  });
}

두 코드는 명백히 리액트에서 차이점을 지니고 있다. 차이점은 다음과 같다.

이후에 소개할 서버 사이드 렌더링 관점에서 useEffect는 클라이언트 사이드에서 실행되는 것을 보장해 준다.

useEffect 내부에서는 window 객체의 접근에 의존하는 코드를 사용해도 된다.

useEffect는 컴포넌트 렌더링의 부수 효과, 즉 컴포넌트의 렌더링이 완료된 이후에 실행된다. 반면 직접 실행은 컴포넌트가 렌더링되는 도중에 실행된다.

따라서 첫번째 코드는 서버 사이드 렌더링의 경우에 서버에서도 실행된다. 그리고 이 작업은 함수형 컴포넌트의 반환을 지연시키는 행위다. 즉, 무거운 작업일 경우 렌더링을 방해하므로 성능에 악영향을 미칠 수 있다.

useEffect의 effect는 컴포넌트의 사이드 이펙트, 즉 부수 효과를 의미한다는 것을 명심하자. useEffect는 컴포넌트가 렌더링된 후에 어떠한 부수 효과를 일으키고 싶을 때 사용하는 훅이다.


두번째 유형: 두개의 인자를 받는다.

첫 번째 인자로 callback 함수, 두 번째 인자로 의존성 배열을 받는다. (dependency array)

useEffect(()=>{
  //작업 ...
}, [value]);

리액트에서는 렌더링이 실행될 때마다 함수 컴포넌트의 함수가 다시 실행된다.

useEffect 함수에서 의존성 배열에 value값을 넣게 되면, 렌더링이 될때마다 실행이 되는 것이 아니라, 화면에 첫 렌더링 될때 실행, 그리고 의존성 배열 안에 있는 value이 바뀔때 실행이 된다.

하지만, 의존성 배열에 value 값이 비어있다면,

useEffect(()=>{
  //작업 ...
}, []);

컴포넌트에 맨 처음에 화면에 첫 렌더링이 될때만 실행이 된다.


import { useEffect, useState } from 'react';
import './App.css';

function App() {
  const [count, setCount] = useState(0);
  const [input, setInput] = useState('');

  const handleCountUpdate = () => {
    setCount(count + 1);
  };

  const handleInputUpdate = (e) => {
    setInput(e.target.value);
  };

  useEffect(() => {
    console.log('useEffect 실행');
  }, [count]);

  return (
    <main className='flex w-full min-h-screen flex-col items-center mt-[200px]'>
      <h1 className='text-blue-700 font-bold text-xl'>은우의 useEffect 실험</h1>
      <div className='flex items-center gap-4 mt-5'>
        <h1>{count}</h1>
        <button
          onClick={handleCountUpdate}
          className='w-[100px] bg-red-600 rounded-xl text-white font-bold p-3'
        >
          +
        </button>
      </div>
      <div>
        <input onChange={handleInputUpdate} type='text' className='border-2 border-blue-300 mr-3' />
      </div>
      <p className='text-center mt-2'>{input}</p>
    </main>
  );
}

export default App;

이 코드에서는,

첫 화면이 떴을 때 첫 번째 렌더링이 수행되고 'useEffect 실행' 이라는 문구가 출력된다. 그리고 '+' 버튼을 눌러 count 값이 2번 증가, 즉 업데이트 되었고, 그 결과 count 값이 업데이트 됐으므로 콘솔 창에는 출력이 2번 되었다. 즉, 렌더링이 2번 됐음을 알 수 있다.

이후, input 태그 안에 값을 아무리 값을 입력하여도 콘솔 창에는 아무 것도 출력되지 않고 이전과 같은 것을 볼 수 있다. 그 이유는, useEffect 함수 안에서, 의존성 배열에 'count' 값을 넣어서, count가 변화할때만 업데이트를 하기 때문이다.

즉, useEffect는 렌더링할 때마다 의존성에 있는 값을 보면서 이 의존성의 값이 이전과 다른 게 하나라도 있으면 부수 효과를 실행하는 함수이다. 다른 말로 표현하면, state와 props의 변화 속에서 일어나는 렌더링 과정에서 실행되는 부수 효과 함수라고 볼 수 있다.


useEffect를 왜 사용하는걸까?

  • 사이드 이펙트 관리: 컴포넌트가 렌더링될 때마다 매번 수행할 필요는 없지만, 어떤 상태나 데이터에 따라 특정 작업을 실행하고 싶을 때 유용하다. 예를 들어, API 호출, 이벤트 리스너 등록, 타이머 설정 등이 여기에 해당하다.

  • 의존성 기반 업데이트: useEffect는 의존성 배열을 통해 특정 상태가 변경될 때만 실행될 수 있다. 이를 통해 불필요한 재실행을 방지하고, 성능 최적화를 도울 수 있다.

  • 정리 작업 수행: useEffect는 정리 함수(cleanup function)를 반환할 수 있습니다. 컴포넌트가 언마운트되거나, 의존성이 변경될 때 실행되는 이 정리 함수는 메모리 누수를 방지하고, 이벤트 리스너 제거, 타이머 해제 등을 처리하는 데 사용된다.

  • 초기화 작업: 컴포넌트가 처음 렌더링될 때만 특정 작업을 수행하고 싶을 때, 빈 배열([])을 의존성 배열로 설정해 초기화 작업을 수행할 수 있다. 이를 통해 초기 데이터 로드나 초기 설정 작업을 할 수 있다.

useEffect를 사용하면 특정 상태나 라이프사이클에 따라 컴포넌트의 동작을 유연하게 제어할 수 있어 React 컴포넌트의 기능성을 확장하는 데 매우 유용하다.


클린업 함수의 목적

클린업 함수는 Event를 등록하고 지울 때 사용해야 한다고 알려져 있다.

import { useState, useEffect } from 'react';
export default function App() {
  const [counter, setCounter] = useState(0);
  function handleClick() {
    setCounter((prev) => prev + 1);
  }
  useEffect(() => {
    function addMouseEvent() {
      console.log(counter);
    }
    window.addEventListener('click', addMouseEvent);
    // 클린업 함수
    return () => {
      console.log('클린업 함수 실행!', counter);
      window.removeEventListener('click', addMouseEvent);
    };
  }, [counter]);
  return (
    <>
      <hl>{counter}</hl>
      <button onClick={handleClick}>+</button>
    </>
  );
}

클린업 함수는 이전 counter 값, 즉 이전 state를 참조해 실행된다는 것을 알 수 있다. 여기서 중요한 것은, 클린업 함수는 비록 새로운 값을 기반으로 렌더링 뒤에 실행되지만 이 변경된 값을 읽는 것이 아니 라 함수가 정의됐을 당시에 선언됐던 이전 값을 보고 실행된다는 것이다.

함수형 컴포넌트의 useEffect는 그 콜백이 실행될 때마다 이전의 클린업 함수가 존재한다면 그 클린업 함수를 실행한 뒤에 콜백을 실행한다.

따라서 이벤트를 추가하기 전에 이전에 등록했던 이벤트 핸들러를 삭제하는 코드를 클린업 함수에 추가하는 것이다. 이렇게 함으로써 특정 이벤트의 핸들러가 무한히 추가되는 것을 방지할 수 있다.

이처럼 클린업 함수는 생명주기 메서드의 언마운트 개념과는 조금 차이가 있는 것을 볼 수 있다. 언마운트는 특정 컴포넌트가 DOM에서 사라진다는 것을 의미하는 클래스형 컴포넌트의 용어다.

클린업 함수는 언마운트라기 보다는 함수형 컴포넌트가 리렌더링됐을 때 의존성 변화가 있었을 당시 이전의 값을 기준으로 실행되 는, 말 그대로 이전 상태를 청소해 주는 개념으로 보는 것이 옳다


useEffect에서는 의전성 배열의 이전 값과 현재 값을 Object.is를 기반으로 얕은 비교를 수행한다. 이전 의존성 배열과 현재 의존성 배열의 값에 하나라도 변경 사항이 있다면 callback으로 선언한 부수 효과를 실행한다.

이것이 useEffect의 본질이다.