React Deep Dive 3강

RookieAND·2024년 3월 16일
4

React Deep Dive

목록 보기
4/8
post-thumbnail

✒️ React 에서 쓰이는 Hook 에 대해 알아보자

✏️ useState

useState 훅은 함수 컴포넌트 내부에서 상태를 정의하고 이를 관리할 수 있도록 돕는 Hook 이다.

해당 훅은 state 값과 이를 변경하도록 돕는 setState 함수를 배열로 묶어 반환하며, 함수형 컴포넌트 내부에서는 비구조화 할당으로 이를 인계 받는다.

const Component = () => {
    const [state, setState] = useState();

    // state 값을 컴포넌트 내부에서 직접 변경하는 것은 리렌더링을 유발하지 않는다.
    const handleButtonClick = () => {
        state = 'hi';
    }

    // 변경된 state 값을 setState 로 넘겨주지 않고 이를 실행하는 것은 리렌더링을 유발하지 않는다.
    const handleButtonClick2 = () => {
        state = 'h1'
        setState();
    }

    // setState 함수에 변경하고자 하는 값을 넣어야 비로소 의도한 리렌더링이 발생한다.
    const handleButtonClick3 = () => {
        setState('hi');
    }


    return (
        <>
            <h1>{state}</h1>
        </>
    )
}

useState 로 반환된 state 값은 Closure 로 관리된다.

  • useState 에서 관리하는 상태 값을 변경하기 위해서는 반드시 setState 함수에 변경하고자 하는 값을 인자로 넣어 이를 실행해야 한다.
  • state 값을 함수 내부에서 변경한다 해도, useState 훅으로 반환받은 값은 컴포넌트 내부의 지역 변수에 할당된 상태이기에 이를 수정하더라도 실제 state 가 변경되지는 않는다.
  • 따라서 클로저에 접근할 수 있는 유일한 수단인 setState 함수를 통해 state 값을 수정해야 한다.

✏️ lazy initialization

useState 의 초기 값으로 상수가 아닌 특정 값을 반환하는 함수를 넣을 수도 있다.
만약 함수를 초기 값으로 넣는다면 state 가 초기에 생성될 때는 해당 함수를 실행한 후 반환된 값을 저장하고, 이후에는 해당 함수를 실행시키지 않는다.

const Component = () => {
    const [state, setState] = useState(() => {
        // 복잡한 연산이 초기 값을 생성하는 과정에서 필요하다면, 
        console.log('복잡한 연산...');
        return 0;
    });

    return (
        <div>
            <h1>{state}</h1>
        </div>
    )
}

어차피 초기 값을 위해서 쓰이는 거라면, 굳이 써야 할까?

예시로 든 케이스의 경우 결국 initial Rendering 에서 초기 상태 값을 구하기 위해 함수를 실행한 후 반환된 값을 저장하고, 이후 리렌더링에서는 해당 값을 쓰지 않고 업데이트 된 상태 값을 반환할 텐데 굳이 이를 사용하는 이유가 궁금했다.

lazy initialization 을 useState 에서 사용하는 이유는 아래와 같다.

  1. 초기 값으로 함수의 실행을 넣어둔다면, 매 렌더링마다 해당 함수가 실행된다.
const Component = () => {
    const [state, setState] = useState(heavyWork());

    return (
        <div>
            <h1>{state}</h1>
        </div>
    )
}
  • 만약 위와 같이 무거운 연산이 필요한 함수의 실행 결과를 useState 의 초기 값으로 넣었다고 가정해보자.
  • 이후 리렌더링에서는 해당 함수의 반환 값이 state 에 쓰이지 않겠지만 매 렌더링마다 함수의 호출은 계속된다.
  • 따라서 함수를 호출하지 않고 함수 자체를 콜백 함수로 넘겨 두 번째 렌더링부터는 이를 "실행하지 않도록" 막는 역할을 한다고 보면 된다.

✏️ useEffect

useEffect 훅은 React 외부에서 관리되는 시스템과 리액트 컴포넌트 간의 Sync 를 맞추기 위해 쓰이는 Hook 이다.

