UseEffect

박요진·2023년 9월 9일
0

React에서는 Side Effect를 발생시킬 때는 아래의 두 조건을 충족시켜야 합니다.
1. 렌더링 이후에 발생시켜야 한다.
2. 매 렌더링 이후가 아니라 조건부로 원하는 순간에만 실행시킬 수 있어야 한다.
React에서는 위의 조건을 충족시키면서 Side Effect를 발생 시킬 수 있는 useEffect라는 Hook을 제공해주며, 이를 통해 손쉽게 Side Effect를 발생시킬 수 있습니다.

1. useEffect 란...?

  • Data Fetching, 구독 등의 side effect는 매우 조심스럽게 다루어야 합니다. 그럼 side effect를 언제, 어떻게 발생시켜야 할까요?

1-1. React에서 Side Effect의 올바른 발생 시점

const App = () => {
  const doSideEffect = () => {
    // do some side effect
  };

  doSideEffect();

  return <h1>Hello World</h1>;
};
  • 함수 컴포넌트에서 side effect를 발생시키고자 할 때, 랜더링 단계(UI를 만들어내는 과정)에서 side effect를 발생시키게 되면 두 가지의 문제가 발생합니다.

1. side effect가 blocking 합니다.
2. 매 렌더링마다 side effect가 수행됩니다.

  • 이게 무슨 소리인지 한번 알아봅시다.

1-1-1. side effect가 렌더링을 blocking

  • 위의 코드는 사이드 이펙트를 함수 컴포넌트 본문 안에서 실행시킵니다.
  • 기본적으로 코드는 위에서 아래 방향으로 순차적으로 실행됩니다.
  • 따라서 함수 컴포넌트는 해당 함수의 동작이 끝날 때까지 JSX를 리턴하는 코드로 넘어가지 않습니다.
  • 컴포넌트가 JSX를 return 하기 전까지 UI가 브라우저 상에 렌더링이 되지 않기때문에 결국 사이드 이펙트가 끝나기 전까지 렌더링을 하지 못하고 멈춰있게 되는 겁니다.

1-1-2. 매 렌더링마다 side effect가 수행

  • 특정한 side effect 들은 매번 실행될 필요가 없을 수도 있습니다.
  • 예를 들면 피드 리스트를 보여주기 위해서는 최초의 피드 데이터들을 가져오는 (Data Fetching) side effect가 필요합니다.
  • 하지만 곰곰이 생각을 해보면 외부에서 데이터를 가져오는 side effect는 매 렌더링마다 수행될 필요는 없고, 오히려 매번 수행한다면 비효율적입니다.
  • App이라는 함수가 다시 호출되고, 그렇다면 다시 함수의 내부의 코드를 위에서부터 아래 방향으로 순차적으로 실행시킵니다.
  • 만약 어떠한 작은 UI 업데이트가 발생할 때마다 매번 모든 피드 데이터를 다 가져온다면 불필요한 동작을 계속 수행하기에 비효율적입니다.

1-1-3. 그래서? 요약 좀 해봐

  • 자 우리가 React에서 side effect를 언제 발생시켜야하는지 생각을 다시 해봅시다.
  1. 렌더링을 Blocking 하지 않기 위해서 렌더링이 모두 다 완료되고 난 후 실행할 수 있어야 한다.
  2. 매 렌더링마다 실행되는 것이 아니라 내가 원할 때만 조건부로 실행될 수 있어야 한다.

자, 위 두 가지 조건을 충족시키면서 발생시키는 것이 가장 좋습니다. 그런데 React에서는 위의 요구사항을 모두 충족시키면서 side effect를 발생시킬 수 있도록 도와주는 useEffect 라는 훅(Hook)이 이미 존재합니다.

1-2. UseEffect

UseEffect는 React에서 side effect를 편리하고 안전하게 발생시킬 수 있도록 도와주는 hook입니다.

