본문으로 바로가기

[React] useEffect란?

category 1. 웹개발/1_1_5 React JS 2022. 3. 25. 22:51

  

[useEffect란?]

애플리케이션 내 컴포넌트의 여러 값들을 활용해 동기적으로 부수 효과를 만드는 메커니즘입니다. 그리고 이 부수 효과가 '언제' 일어나는지보다 어떤 상태값과 함께 실행되는지 살펴보는 것이 중요합니다.

 

먼저 useEffect의 일반적인 형태를 살펴봅시다.

function Component() {
  // 첫 번째 인수로는 effect 함수, 두 번째 인수로는 dependency array 
    useEffect(() => {
        // do Somthing...

        // effect 이후에 어떻게 정리(clean-up)할 것인지 표시
        return () => {
           ....
        }
    }, []) // 빈 배열을 입력하는 경우 렌더링 될 때 마다 실행
}

 

첫 번째 인수로는 실행할 부수 효과가 포함된 함수들, 두 번째 인수로는 의존성 배열을 전달합니다. 이 의존성 배열은 어느정도 길이를 가진 배열일 수도, 아무런 값이 없는 빈 배열일 수도 있고, 배열 자체를 넣지 않고 생략할 수도 있습니다.

 

의존성 배열이 변경될 때 마다 useEffect()의 첫 번째 인수인 콜백을 실행하는 것은 사실 널리 알려진 사실입니다. 하지만 useEffect는 어떻게 의존성 배열이 변경된 것을 알고 실행할까요? 여기서 한 가지 기억해야 할 사실은 바로 함수형 컴포넌트는 매번 함수를 실행할 때 마다 렌더링을 수행한다는 것입니다. useEffect는 state와 props의 변화 속에서 일어나는 렌더링 과정에서 실행되는 부수 효과 함수라고 볼 수 있습니다. 이해가 안됐을 수 있으니 이제 각 인수에 대해 자세히 알아보겠습니다.

 

1. 의존성 배열

의존성 배열은 보통 빈 배열을 두거나, 아예 아무런 값도 넘기지 않거나, 혹은 사용자가 직접 원하는 값을 넣어줄 수 있습니다. 만약 빈 배열을 둔다면 리액트가 이 useEffect는 비교할 의존성이 없다고 판단해 최초 렌더링 직후에 실행된 다음부터는 더 이상 실행되지 않습니다.

 

그렇다면 한 가지 의문점을 제기해보겠습니다.

의존성 배열이 없는 useEffect가 매 렌더링마다 실행된다면 그냥 useEffect 없이 써도 되는 것이 아닐까요? 아래 코드처럼요.

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

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

 

위의 두 코드는 명백히 리액트에서 차이점을 지니고 있습니다.

useEffect는 컴포넌트 렌더링의 부수 효과, 즉 컴포넌트의 렌더링이 완료된 이후에 실행됩니다. 반면 1번과 같이 함수 내부에서의 직접 실행은  컴포넌트가 렌더링되는 도중에 실행됩니다. useEffect의 effect는 컴포넌트의 사이드 이펙트, 즉 부수 효과를 의미한다는 것을 명심해야 합니다. useEffect는 컴포넌트가 렌더링된 후에 어떠한 부수 효과를 일으키고 싶을 때 사용하는 훅입니다.

 

2. Clean Up 함수

클린업 함수는 보통 이벤트를 등록하고 지울 때 사용해야 한다고 알려져있습니다. 아래 코드의 출력은 어떻게 될까요?

const [counter, setCounter] = useState(0);

function handleClick() {
  setCounter((prev) => prev + 1);
}

useEffect(() => {
  function addMouseEvent() {
    console.log(counter);
  }
  
  window.addEventListener('click', addMouseEvent);
    
  // Clean Up 함수
  return () => {
    console.log('클린업 함수 실행!', counter);
    window.removeEventListener('click', addMouseEvent);
  };
}, []);

 

아래는 위의 코드 결과입니다.

클린업 함수 실행! 0
1

클린업 함수 실행! 1
2

클린업 함수 실행! 2
3

// ...

 

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

 

따라서, useEffects는 그 콜백이 실행될 때 마다 이전의 클린업 함수가 존재한다면 그 클린업 함수를 실행한 뒤에 콜백을 실행합니다. 따라서 이벤트를 추가하기 전에 이전에 등록했던 이벤트 핸들러를 삭제하는 코드를 클린업 함수에 추가하는 것입니다. 이렇게 함으로써 특정 이벤트의 핸들러가 무한히 추가되는 것을 방지할 수 있습니다.

 

* unMount와 다른 점

클린업 함수는 생명주기 메서드의 unMount 개념과는 조금 차기아 있습니다. 언마운트는 특정 컴포넌트가 DOM에서 사라진다는 것을 의미하는 클래스형 컴포넌트의 용어입니다. 클린업 함수는 언마운트라기보다는 함수 컴포넌트가 리렌더링됐을 때 의존성 변화가 있었을 당시 이전의 값을 기준으로 실행되는, 말 그대로 이전 상태를 청소해 주는 개념으로 보는 것이 옳습니다.

 

