티스토리 뷰

Web/React

[React] useEffect()

해구름 2021. 6. 22. 10:20
반응형

이 글은 A Simple Explanation of React.useEffect()를 바탕으로 번역된 내용입니다. 잘못된 해석이 있을 수 있으며, 모든 권리는 원저자에게 있으며 원저자의 요청에 따라 언제든지 수정/삭제될 수 있습니다.

React hook의 풍부한 표현식은 상당히 인상적입니다.  hook을 사용하면 적은 코드를 작성하면서 더 많은 작업을 할 수 있습니다.

하지만 hook의 간결함에는 대가가 있습니다. 상대적으로 사용하기 어렵죠. 함수형 컴포넌트에서 side-effect를 관리하기 위한 useEffect() hook이 특히 사용하기 어렵습니다.

이 포스트에서는 useEffect() hook은 언제 어떻게 사용해야하는지 안내해드립니다.

1. useEffect()는 Side-effect를 위한 함수

React의 함수형 컴포넌트는 props와 state를 바탕으로 결과를 생성합니다. 만약 함수형 컴포넌트가 결과를 반환하는 것과 무관한 연산이 포함한다면, 우리는 이러한 연산들을 side-effect라고 부릅니다.

side-effect 연산의 예를들면 네트워크를 통해 Reqest를 전송하거나, DOM을 직접 수정하거나, setTime()과 같이 타이머 함수를 사용하는 것들이 있습니다.

컴포넌트가 결과를 렌더링하는 것과 Side-effect 로직은 서로 무관한 독립적 작업들입니다. 컴포넌트와 무관한 Side-effect 연산을 컴포넌트 body에서 직접 수행하는 것은 잘못된 컴포넌트 개발로 볼 수 있습니다.

컴포넌트가 얼마나 자주 렌더링 되느냐는 개발자가 통제할 수 있는 영역이 아닙니다. 만약 React가 컴포넌트를 렌더링해야 한다고 판단한다면 개발자는 이를 멈출 수 있는 방법이 없습니다.

function Greet({ name }) {
  const message = `Hello, ${name}!`; // 결과 연산

  //BAD
  document.title = 'Greetings page'; // Side-effect 코드

  return <div>{message}</div>;       // 결과 반환
}

Side-Effect 코드를 렌더링과 분리하려면 어떻게 해야할까요? 이럴 때 useEffect()함수가 사용됩니다. useEffect() hook을 통해 렌더링과 무관한 side-effect 연산들을 분리해 낼 수 있습니다.

import { useEffect } from 'react';

function Greet({ name }) {
  const message = `Hello, ${name}!`;   // 결과 연산

  useEffect(() => {
    //Good
    document.title = 'Greetings page'; // Side-effect
  }, []);

  return <div>{message}</div>;         // 결과 반환
}

useEffect()는 2개의 매개변수를 받습니다.

useEffect(callback[, dependencies]);
  • callback은 side-effect 로직을 포함하는 callback 함수를 입력받습니다. React는 컴포넌트의 변경사항들을 반영하고 화면을 변경하고 useEffect()에 전달된 callback 함수를 실행합니다.
  • dependencies는 의존성 배열 매개변수이며 생략할 수 있습니다. dependencies 매개변수에 배열을 전달하면 useEffect()는 dependencies 배열에 포함된 값이 변경될 때에만 callback 함수를 실행합니다. dependencies 배열을 생략하면 useEffect()는 컴포넌트가 렌더링 될 때 마다 반복해서 실행됩니다.

Side-Effect 로직은 useEffect()의 callback 함수로 작성해주세요. Side-Effect 로직이 언제 실행 될지 제어하기 위해 dependencies 매개변수를 사용해주세요.

2. useEffect()의 dependencies 배열 매개변수

useEffect(callback, dependencies)의 dependencies 매개변수를 사용하면 Side-Effect 로직이 언제 실행될 지 통제할 수 있습니다. 

  • A) dependencies 매개변수를 생략할 경우: Side-Effect 로직은 컴포넌트가 렌더링 될 때 마다 실행됨
    import { useEffect } from 'react';
    
    function MyComponent() {
      useEffect(() => {
        //컴포넌트가 렌더링 될 때 마다 실행됨
      });  
    }
    
  • B) dependencies 매개변수에 빈배열을 전달할 경우: Side-Effect 로직은 컴포넌트가 렌더링된 후 한번만 실행됨
    import { useEffect } from 'react';
    
    function MyComponent() {
      useEffect(() => {
        // 최초 렌더링이 종료된 후 한번만 실행됨
      }, []);
    }
    
  • C) dependencies 매개변수에 값을 포함하는 배열을 전달할 경우: 배열에 포함된 값이 변경될 때마다 Side-Effect 로직이 실행됨
    import { useEffect, useState } from 'react';
    
    function MyComponent({ prop }) {
      const [state, setState] = useState('');
      useEffect(() => {
        // 최초 렌더링이 종료되고 한번 실행됨
        // dependencies 배열에 포함된 값이 변경될 때마다 실행됨
      }, [prop, state]);
    }
    