1-2-1. UseEffect 사용 방법

import { useEffect } from 'react';

const App = () => {
  // 코드 생략

  // useEffect(콜백 함수);
  useEffect(doSideEffect);

  return <h1>Hello, Wecoder</h1>;
};
  • 위 코드는 side effect를 발생시키는 것이 아니라 useEffect의 인자로 전달했습니다.
    위와 같이 useEffect의 인자로 전달된 콜백함수는 곧바로 호출되는 것이 아니라 모든 렌더링이 완료 된 후에 호출됩니다.

1-3. 조건부로 Side Effect 발생시키기

  • useEffect를 통해 렌더링이 모두 완료된 후 side effect를 실행한다는 요구사항은 충족시켰지만 아직도 매 렌더링마다 side effect가 실행이 된다는 사실은 변함이 없습니다.

  • 사실 useEffect는 콜백 함수 외에 한 가지의 매개 변수를 더 가지고 있습니다. useEffect의 완전한 형태는 아래와 같습니다.

useEffect(콜백 함수, 의존성 배열);

1-3-1. useEffect의 동작 방식

  • 첫 번째 렌더링 이후에는 무조건 useEffect에 전달된 콜백 함수를 호출하고 다음 렌더링부터는 아래의 조건에 따라서 동작합니다.
  1. 의존성 배열이 전달되지 않았다면 매 렌더링마다 콜백함수 호출
  2. 의존성 배열이 전달되었다면 의존성 배열의 값 검사
    2-1. 의존성 배열에 있는 값 중 하나라도 이전 렌더링과 비교했을 때 달라졌다면 콜백 함수 호출
    2-2. 의존성 배열에 있는 값이 이전 렌더링과 비교했을 때 모두 다 같다면 콜백함수를 호출하지 않는다.
  • 즉, useEffect에서 첫 번째 인자인 콜백 함수는 '실행시킬 동작'을 결정하고, 두 번째 인자인 의존성 배열은 '실행시킬 타이밍'을 결정짓는다고 할 수 있습니다.
import { useEffect } from 'react'

// 사용법
useEffect(콜백 함수, 의존성 배열);

// 1. 의존성 배열이 전달되지 않았으므로 매 렌더링마다 side effect가 실행된다
useEffect(() => {
  // side effect
});
// 2. 의존성 배열이 비어있으므로 최초 한번만 side effect가 실행된다.
useEffect(() => {
  // side effect  
}, []);

// 3. 첫 번째 렌더링 이후에 side effect를 실행하고
// 그 이후에는 value 값이 변했을 때만 실행한다.
useEffect(() => {
  // side effect
}, [value]);

// 4. 첫 번째 렌더링 이후에 side effect를 실행하고
// 그 이후에는 value1, value2 중 하나라도 변하면 side effect를 실행한다.
useEffect(() => {
  // side effect
}, [value1, value2]);

1-3-2. 함수 컴포넌트의 렌더링과 useEffect가 발생하는 과정을 풀어서 설명하자면...

  1. 컴포넌트가 렌더링 된다.

    (최초로 진행되는 렌더링은 브라우저에 처음으로 이 컴포넌트가 보였다는 의미로 mount 라고 표현합니다.)

  2. useEffect 첫 번째 인자로 넘겨준 콜백 함수가 호출된다. (Side Effect)

  3. 컴포넌트의 state 또는 props가 변경되었을 경우 리렌더링이 발생한다. (update)

  4. useEffect는 두 번째 인자에 들어있는 의존성 배열을 확인한다

    • a. 만약 의존성 배열이 전달되지 않았거나 / 의존성 배열 내부의 값 중 이전 렌더링과 비교했을 때 변경된 값이 하나라도 있다면 첫 번째 인자로 넘겨준 콜백 함수가 호출된다. (Side Effect)
    • b. 의존성 배열 내부의 값 중 이전 렌더링과 비교했을 때 변경된 값이 없다면 콜백 함수를 호출하지 않는다.
    • c. state 또는 props가 변경된다면 3~4의 과정을 반복
  5. 컴포넌트가 더 이상 필요 없어지면 화면에서 사라진다.

    (컴포넌트가 브라우저의 화면에서 사라졌다는 의미로 unmount라고 표현합니다.)