첫 번째 인자로는 실행될 부수 효과가 포함된 함수를, 두 번째 인자로는 의존성 배열 (deps) 를 반환한다.

import { useEffect } from "react";
import { createConnection } from "./chat.js";

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState("https://localhost:1234");

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]);
  // ...
}
  • 의존성 배열 내의 값이 변경될 때마다 useEffect 의 첫번째 인자로 넘긴 함수가 실행된다.
  • 컴포넌트가 매번 렌더링 될때마다 useEffect 의 의존성 배열 내 값들이 이전과 달라졌는지를 비교한다.
  • 만약 의존성 배열 내 값들 중 하나라도 이전과 달라졌다면 부수 효과를 일으키는 함수를 실행한다.

✏️ clean-up function in React

useEffect 훅의 첫번째 인자로 넘긴 함수는 cleanup 함수 를 반환할 수 있다.
해당 함수는 컴포넌트의 렌더링이 일어나기 전에 실행되며, 함수가 정의되었을 당시 선언되었던 값을 기반으로 실행되기에, 이후 렌더링에서 변경된 값을 참조하지 않는다.

clean-up 함수의 경우 주로 React 외부에서 관리되는 부수 효과를 정리하기 위해 쓰인다.

  • 특정 DOM 에 등록했던 이벤트 핸들러를 제거할 때
  • 컴포넌트 Unmount 시 WebSocket Connection 을 끊어야 할 때

✏️ dependencies array in React

  • 의존성 배열은 useEffect 를 실행시키는 중요한 기준이다. 이전 렌더링 시점과 현재 시점을 기준으로 의존성 배열 내 요소가 변경될 경우 useEffect 는 재실행된다.
  • 만약 배열에 아무런 값도 넘겨주지 않는다면 해당 useEffect 는 컴포넌트의 최초 렌더링 직후에만 실행되며, 이후 렌더링에 대해서는 실행되지 않는다.

의존성 배열이 없는 useEffect 가 매 렌더링마다 실행되면 useEffect 없이 써도 되지 않을까?

useEffect 훅은 브라우저에서 컴포넌트가 완전히 렌더링 된 이후에 실행됨을 최대한 보장한다. 따라서 useEffect 는 반드시 Client - Side 에서 실행해야 하는 코드를 사용할 때 좋은 수단이 된다.

만약 useEffect 외부에 특정 코드를 실행할 경우 이는 Server - Side 에서도 실행될 수 있음을 의미하며, 작업이 긴 태스크의 경우 렌더링을 지연시킨다.

또한 useEffect 에서 관리하는 Passive Effect 가 실행되기 이전에 렌더링을 유발시키는 Task 가 트리거 될 경우, 현재 렌더링에 대한 Effect 를 소비해야 함으로 렌더링 이전에 실행이 될 가능성도 염두해야 한다.

✏️ useEffect 현명하게 사용하는 방법

  1. eslint-disable-line, react-hooks/exhaustive-deps 지키기

react-hooks/exhaustive-deps ESLint Rule 의 경우, useEffect 내부에서 쓰이는 Reactive 한 값이 반드시 deps 에 있어야 함을 지적하는 규칙이다.

만약 해당 룰을 지키지 않는다면 useEffect 에서 의도했던 Reactive 한 값의 변경을 추적하지 못하므로 인자로 받은 setup 함수와 실제 실행 여부에 대한 연결점이 사라짐을 의미한다.

useEffect 에 빈 배열을 넣어야 한다면 꼭 해당 setup 함수가 컴포넌트의 렌더링 상황에서 일어나야 하는지를 잘 고민해야 한다.

const Component = () => {

  const [isLoading, setIsLoading] = useState(false);

  // 과연 이렇게 하는 게 꼭 맞을까?
  useEffect(() => {
    setIsLoading(true);
  }, [])

  // 최초 렌더링 이후로 계속 값이 true 라면, 이렇게 해도 된다.
  if (!isLoading) {
    setIsLoading(true);
  }
}
  1. useEffect 의 첫 번째 인수에 함수 명을 부여하라