위에서 자주 사용되는 B)와 C)케이스에 대해서 자세히 알아보도록 합시다.

3. 컴포넌트 마운트와 Side-Effect

컴포넌트가 마운트 되고 Side-Effect 로직을 단 한번만 실행하려면 dependencies 매개변수에 빈배열을 지정해주세요.

import { useEffect } from 'react';

function Greet({ name }) {
  const message = `Hello, ${name}!`;

  useEffect(() => {
    // 마운트가 완료되고 한번만 실행됨
    document.title = 'Greetings page';
  }, []);

  return <div>{message}</div>;
}

위 코드에서 dependencies 매개변수로 빈배열(useEffect(..., []))이 전달되었습니다. 이렇게 작성하면 컴포넌트가 마운트 된 후 callback 함수가 단 한번만 실행됩니다.

심지어 name Property가 변경되어 컴포넌트가 다시 렌더링되더라도 Side-Effect 로직은 다시 실행되지 않습니다.

// 최초 렌더링
<Greet name="Eric" />   // Side-effect가 실행됨

// 두번째 렌더링: name prop 값이 변경됨
<Greet name="Stan" />   // Side-effect가 실행되지 않음

// 세번째 렌더링: name prop 값이 변경됨
<Greet name="Butters"/> // Side-effect가 실행되지 않음

4. 컴포넌트 업데이트와 Side-Effect

Side-Effect 로직 내에서 props나 state 값을 사용하려면 반드시 이 값들을 dependencies 매개변수로 전달해야합니다.

import { useEffect } from 'react';

function MyComponent({ prop }) {
  const [state, setState] = useState();

  useEffect(() => {
    // Side-effect로직이 `prop`와 `state` 값을 사용함
  }, [prop, state]);

  return <div>....</div>;
}

위와 같이 코드를 작성하면 dependencies 배열에 지정한 [prop, state] 값이 변경될 때 컴포넌트의 변경사항이 DOM에 반영되고 useEffect()의 callback 함수가 실행됩니다.

useEffect()의 dependencies 매개변수를 사용하면 컴포넌트의 렌더링 사이클과 무관하게 Side-Effect가 실행되어야 할 시점을 통제할 수 있습니다. 이는 useEffect() hook이 가지는 가장 중요한 특징입니다.

name prop를 사용하여 Greet 컴포넌트를 개선해보면 다음과 같습니다.

import { useEffect } from 'react';

function Greet({ name }) {
  const message = `Hello, ${name}!`;

  useEffect(() => {
    document.title = `Greetings to ${name}`; 
  }, [name]);

  return <div>{message}</div>;
}

useEffect() 함수의 dependencies 매개변수로 name이 지정되었습니다. Greet 컴포넌트는 처음 렌더링이 완료되고 Side-Effect 로직을 한번 실행할 것이고, 이 후 name이 변경될 때 마다 Side-Effect 로직을 다시 실행할 것입니다.

// 최초 렌더링
<Greet name="Eric" />   // Side-effect가 실행됨

// 두번째 렌더링: name prop 값이 변경됨
<Greet name="Stan" />   // Side-effect가 실행됨

// 세번째 렌더링: name prop 값이 변경되지 않음
<Greet name="Stan" />   // Side-effect가 실행되지 않음

// 네번째 렌더링: name prop 값이 변경됨
<Greet name="Butters"/> // Side-effect가 실행됨

5. 네트워크 호출

useEffect()는 네트워크를 통해 데이터를 송수신하는 Side-Effect 로직을 실행할 수 있습니다.

아래 FetchEmployeesByQuery 컴포넌트는 네트워크를 통해 임직원 내역을 내려받습니다. props으로 전달받는 query 값을 통해 조회될 임직원 내역을 결정합니다.

import { useEffect, useState } from 'react';

function FetchEmployeesByQuery({ query }) {
  const [employees, setEmployees] = useState([]);

  useEffect(() => {
    async function fetchEmployees() {
      const response = await fetch(
        `/employees?q=${encodeURIComponent(query)}`
      );
      const fetchedEmployees = await response.json(response);
      setEmployees(fetchedEmployees);
    }
    fetchEmployees();
  }, [query]);

  return (
    <div>
      {employees.map(name => <div>{name}</div>)}
    </div>
  );
}

컴포넌트가 마운트되면 useEffect()는 fetchEmployees() 비동기 함수를 호출하여 fetch 요청을 시작합니다.

fetch 요청이 완료되면 setEmployees(fetchedEmployees) 함수는 employees state를 새로운 임직원 내역으로 업데이트합니다.

