useEffectEvent 만들어 보았습니다!

정우시·2023년 9월 30일
4

우아한테크코스

목록 보기
13/14
post-thumbnail

서론

안녕하세요! 요즘카페 팀에서 프론트엔드를 담당하고 있는 정우시입니다.

리액트 애플리케이션을 개발하면서, 컴포넌트 외부와의 상호작용을 다루는 데 자주 useEffect를 활용하게 됩니다. 이렇게 사용되는 useEffect는 주로 데이터 가져오기, DOM 조작, 타이머 설정 등과 같은 부수 효과를 다루는 데에 사용됩니다.

저희는 '어떻게 하면 useEffect를 더 효율적으로 활용할 수 있을까?'라는 고민을 하다가 useEffectEvent라는 것을 알게 되었고, 이를 사용하면 코드를 더 효율적으로 작성할 수 있을 것으로 판단했습니다.

이와 관련해서 아래의 본문을 통해 예제 코드와 함께 자세히 설명해 드리도록 하겠습니다.

본론

리액트의 스냅샷(Snapshot)과 useEffect의 의존성 관리

리액트 애플리케이션을 개발하다보면, 컴포넌트의 상태(State)나 프롭스(Props)가 변할 때마다 원하는 동작을 수행하기 위해 useEffect 훅을 사용하는 경우가 많습니다. 그런데 때로는 이렇게 의존성을 관리하는 것이 조금 까다로워질 수 있습니다. 두 가지 예시 코드를 통해 이러한 문제와 함께 리액트의 스냅샷(Snapshot) 개념에 대해 살펴보겠습니다.

예시 1: 의존성 없이 빈 배열로 사용하기

import { useState, useEffect } from "react";

export default function App() {
  const [fruit, setFruit] = useState("Apple");

  useEffect(() => {
    const handleClick = () => console.log(fruit);

    window.addEventListener("click", handleClick);

    return () => window.removeEventListener("click", handleClick);
  }, []);

  return (
    <div>
      <h1>{fruit}</h1>
      <button onClick={() => setFruit("Banana")}>change</button>
    </div>
  );
}

useEffect 내부에서 fruit을 의존성으로 사용하지 않고 빈 배열([])을 사용하고 있습니다. 이 경우, fruit의 변경에 따라 이펙트가 다시 실행되지 않습니다. 버튼을 클릭하더라도 콘솔에는 항상 처음 설정한 "Apple"이 출력됩니다.

예시 2: 의존성으로 fruit 사용하기

import { useState, useEffect } from "react";

export default function App() {
  const [fruit, setFruit] = useState("Apple");

  useEffect(() => {
    const handleClick = () => console.log(fruit);

    window.addEventListener("click", handleClick);

    return () => window.removeEventListener("click", handleClick);
  }, [fruit]);

  return (
    <div>
      <h1>{fruit}</h1>
      <button onClick={() => setFruit("Banana")}>change</button>
    </div>
  );
}

위 코드에서는 useEffect 내부에서 fruit을 의존성(dependency)으로 사용하고 있습니다. 즉, fruit이 변경될 때마다 이펙트가 다시 실행됩니다. 이 코드를 실행하면 버튼을 클릭할 때마다 콘솔에 현재 fruit이 찍히게 됩니다.

스냅샷(Snapshot)의 개념

이러한 동작의 차이는 리액트의 스냅샷(Snapshot) 개념과 관련이 있습니다. useEffect에서 의존성 배열(dependency array)을 지정하면, 그 배열 안의 값들이 변경될 때마다 이펙트가 "스냅샷"을 찍어서 저장하게 됩니다. 그리고 이 스냅샷을 사용하여 이펙트 내에서 이전 값을 비교하고, 값이 변경되었을 때만 이펙트를 다시 실행합니다.

따라서 예시 1에서는 빈 배열을 사용하면 의존성이 없기 때문에 스냅샷이 찍히지 않습니다. 이로 인해 이펙트는 항상 처음 설정한 fruit 값인 "Apple"을 출력하게 되는 것입니다.

반면, 예시 2에서 fruit을 의존성으로 사용하면, fruit의 값이 변경될 때마다 새로운 스냅샷이 찍히고, 그 값과 이전 스냅샷의 값이 비교되어 이펙트를 다시 실행하게 됩니다.

그러나 만약 의존성이 fruit 하나만 있는 것이 아닌 여러 개가 있으면 어떨까요? 종속성 배열을 정확하게 관리하지 않으면 의도치 않은 렌더링과 성능 문제가 발생할 수 있습니다.

또한 종종 특정 로직은 특정 종속성의 변경에 반응하지 않아야 할 때가 있습니다. 예를 들어, 특정 데이터가 변경될 때만 이를 로깅하고 싶을 때, 해당 데이터의 변경에만 반응하고 다른 종속성의 변경에는 반응하지 않아야 합니다.

이런 상황에서 useEffectEvent라는 개념이 도움이 됩니다. 이를 통해 특정 코드 블록을 effect 안에서 실행하면서, 이 코드 블록이 특정 종속성에 반응하지 않도록 할 수 있습니다. 이를 통해 코드를 더 명확하고 효율적으로 작성할 수 있으며, 애플리케이션의 성능을 최적화할 수 있습니다.

useEffectEvent

useEffectEvent는 리액트의 effect 안에서 특정 코드 블록을 실행할 때, 해당 코드 블록이 특정 종속성에 반응하지 않도록 하는 메커니즘입니다. 이것은 props 또는 state의 최신 값을 기반으로 작업을 수행하려는 경우에 유용하며, 이렇게 하면 effect가 특정 값이 변경될 때만 다시 실행되며, 다른 값들의 변경에는 반응하지 않습니다.