책에서는 useEffect 내부의 코드가 복잡해질 것을 염려하여 setup 함수를 Arrow Function 같은 익명 함수로 넘기지 말고 기명을 할 것을 추천한다.

하지만 근본적으로 useEffect 내부의 로직이 거대할 경우 useEffect 가 언제 실행되는지 알기가 어렵고 컴포넌트의 로직이 특정 값의 변경에 따라 지나치게 의존적인 구조로 이루어질 수 있다.

만약 불가피하게 Reactive 한 값이 Effect 내부에 여러 개 들어간다면 최대한 해당 값들을 Memoization 하여 값의 변경을 막는 것이 좋다.

  1. 블필요한 외부 함수를 만들지 마라.

useEffect 내부에서 실행되는 함수를 외부로 뺄 경우 불필요한 코드가 많아지고 가독성 또한 떨어질 가능성이 높다.

useEffect 내부에서 쓰이는 값이나 함수는 최대한 Effect 내부에 선언한다면 불필요한 의존성 배열도 줄어들기에 보다 효과적으로 useEffect 를 관리할 수 있다.

useEffect 의 setup 함수로 비동기 함수를 바로 넣을 수 없는 이유?

function Example() {
  const [data, dataSet] = useState<any>(null)

  // useEffect must return a cleanup function or nothing...
  useEffect(async () => {
      let response = await fetch('api/data')
      response = await response.json()
      dataSet(response)
  }, [])

  return <div>{JSON.stringify(data)}</div>
}

만약 useEffect setup 가 비동기로 동작한다면 사용자의 의도와는 다르게 내부에서 실행된 비동기 로직의 응답 속도에 따라 상이한 결과를 보이게 된다.

  • 만약 두 번의 Effect 가 실행되었고, 첫번째 API 응답 속도는 예상치 못한 지연으로 인해 10초 뒤 응답이 왔다.
  • 하지만 두 번째 API 는 1초 뒤 응답이 왔고, 먼저 useEffect 가 실행되어 결과를 setState 에 넣어 실행했다.
  • 사용자는 첫 번째 API 에 대한 결과가 먼저 나오기를 기대했으나, 실제 실행 결과는 두 번째 API 실행에 대한 결과를 먼저 본 셈이다.

useEffect 는 setup 함수가 cleanup 함수를 반환하거나 반환 값이 없는 함수이기를 기대한다. 따라서 setup 함수가 비동기로 동작할 경우 (Promise 를 반환) Warning 을 띄우는 것이다.

function Example() {
  const [data, dataSet] = useState<any>(null)

  useEffect(() => {
    async function fetchMyAPI() {
      let response = await fetch('api/data')
      response = await response.json()
      dataSet(response)
    }

    // 내부에서 async 함수를 생성하고 이를 실행하면 된다.
    fetchMyAPI()
  }, [])

  return <div>{JSON.stringify(data)}</div>
}

만약 정 useEffect 내부에서 비동기 작업을 호출하고 싶다면 async 함수를 내부에서 선언한 후에 이를 실행시키면 된다. 단 이 경우 사용자의 의도와 다르게 useEffect 가 실행될 수 있음을 유의해야 한다.

✏️ useLayoutEffect

useEffect 와 동일한 메커니즘을 가지나, 모든 DOM 의 변경이 완료된 이후에 동기적으로 동작하는 Hook 이다.

한 가지 유의할 점은 useLayoutEffect 의 setup 함수는 브라우저 렌더링 이후가 아닌 DOM 의 변경 이후에 실행된다는 점이다. 그리고 동기적으로 발생한다는 것은 반드시 useLayoutEffect작업이 종료된 이후에 브라우저 렌더링이 진행된다는 점이다.

useLayoutEffect 는 DOM 이 비록 계산되었으나 이것이 실제 화면에 반영되기 전에 하고 싶은 작업이 있을 때만 사용하는 것이 좋다. 예를 들면 DOM 요소를 기반으로 한 애니메이션, 스크롤 위치 제어 같이 화면이 띄워지기 전에 반영되면 좋은 작업들이 있다.