렌더링이 완료된 후에는 query prop 값이 변경될 때마다 useEffect() hook이 새로운 fetch 요청을 시작하게됩니다.

useEffect(callback)의 callback 매개변수에는 async 함수를 전달할 수 없습니다. 하지만 callback 함수내에 async 함수를 정의하고 호출함으로서 비동기함수를 구현할 수 있습니다.

function FetchEmployeesByQuery({ query }) {
  const [employees, setEmployees] = useState([]);

  useEffect(() => {  // <--- async function를 사용할 수 없음
    async function fetchEmployees() {
      // ...
    }
    fetchEmployees(); // <--- 하지만 async function을 선언하고 호출할 수 있음
  }, [query]);

  // ...
}

컴포넌트가 마운트 된 후 fetch 요청이 단한번만 실행되도록 하려면 dependencies 매개변수에 빈배열을 지정해주세요 (예: useEffect(fetchSideEffect, []);)

6. Side-Effect 정리(Cleanup)

어떤 SideEffect 로직은 소켓을 닫거나, 타이머를 종료하거나, 이벤트를 제거하거나, 자원을 해제하는 등의 정리(Cleanup) 작업을 필요로 합니다.

useEffect(callback)에 전달한 callback 함수가 함수를 반환하면 useEffect()는 Side-Effect 정리(Cleanup)을 위해 해당 함수를 적절한 시점에 호출합니다.

useEffect(() => {
  // Side-effect...

  return function cleanup() {
    // Side-effect cleanup...
  };
}, dependencies);

정리(cleanup)작업은 다음과 같은 방법으로 실행됩니다.

A) 컴포넌트가 처음 마운트 된 후 useEffect()는 callback 함수를 호출하여 Side-Effect를 실행하지만 Cleanup 함수는 실행하지 않습니다.

B) 렌더링 후에는, useEffect()가 다시 Side-Effect 로직을 실행하기 전에, 이전에 실행되었던 Side-Effect의 Cleanup 함수를 실행합니다. Cleanup 함수가 실행완료된 후에 Side-Effect 로직을 실행합니다.

C) 컴포넌트가 언마운트되는 경우, useEffect()는 마지막으로 실행되었던 Side-Effect의 Cleanup 함수를 실행합니다.

Side-Effect Cleanup이 유용한 몇가지 예제를 살펴봅시다.

아래 예제의 <RepeatMessage message="My Message" /> 컴포넌트는 prop 갑승로 message를 받습니다. 그리고 2초마다 message prop 값을 콘솔에 로그로 남깁니다.

import { useEffect } from 'react';

function RepeatMessage({ message }) {
  useEffect(() => {
    setInterval(() => {
      console.log(message);
    }, 2000);
  }, [message]);

  return <div>I'm logging to console "{message}"</div>;
}

데모 페이지를 열고 아무 값이나 입력해보세요. 콘솔 로그는 Input에 입력했었던 모든 내용들을 2초마다 출력합니다. 만약 마지막에 입력했던 내용만 출력하려면 어떻게 해야할까요?

Side-Effect Clean 함수를 사용하면 문제를 해결할 수 있습니다. Cleanup을 통해 이전의 타이머를 취소하고 새로운 타이머를 시작하도록 작성하면 됩니다. 아래는 이전의 타이머를 중지시키는 Cleanup함수를 포함하는 에제입니다.

import { useEffect } from 'react';

function RepeatMessage({ message }) {
  useEffect(() => {
    const id = setInterval(() => {
      console.log(message);
    }, 2000);
    return () => {
      clearInterval(id);
    };
  }, [message]);

  return <div>I'm logging to console "{message}"</div>;
}

데모 페이지를 열고 아무 값이나 입력해보면 마지막에 입력했던 값이 콘솔에 출력되는 것을 확인하실 수 있습니다.

7. 결론

useEffect(callback, dependencies)는 함수형 컴포넌트에서 Side-Effect를 관리하기 위한 hook입니다. callback 매개변수에는 Side-Effect를 포함하는 함수를 전달해야합니다. dependencies 매개변수에는 Side-Effect 로직이 의존하는 값(props나 state 등) 배열을 지정해야합니다.

useEffect(callback, dependencies)에 전달된 callback 함수는 컴포넌트가 처음 마운트된 후에 실행됩니다. 혹은 dependencies에 전달한 값이 변경될 때마다 실행됩니다.

useEffect() hook는 클로저에 의존하기 때문에, 개발자는 이를 잘 관리해야합니다. 또한 Stale closures issue를 조심해야합니다.

useEffect()를 잘 사용하기 위해 다음에는 useEffect()의 무한 루프 함정을 이해하고 회피해는 방법에 대해 다루도록 하겠습니다.

 

댓글