다만 useEffectEvent를 사용을 하기 위해서는 리액트 18 기준으로 정식으로 출시된 훅은 아니기 때문에 커스텀 훅으로 직접 작성을 해야합니다.

useEffectEvent 훅을 다음과 같이 작성해 보았습니다:

import { useEffect, useRef } from 'react';

function useEffectEvent(callback) {
  // 1. useRef를 사용하여 초기 콜백 함수를 저장할 callbackRef를 생성합니다.
  const callbackRef = useRef(callback);

  // 2. useEffect를 사용하여 콜백 함수가 변경될 때마다 callbackRef를 업데이트합니다.
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // 3. 최종적으로 반환되는 함수는 콜백 함수를 호출하는 함수입니다.
  //    이 함수는 어떤 인자든 받을 수 있도록 (...args) => { ... } 형태로 정의되어 있습니다.
  return (...args) => {
    // 4. 현재 저장된 콜백 함수를 가져와서 타입을 확인합니다.
    if (typeof callbackRef.current === 'function') {
      // 5. 콜백 함수가 함수 타입인 경우 해당 함수를 호출하고,
      //    함수에 (...args)를 통해 전달된 인자들을 전달합니다.
      callbackRef.current(...args);
    } else {
      // 6. 콜백 함수가 함수 타입이 아닌 경우 콘솔에 오류 메시지를 출력합니다.
      console.error('Callback이 함수 타입이 아닙니다.');
    }
  };
}
  1. useRef를 사용하여 callback을 저장할 callbackRef를 생성합니다. callbackRef는 현재 콜백 함수를 추적하는 데 사용됩니다.

  2. useEffect를 사용하여 callback이 변경될 때마다 callbackRef를 업데이트합니다. 이렇게 하면 항상 가장 최신의 콜백 함수를 사용할 수 있습니다.

  3. useEffectEvent 훅은 마지막에 함수를 반환합니다. 이 함수는 인자로 받은 인자들을 받아서 콜백 함수를 호출합니다.

  4. 반환된 함수가 호출될 때, 현재 저장된 콜백 함수인 callbackRef.current를 가져와서 해당 콜백 함수의 타입을 확인합니다.

  5. 콜백 함수가 함수 타입인 경우, callbackRef.current(...args)를 사용하여 콜백 함수를 호출하고, 함수에 (...args)를 통해 전달된 인자들을 전달합니다.

  6. 콜백 함수가 함수 타입이 아닌 경우, 콘솔에 오류 메시지를 출력합니다.

useEffectEvent를 이용해서 예제 코드 리팩토링 해보기

import { useState, useEffect, useRef } from "react";

// --- useEffectEvent 훅 ---
function useEffectEvent(callback) {
  const callbackRef = useRef(callback);

  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  return (...args) => {
    if (typeof callbackRef.current === "function") {
      callbackRef.current(...args);
    } else {
      console.error("Callback이 함수 타입이 아닙니다.");
    }
  };
}
// ------

export default function App() {
  const [fruit, setFruit] = useState("Apple");

  const handleClick = useEffectEvent(() => {
    console.log(fruit);
  });

  useEffect(() => {
    window.addEventListener("click", handleClick);

    return () => window.removeEventListener("click", handleClick);
  }, []);

  return (
    <div>
      <h1>{fruit}</h1>
      <button onClick={() => setFruit("Banana")}>change</button>
    </div>
  );
}

이전에 소개한 예제 코드를 useEffectEvent를 사용하여 리팩토링하였습니다. useEffectEvent를 활용하면 의존성을 간편하게 관리할 수 있습니다.

먼저, useEffectEvent 훅을 사용하여 handleClick 함수를 생성합니다. 이 함수는 현재 fruit 상태를 콘솔에 출력하는 역할을 합니다.

const handleClick = useEffectEvent(() => {
  console.log(fruit);
});

그리고 useEffect를 사용하여 handleClick 함수를 클릭 이벤트에 연결합니다. 이때, useEffect의 두 번째 인자로 빈 배열([])을 전달하여 이 이펙트가 컴포넌트가 처음 마운트될 때만 실행되도록 설정합니다.

useEffect(() => {
  window.addEventListener("click", handleClick);

  return () => window.removeEventListener("click", handleClick);
}, []);

나머지 코드는 이전 예제와 동일합니다. 컴포넌트의 반환값으로는 fruit 값을 화면에 출력하는 <h1> 요소와 "change" 버튼이 있습니다. 버튼을 클릭하면 setFruit 함수를 호출하여 fruit 상태를 변경할 수 있습니다.

이렇게 useEffectEvent를 사용하면 코드가 간결해지며, 또한 의존성이 없이도 개발자의 의도대로 useEffect를 사용할 수 있게 됩니다.

결론

이 글에서는 리액트 애플리케이션 개발 중 자주 마주하게 되는 useEffect 훅과 함께 발생하는 의존성 관리의 어려움을 다뤘습니다. 빈 배열과 의존성 배열을 사용한 useEffect의 동작 차이를 설명하고, useEffectEvent라는 특별한 훅을 소개하여 의존성 관리를 보다 효율적으로 처리하는 방법을 제안했습니다.

useEffectEvent는 현재 정식으로 출시된 것은 아니지만, 리액트 애플리케이션 개발에서 의존성 관리와 코드 최적화에 유용하게 사용될 수 있는 훅입니다. 저희는 요즘카페 프로젝트에서 useEffectEvent를 사용해볼 계획이며, 이를 통해 코드의 가독성과 유지보수성을 향상시키려고 합니다. 프론트엔드 개발자로서 의존성 관리와 성능 최적화에 관심이 있다면, useEffectEvent를 고려해봐도 좋다고 생각합니다.

감사합니다.


참고 자료

profile
프론트엔드 공부하고 있는 정우시입니다.

0개의 댓글