✏️ useMemo

useMemo 는 비용이 큰 연산의 결과를 Memoization 하고 저장된 값을 반환하도록 하는 Hook 이다.

첫 번째 인자로는 값을 반환하는 함수를, 두 번째 인자로는 해당 함수가 의존하는 deps (의존성 배열) 을 받는다.

만약 deps 내 값 중 하나 이상이 동등성 비교를 통해 변경되었음을 확인했다면 React 는 useMemo 내 함수를 재실행하여 이를 반환하고 Memoization 한다.

import { useMemo } from 'react';

function TodoList({ todos, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  // ...
}

✏️ useCallback

useCallback 은 인자로 받은 함수를 Memoization 하고, 이후 렌더링 시 재생성된 함수가 아닌 이전에 저장했던 함수를 반환하는 Hook 이다.

첫 번째 인자로는 함수를, 두 번째 인자로는 해당 함수가 의존하는 deps (의존성 배열) 을 받는다.

In JavaScript, a function () {} or () => {} always creates a different function,

하위 컴포넌트에 함수를 props 로 전달해야 할 경우, 기존에는 매 렌더링마다 새로운 함수를 생성하여 넘겨주기 때문에 리렌더링을 유발시켰다. 이를 useCallback 으로 감싸 전달하면 불필요한 리렌더링을 막을 수 있다.

물론 하위 컴포넌트도 React.memo 로 감싸야 한다. (props 가 변경되지 않았다면 리렌더링을 방지해야 하므로)

function ProductPage({ productId, referrer, theme }) {
  // theme 이 변경될 때마다 handleSubmit 는 재생성 된다.
  function handleSubmit(orderDetails) {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }
  
  return (
    <div className={theme}>
      {/* ... ShippingForm 에 인자로 전달되는 함수는 매 렌더링마다 달라진다. */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

✏️ useRef

useRef Hook 은 컴포넌트의 렌더링을 발생시키지 않는 값을 컴포넌트 내부에 저장하는 Hook 이다.

current property 로 useRef 훅이 관리하는 Ref 값을 가져올 수 있으며, 컴포넌트가 리렌더링 되어도 이전과 같은 값을 유지한다.

// DOM 을 ref 에 적재하여 내부 로직에서 사용할 수 있다.
const Component = () => {
  const ref = useRef<HTMLDivElement | null>(null);

  return <input ref={ref} type="text">
}
  • 특정 DOM 의 property 와 컴포넌트가 서로 상호작용해야 할 상황에서는 Callback Ref 를 사용하여 DOM 내부의 속성을 ref 로 접근할 수 있다.
  • DOM 의 width, height 를 받아 계산을 해야 할 필요가 있을 때나 그 외 속성의 변화를 관측하고 싶다면 useEffect 대신 Callback Ref 로 해결이 가능하다./
// DOM 을 ref 에 적재하여 내부 로직에서 사용할 수 있다.
const Component = ({ show }) => {
  const ref = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    // ref 가 null 인 상태로 동작하기에 focus 가 되지 않는다.
    ref.current?.focus()
  }, [])

  return (
    <form>
      <button type="button" onClick={() => setShow(true)}>
        show
      </button>
      // 조건부 렌더링으로 input 을 실행하기에 초기 렌더링 시 ref 는 null 이다.
      {show && <input ref={ref} />}
    </form>
  )
}

const Component = ({ show }) => {
  const ref = useRef<HTMLDivElement | null>(null);

  return (
    <form>
      <button type="button" onClick={() => setShow(true)}>
        show
      </button>
      // input 이 렌더링된 이후에 ref 가 호출되므로 정상적으로 focus 가 동작한다
      {show && <input ref={(element) => element?.focus()} />}
    </form>
  )
}

const Component = () => {
  const [height, setHeight] = useState(0)

  // 굳이 불필요한 useEffect 말고, 아래와 같이 DOM 의 height 를 받을 수 있다.
  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height)
    }
  }, [])

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  )
}

