useEffect 훅의 목적 중 하나는 부수효과를 안전하게 처리하는 것입니다. 부수효과란 앱이 의도한 대로 동작하기 위해 실행되어야 하지만 현재의 컴포넌트 렌더링 과정에 직접적인 영향을 미치지는 않는 작업입니다. 이러한 작업들은 꼭 실행되어야하는 작업이지만 현 컴포넌트 렌더링 과정에 직접적이고 즉각적인 영향을 미치지 않는 작업들이죠. 즉각적인 변화를 주지 않고 뒤늦게 영향을 주기 때문에 무한 루프 같은 문제점이 발생할 수 있습니다.

useEffect로 부수효과 안전하게 처리하기

HTTP 요청에서의 부수효과

일반적으로 이러한 부수 효과는 외부 서버에서 데이터를 불러오는 작업 등 비동기 함수를 실행하는 상황에서 쉽게 볼 수 있습니다. 다음 코드를 보시죠.

import { useState, useEffect } from 'react';

const DataFetchingComponent = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  const fetchData = async () => {
    try {
      const response = await fetch('https://api.example.com/data');
      const result = await response.json();

      setData(result);
      setLoading(false);
    } catch (error) {
      console.error('Error fetching data:', error);
      setLoading(false);
    }
  };

  fetchData();

  //...

 

앱이 렌더링되면 fetchData함수가 실행되어 Data와 isLoading의 상태가 바뀝니다. 그런데 매번 렌더링할 때마다 fetchData가 실행되면서 set 함수들이 호출되기 때문에 무한 루프에 빠지게 될 것입니다. 이럴 때는 다음과 같이 useEffect를 사용해 무한 루프를 해결해야 됩니다.

import { useState, useEffect } from 'react';

const DataFetchingComponent = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/data');
        const result = await response.json();

        setData(result);
        setLoading(false);
      } catch (error) {
        console.error('Error fetching data:', error);
        setLoading(false);
      }
    };

    fetchData();

  }, []); // 빈 종속성 배열은 컴포넌트의 마운트와 언마운트 시에만 실행됩니다.

  //...

 

이렇게 바뀐 useEffect의 셋업 함수(useEffect의 첫 번째 인자인 함수의 실행문들)는 컴포넌트가 실행된 이후, 그러니깐 컴포넌트 함수가 가진 return문의 JSX코드가 반환된 이후에 평가됩니다. useEffect 안에서 컴포넌트의 리렌더링을 트리거하는 코드가 있다면 컴포넌트는 리렌더링 될 것입니다. 그리고 대부분의 경우에는 그러한 코드들이 포함되도록 작성하게 될 것입니다. 추가적으로 리렌더링의 직전에 실행되는 코드도 추가할 수 있는데, 제 블로그의 이 글을 참고하거나 공식문서의 useEffect를 참고하세요.

useRef 그리고 DOM  API를 사용하기 위한 useEffect

앞서 useEffect는 컴포넌트 함수가 JSX코드를 반환한 이후에 실행된다고 하였습니다. 바로 이 점 덕분에 useRef로 참조하고 있는 DOM element를 사용할 때 useEffect를 사용할 수 있습니다. 다음 코드를 보시죠.

import { useEffect, useRef } from 'react';

const ExampleComponent = () => {
  const myRef = useRef();

  useEffect(() => {
    myRef.current.focus();
  }, []);

  return <input ref={myRef} />;
};

 

만약 useEffect를 사용하지 않고 바로 myRef.current.focus();를 사용한다면, 오류가 발생할 것입니다. 함수 컴포넌트의 JSX가 반환되기 전이라 myRef가 input element를 참조하기 전이기 때문입니다. 하지만 위의 코드는 useEffect를 사용하였고 useEffect를 사용함으로 인해, myRef.current.focus(); 는 jsx코드가 반환되고 DOM에 렌더링 된 이후에 실행됩니다. 즉 input element와 myRef가 무사히 연결된 이후 myRef.current.focus();가 호출되기 때문에, 버그없이 해당 input 태그에 포커스가 주어질 것입니다.

clean up 함수를 이용해서 부수효과 방지하기

예전의 글에서도 언급한적 있듯이 useEffect는 클린업 함수를 가지고 있습니다. 클린업 함수는 컴포넌트가 리렌더링되기 직전에 실행되는 함수입니다. 이를 통해 부수효과를 관리할 수 있습니다.

import { useEffect } from 'react';