[useEffect를 사용할 때 주의할 점]

1. eslint-disable-line, react-hooks/exhaustive-deps 주석은 최대한 자제하라.

[React] useEffect 의존성 warning - react-hooks/exhaustive-deps 왜?

 

[React] useEffect 의존성 warning - react-hooks/exhaustive-deps 왜?

이번 글에서는 왜 eslint-disable-line, react-hooks/exhaustive-deps 경고를 알려주는지 알아보도록 하겠습니다. 리액트 코드를 작성하거나, 읽다보면 제법 심심치 않게 eslint-disable-line, react-hooks/exhaustive-deps

itprogramming119.tistory.com

 

2. useEffect의 첫 번째 인수에 함수명을 부여하라.

useEffect를 사용하는 많은 코드에서 useEffect의 첫 번째 인수로 익명 함수를 넘겨줍니다. 이는 리액트 공식 문서도 마찬가지입니다. useEffect의 수가 적거나 복잡성이 낮다면 이러한 익명 함수를 사용해도 큰 문제는 없습니다. 그러나 useEffect의 코드가 복잡하고 많아질수록 무슨 일을 하는 useEffect 코드인지 파악하기 어려워집니다. 이때 이 useEffect의 인수를 익명 함수가 아닌 적절한 이름을 사용한 기명 함수로 바꾸는 것이 좋습니다. 우리가 변수에 적절한 이름을 붙이는 이유는 해당 변수가 왜 만들어졌는지 파악하기 위함입니다. useEffect도 마찬가지로 적절한 이름을 붙이면 useEffect의 목적을 파악하기 쉬워집니다.

useEffect(
  function logActiveUser() {
    logging(user.id);
  },
  [user.id],
)

 

3. 거대한 useEffect를 만들지 마라.

useEffect는 의존성 배열을 바탕으로 렌더링 시 의존성이 변경될 때마다 부수 효과를 실행합니다. 이 부수 효과의 크기가 커질수록 애플리케이션 성능에 악영향을 미칩니다. 비록 useEffect가 컴포넌트의 렌더링 이후에 실행되기 때문에 렌더링 작업에는 영향을 적게 미칠 수 있지만 여전히 자바스크립트 실행 성능에 영향을 미친다는 것은 변함이 없습니다. 가능한 한 useEffect는 간결하고 가볍게 유지하는 것이 좋습니다. 만약 부득이하게 큰 useEffect를 만들어야 한다면 적은 의존성 배열을 사용하는 여러 개의 useEffect로 분리하는 것이 좋습니다. 만약 의존성 배열이 너무 거대하고 관리하기 어려운 수준까지 이른다면 정확히 이 useEffect가 언제 발생하는지 알 수 없게 됩니다. 만약 의존성 배열에 불가피하게 여러 변수가 들어가야 하는 상황이라면 최대한 useCallback과 useMemo 등으로 사전에 정제한 내용들만 useEffect에 담아두는 것이 좋습니다. 이렇게 하면 언제 useEffect가 실행되는지 좀 더 명확하게 알 수 있습니다.

 

4. 불필요한 외부 함수를 만들지 마라.

useEffect의 크기가 작은 것과 같은 맥락에서 useEffect가 실행하는 콜백 또한 불필요하게 존재해서는 안됩니다. 다음 코드를 살펴보겠습니다.

import { useState } from "react";

function Component({ id }) {
  const [info, setInfo] = useState(null);
  const controllerRef = useRef(null);
  const fetchInformation = useCallback(async (fetchId) => {
    controllerRef.current?.abort();
    controllerRef.current = new AbortController();

    const result = await fetchInformation(fetchId, { signal: controllerRef.signal });
    setInfo(await result.json());
  })

  useEffect(() => {
    fetchInformation(id);
    return () => controllerRef.current?.abort();
  }, [id, fetchInformation]);

  return <div>rendering</div>
}

 

이 컴포넌트는 props를 받아서 그 정보를 바탕으로 API 호출을 하는 useEffect를 가지고 있습니다. 그러나 useEffect 밖에서 함수를 선언하다 보니 불필요한 코드가 많아지고 가독성이 떨어졌습니다. 위의 코드를 수정해보겠습니다.

import { useEffect, useState } from "react";

function Component({ id }) {
  const [info, setInfo] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    (async () => {
      const result = await fetchInformation(fetchId, { signal: controllerRef.signal });
      setInfo(await result.json());
    })()

    return () => controllerRef.abort();
  }, [id]);

  return <div>rendering</div>
}

 

useEffect 외부에 있던 관련 함수를 내부로 가져왔더니 훨씬 간결한 모습입니다. 불필요한 의존성 배열도 줄일 수 있었고, 또 무한루프에 빠지기 위해 넣었던 코드인 useCallback도 삭제할 수 있었습니다. useEffect 내에서 사용할 부수 효과라면 내부에서 만들어서 정의해서 사용하는 편이 훨씬 도움이 됩니다.

 

 

 

 

Reference
react.org-hooks-useEffect

useEffect 의존성 warning - react-hooks/exhaustive-deps 왜?