컴포넌트 외부에 값을 선언하고, 이를 참조하면 되지 않나요?

// 컴포넌트 외부에서 관리되는 값을 선언한다.
value = 0;

const Component = () => {

  // 컴포넌트 내부에서 외부에 선언된 값을 참조한다면.. 될까?
  const handleClick = () => {
    value += 1;
  }
}

해당 방식은 몇 가지 단점을 가지는데, 항목을 따지자면 아래와 같다.

  1. 컴포넌트가 여러 번 생성되더라도 각 컴포넌트에서 참조하는 value 는 한 곳으로 동일하다.
  • 컴포넌트가 생성된다는 의미는 해당 모듈 내부의 함수가 실행된다는 것이고 함수 외부에 선언된 변수는 하나다.
  • 따라서 매번 컴포넌트 함수가 실행되더라도 참조되는 변수는 최초에 생성된 value 가 유일하다.
  1. 컴포넌트가 아직 렌더링 되지 않았지만 외부에서 변수를 생성했기 때문에 메모리에 기본적으로 적재된다.

✏️ Context

Context 는 React 에서 특정 컴포넌트 트리에 소속된 컴포넌트들이 접근 가능한 값을 의미한다.

기존에는 특정 값을 하위 컴포넌트로 넘겨주기 위해서는 props drilling 을 사용하여 이를 전달했으나. 해당 방식은 아래와 같은 문제점을 낳았다.

  1. 값을 사용하는 컴포넌트는 항상 이를 받기 위해 props 를 열어야 하며, 여러 컴포넌트를 거쳐 값이 인계된 경우에는 데이터의 흐름을 추적하기 어렵다.
  2. 값을 넘겨주는 컴포넌트의 경우에도 하위 컴포넌트에 값을 넘겨주기 위해 불필요한 props 를 받아야 하기 때문에 불편하다.

Context 는 특정 컴포넌트 트리를 대상으로 접근 가능한 값을 제공하기 때문에, 불필요한 props drilling 없이도 하위 컴포넌트에서 해당 값을 사용할 수 있다.

const PopOverContext = createContext<PopOverContextType | null>(null);

const PopOverRoot = ({ children }: PropsWithChildren) => {
    const {
        value: isPopOverOpen,
        open: openPopOver,
        close: closePopOver,
    } = useDisclosure(false);

    const value = useMemo(
        () => ({
            isPopOverOpen,
            openPopOver,
            closePopOver,
        }),
        [closePopOver, isPopOverOpen, openPopOver],
    );

    return (
        <PopOverContext.Provider value={value}>
            <div className={S.wrapper}>
                {children}
            </div>
        </PopOverContext.Provider>
    );
};

✏️ useContext

Context 의 값을 컴포넌트에서 사용하기 위해서는 useContext 훅을 반드시 사용해야 하며, 해당 컴포넌트는 반드시 Context Provider 내부에 위치해야 한다.

만약 해당 Context 에 대해서 여러 개의 Provider 가 존재한다면 useContext 훅을 호출한 위치로부터 가장 가까운 Provider 에서 관리되는 값을 가져온다.

const PopOverItem = ({
    children,
    className,
    onClick,
    ...restProps
}: ComponentProps<'button'>) => {
    const { closePopOver } = useContext(PopOverContext);

    const handlePopOverItem = (event: MouseEvent<HTMLButtonElement>) => {
        event.stopPropagation();
        onClick?.(event);
        closePopOver();
    };

    return (
        <button
            className={clsx(S.item, className)}
            onClick={handlePopOverItem}
            {...restProps}
        >
            {children}
        </button>
    );
};

export default PopOverItem;

✏️ useContext 사용 시 주의점

Context 는 상태 관리가 아닌 상태 주입을 위한 API 다.

상태 관리 라이브러리가 되려면 특정 상태를 기반으로 다른 상태를 만들어내거나, 경우에 따라 상태의 변화를 최적화할 수 있어야 한다.