const IntervalExample = () => {

  useEffect(() => {
    const intervalId = setInterval(() => {
    console.log("실행")
    }, 1000);
  });
  // ...

 

위의 코드 블럭에서는 useEffect의 의존성 배열이 작성되어 있지 않습니다. 내부의 setInterval은 마운트와 언마운트를 포함해 모든 렌더링마다 실행되겠죠. 매 번 컴포넌트가 렌더링될 때마다 새로운 interval이 추가되어 쌓이게 될 것입니다. 다른 코드를 추가하여 interval을 지울 수도 있겠지만 컴포넌트가 리렌더링될 때 이전의 interval을 지우는 것이 더 합리적인 경우가 많을 것 같습니다. 이는 다음과 같이 해결할 수 있습니다.

  useEffect(() => {
    const intervalId = setInterval(() => {
    console.log("실행")
    }, 1000);

    return () => {
      clearInterval(intervalId);
    };

 

이렇게 하면 언마운트 직전을 포함하여 모든 렌더린 직전에 interval은 사라지게 됩니다. 성능 저하의 염려가 없어지는 것이죠.

그런데 모든 부수효과에 useEffect가 필요하지는 않습니다.

useEffect 훅이 필요한 경우는 무한 루프를 방지하거나 컴포넌트가 최소 한 번은 렌더링 된 후 작동하는 코드가 있을 경우입니다. 그래서 클릭 등, 사용자에 의한 이벤트에 의해 실행되는 함수일 경우에는 useEffect를 사용하지 않는 경우가 많습니다. 사용자의 클릭에만 반응하므로 무한 루프에 빠지지 않기 때문입니다. 물론 애초에 훅은 컴포넌트의 최상단에서만 사용할 수 있으므로 사용자와의 상호작용을 처리하는 함수에서는 사용이 불가능합니다.

또 동기(Synchronous) 작업일 경우 입니다. 모든 작업이 즉각적으로 처리되므로 useEffect는 사용할 필요가 없습니다. 다음 코드를 보시죠.

import { useState, useEffect } from 'react';

const LocalStorageExample = () => {
  const [storedData, setStoredData] = useState('');

  useEffect(() => {
    const dataFromLocalStorage = localStorage.getItem('key');
    setStoredData(dataFromLocalStorage || 'Default Value');
  }, []);

 //...

 

컴포넌트가 마운트될 때 로컬스토리지에서 데이터를 가져오기 위해 useEffect를 사용하고 있습니다. 컴포넌트가 최초로 마운트되면 localStorage.getItem('key');를 통해 key로 저장된 데이터를 가져와 storedData에 저장합니다. 그런데 이 상황에서는 굳이 useEffect를 사용할 필요가 없습니다. 로컬스토리지에서 데이터를 가져오는 작업은 즉각적으로 이루어지는 동기 작업이기 때문입니다. 대신 다음과 같이 사용하는 것이 더 나을 수 있습니다.

import { useState } from 'react';
const dataFromLocalStorage = localStorage.getItem('key') || 'Default Value';

const LocalStorageExample = () => {
  const [storedData, setStoredData] = useState(dataFromLocalStorage);

  // ...

 

로컬스토리지에서 가져온 데이터를 애초에 storedData의 초기값으로 넣어주는 것이 더 나은 선택이 될 것입니다. 그리고 이렇게 사용할 경우 리렌더링이 발생하지 않으므로(state가 바뀌지 않기 때문) 화면의 번쩍거림도 없을 것입니다.

 

dataFromLocalStorage를 굳이 컴포넌트 밖으로 뺀 이유는 만약 컴포넌트 안에 해당 코드를 넣게 되면 매번 렌더링 할때마다 localstorage에 접근하여 데이터를 가져오는 과정을 실행하기 때문에 불필요한 오버헤드가 발생하기 때문입니다. 컴포넌트 밖으로 꺼내어, 초기값은 단 한번만 할당되도록 하는 것이 더 나을 것입니다.그리고 useState의 초기값은 마운트시에만 사용되고 그 이후는 리렌더링 직전의 값을 참고하도록 만들어져 있어 초기값이 계속 만들어지는 것은 명백한 낭비입니다. 다만 이렇게 컴포넌트 밖에 정해진 변수는 번들링된 JavaScript 파일에 포함된 단일 변수로 존재하며 메모리 공간을 계속해서 차지할 것입니다. 공간 및 시간 복잡도를 살펴보아 초기값이 생성되는 위치를 정하는 것이 좋을 것입니다.

 

컴포넌트 내부가 아닌 자바스크립트 최상위 파일의 코드는 최초 앱의 실행, 다시 말해 웹 사이트에 접속하는 순간 실행됩니다. 따라서 고정된 함수를 포함한 상수들을 사용해야할 때 컴포넌트 외부에서 코드를 작성하기도 합니다.

그러나 로컬 스토리지에 저장하는 값이 바뀔 수 있고 해당 컴포넌트가 항상 최신 값을 가져와야 한다면 localstorage에 접근하는 함수는 컴포넌트 안에 있도록 만들거나 Redux 혹은 Zustand를 통해 전역으로 관리하는 것이 나을 것입니다.

+ Recent posts