2. Clean Up Effect

clean up은 무언가를 정리하고 치운다는 의미 입니다.
useEffect hook은 side effect를 clean up 해주는 기능 또한 가지고 있습니다.

2-1. Clean Up의 필요성

  • Clean Up이 필요한 시점의 기준은 해당 side effect가 지속적으로 남아있는가? 를 생각해보면 됩니다.
useEffect(() => {
  const countTime = () => {
    console.log('100ms가 지났습니다.');
  };

  setInterval(countTime, 100);
}, []);
  • 위의 코드는 Clean up이 필요합니다.
  • 코드를 해석해보면 이 side effect는 setInterval 함수를 100ms 마다 countTime 함수가 호출되도록 하고 있습니다.
  • 의존성 배열에도 빈 배열이 전달되었기 때문에 이 side effect는 clean up 해주지 않으면 컴포넌트가 unmount 되는 경우 등 serInterval을 통한 구독이 필요가 없어진 상황에서도 계속해서 콘솔이 출력되고 있을 것 입니다.
  • 정리하면, 불필요하게 계속해서 side effect가 남아있어서 비효율적으로 작동할 수 있고, 프로그램의 동작이 의도한 대로 되지 않을 수도 있기 때문에 지속적으로 남아있는 side effect는 반드시 clean up을 해주어야 합니다.

2-2. Clean Up 하는 방법

  • useEffect는 side effect를 clean up 할 수 있는 방법을 제공해 줍니다.
    useEffect에서 side effect를 clean up 하기 위해서는 useEffect에 전달한 콜백 함수에서 clean up을 하는 함수를 리턴하면 됩니다.
useEffect(() => {
  const button = document.getElementById('consoleButton');

  const printConsole = () => {
    console.log('button clicked');
  };

  button.addEventListener('click', printConsole);

  // side effect를 clean up 하기 위한 함수를 선언한다.
  const removeEventListener = () => {
    button.removeEventListener('click', printConsole);
  };

  // clean up 함수를 return 한다.
  return removeEventListener;
});
  • Clean up은 위와 같이 발생시킨 side effect를 상쇄하기 위한 함수를 만들 뒤 그 함수를 return 해주면 됩니다.
  • addEventListener로 등록한 eventListnerremoveEventListener 함수를 통해서 제거할 수 있기 떄문에 해당 동작을 하는 함수(clean up 함수)를 만든 뒤 콜백 함수 내에서 clean up 함수를 리턴해주었습니다.
  • clean up 함수를 return 만 해준다면 clean up 함수를 적절한 시점에 호출해 주는 일은 useEffect가 알아서 처리해줍니다.

2-2-1. useEffect가 clean up 함수를 호출해주는 경우

1. 다음 side effect를 발생시키기 전
2. 컴포넌트가 unmount 될 때

2-2-2. 위의 코드의 동작을 차근차근 풀어보면...

  1. useEffect에 의존성 배열을 전달하지 않았기 때문에 해당 effect는 매 렌더링마다 실행된다.
  2. 리렌더링이 발생해서 useEffect가 다시 호출되는 상황이 발생한다.
    • a. clean up 함수를 리턴해줬기 때문에 clean up 함수가 호출된다.
    • b. clean up 함수가 호출된 뒤 effect가 발생된다.
    • c. 리렌더링이 발생하면 a ~ b의 과정이 반복된다.
  3. 컴포넌트가 unmount 되면 clean up 함수가 호출된다.
profile
프론트엔드 개발자 지망생입니다.

0개의 댓글

관련 채용 정보