하지만 Context 의 경우 단순히 저장된 값을 하위 컴포넌트로 전달할 뿐이며, 컴포넌트 트리 최상단에 Provider 를 감싸기 때문에 만약 Provider 단에서 리렌더링이 발생할 경우 모든 하위 컴포넌트도 리렌더링 된다.

이는 React.memo 로 어느 정도 방어가 가능하지만, 이 또한 만능이 아니라는 점을 알아야 한다.

✏️ useReducer

useState 와 같이 상태를 관리하는 Hook 이지만, 사용자가 정의한 dispatcher 함수에 맞춰 특정 Action 을 기반으로 상태를 변경하도록 유도하는 Hook 이다.

  • 첫 번째 인자로 reducer 함수를 받는다, 해당 함수는 useReducer 의 Action 을 정의하는 함수이며 변경 이전의 state 와 action 을 받아 이를 기반으로 변경된 상태를 반환한다.
  • 두 번째 인자로 useReducer 에서 관리하는 상태의 initialState 를 받는다.
  • 세 번째 인자로 초기 값을 반환하는 함수를 받는다. useState 에서 인자에 함수를 넘기는 것 처럼 Lazy initialization 을 하고 싶을 때 사용한다.
import { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        name: state.name,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      return {
        name: action.nextName,
        age: state.age
      };
    }
  }
  throw Error('Unknown action: ' + action.type);
}

export default function Form() {
  const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });

  function handleButtonClick() {
    dispatch({ type: 'incremented_age' });
  }

  function handleInputChange(e) {
    dispatch({
      type: 'changed_name',
      nextName: e.target.value
    }); 
  }

  return (
    <>
      <input
        value={state.name}
        onChange={handleInputChange}
      />
      <button onClick={handleButtonClick}>
        Increment age
      </button>
      <p>Hello, {state.name}. You are {state.age}.</p>
    </>
  );
}

✏️ forwardRef & useImperativeHandle

ref 는 key 와 같이 React 에서 특수하게 다루는 props 이기 때문에 하위 컴포넌트로 ref 를 넘기고 싶다면 반드시 forwardRef 함수로 컴포넌트를 감싸야 한다.

그리고 useImperativeHandle Hook 은 ref 에 할당된 값을 DOM 객체가 아닌 컴포넌트 내부에서 커스터마이징 한 객체로 변경할 수 있다. 기존에는 부모 컴포넌트로부터 ref 를 넘겨 받았다면, 이제는 자식 컴포넌트에서 노출하고 싶은 ref 를 별도로 정의할 수 있다.

import { useRef } from 'react';
import MyInput from './MyInput.js';

export default function Form() {
  const ref = useRef(null);

  function handleClick() {
    // 하위 컴포넌트에서 ref 의 focus 핸들러를 정의했기에 이를 사용합니다.
    ref.current.focus();
  }

  return (
    <form>
      // MyInput 하위 컴포넌트로 ref 를 전달합니다.
      <MyInput placeholder="Enter your name" ref={ref} />
      <button type="button" onClick={handleClick}>
        Edit
      </button>
    </form>
  );
}

// MyInput.jsx
import { forwardRef, useRef, useImperativeHandle } from 'react';

const MyInput = forwardRef(function MyInput(props, ref) {
  const inputRef = useRef(null);

  // 부모 컴포넌트로부터 받은 ref 를 기반으로 커스텀한 Handler 를 제작합니다.
  useImperativeHandle(ref, () => {
    return {
      focus() {
        inputRef.current.focus();
      },
      scrollIntoView() {
        inputRef.current.scrollIntoView();
      },
    };
  }, []);

  return <input {...props} ref={inputRef} />;
});

export default MyInput;

React 에서는 ref 를 기반으로 한 로직을 그다지 선호하지 않는다.

ref 는 React 의 LifeCycle 에 영향을 받지 않는 로직이며, useImperativeHandle 은 기존의 React 에서 고려하는 부모 -> 자식의 단방향 흐름을 단숨에 역행하는 로직이다.

즉 state 기반의 UI 변경을 유도하는 React 의 철학과 ref 기반의 명령형 프로그래밍 기반의 코드가 정면으로 충돌하는 상황이 발생한다.

React 의 공식문서에서도 이러한 이유 때문에 ref 를 기반으로 한 로직을 컴포넌트 내부에 많이 사용하지 않을 것을 권장한다.

✏️ useDebugValue

useDebugValue 는 React 애플리케이션을 개발하는 과정에서 디버깅이 필요한 정보를 인자로 넣으면 개발자 도구에서 이를 열람하도록 돕는 Hook 이다.

useDebugValue 는 반드시 다른 Hook 내부에서만 실행될 수 있음을 유의하자, 만약 컴포넌트 레벨에서 이를 실행하려 할 경우 작동하지 않는다.

✒️ Rules of Hook

React 에서 제공하는 훅은 아래와 같은 규칙을 기본적으로 가진다.

  1. 모든 훅은 항상 컴포넌트 최상단에서 호출되어야 한다.
  2. 특정 조건문 혹은 컴포넌트 내부에서 정의한 함수 내부에서 훅을 호출할 수 없다.
  3. 훅을 호출할 수 있는 주체는 함수형 컴포넌트 혹은 사용자 정의 훅이다.

만약 이러한 규칙을 지키지 않는다면 Hook 간의 순서가 보장되지 않으며 이는 개발자가 의도하지 않은 결과로 이어질 확률이 높다.

✒️ 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?

✏️ 사용자 정의 훅

서로 다른 두 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 사용하는 것이 사용자 정의 훅이다.

사용자 정의 훅을 만들기 위해서는 훅의 식별자 명이 반드시 use 로 시작해야 한다.

되도록이면 react-hooks/rules-of-hooks 의 규칙을 준수해야 하며, 리액트 Hook 의 규칙도 같이 준수해야 에러가 발생하지 않는다.

import { useEffect } from 'react';

/**
 * Window 기반의 커스텀 이벤트를 수신 받아 로직을 실행시키는 Hook useEventListeners
 */
export const useEventListeners = <T extends keyof WindowEventMap>(
    eventName: T,
    handler: (event: WindowEventMap[T]) => void,
    options?: boolean | AddEventListenerOptions,
): void => {
    useEffect(() => {
        window.addEventListener(eventName, handler, options);

        return () => {
            window.removeEventListener(eventName, handler, options);
        };
    }, [eventName, handler, options]);
};

✏️ 고차 컴포넌트 (HOC)

컴포넌트의 로직을 재사용하기 위해 쓰이며 JS 에서 함수 또한 일급 객체에 속하므로 고차 함수의 개념으로 생각하면 편하다.

React 에서는 React.memo 라는 HOC 를 제공하며, 함수형 컴포넌트를 React.memo 함수의 인자로 넣어 memo 로직이 먼저 실행되고, 이후 결과를 기반으로 컴포넌트의 렌더링을 진행한다.

사용자 정의 고차 컴포넌트를 제작할 때는 사용자 정의 훅과 다르게 with 접두사를 붙이는 것이 관례이다.

고차 컴포넌트 사용 시에는 부수 효과를 최소화 해야 하며, 인자로 받은 컴포넌트의 props 를 임의로 수정하거나 추가, 삭제하는 일이 없어야 한다.

// 인자로 받은 컴포넌트를 렌더링 하기 전 localStorage 의 값을 체크한다.
export const withAuth = (Component : ComponentType) => (props: ComponentProps<typeof Component>) => {
  const router = useRouter();

  useEffect(() => {
    if (!localStorage.getItem("accessToken")) {
      router.push("/main");
    }
  }, []);

  return <Component {...props} /> ;
}

// main.tsx
const UserPage = () => {
  // ...
}


// withAuth 를 감싸 로그인이 된 유저만 접근이 가능하도록 수정한다.
export default withAuth(UserPage)
profile
항상 왜 이걸 써야하는지가 궁금한 사람

0개의 댓글