IT 개발자가 되기위한 여정

컴퓨터 공부를 시작함에 앞서 계획 및 개발에 대한 내용을 풀어나갈 생각입니다.

IT 학습/프레임워크

React W Hooks 톺아보기

제로시엘 2023. 12. 24. 14:29

시작하기에 앞서

이 포스트를 통해 커스텀 훅의 사용 예시와 더불어 실제 프로젝트의 적용 코드 및 사용 주의사항 등을 공유할 예정입니다.

먼저 Hooks 란?

리액트(React)의 훅(Hooks)은 함수형 컴포넌트에서 상태 관리, 라이프 사이클 이벤트 처리, 그리고 React의 다른 기능들을 사용할 수 있게 해주는 기능들입니다. 훅을 사용하면 클래스 기반 컴포넌트에서만 가능했던 여러 기능들을 함수형 컴포넌트 에서도 쉽게 사용할 수 있습니다. 우리가 자주 쓰는 useEffect , useState를 포함해 정의된 Hooks를 통해 React의 생명주기를 다룰 수 있습니다.

Hooks의 종류

Built-in React Hooks – React

[Built-in React Hooks – React

The library for web and native user interfaces

react.dev](https://react.dev/reference/react/hooks)

  1. 상태 훅(State Hooks): 컴포넌트가 사용자 입력과 같은 정보를 "기억"하게 해줍니다. 예를 들어, useStateuseReducer가 이에 해당합니다.
  2. 컨텍스트 훅(Context Hooks): 컴포넌트가 멀리 떨어진 부모로부터 정보를 props 없이 받을 수 있게 해줍니다. useContext가 이에 해당합니다.
  3. 참조 훅(Ref Hooks): 컴포넌트가 렌더링에 사용되지 않는 정보(예: DOM 노드, 타임아웃 ID)를 유지할 수 있도록 합니다. useRefuseImperativeHandle이 여기에 속합니다.
  4. 효과 훅(Effect Hooks): 컴포넌트가 외부 시스템과 연결되고 동기화할 수 있도록 합니다. useEffect, useLayoutEffect, useInsertionEffect가 이에 해당합니다.
  5. 성능 훅(Performance Hooks): 재렌더링 성능을 최적화하기 위해 사용됩니다. useMemouseCallback이 여기에 해당합니다.
  6. 리소스 훅(Resource Hooks): 컴포넌트가 상태의 일부로 갖지 않고도 리소스에 접근할 수 있게 해줍니다. use가 이에 해당합니다.
  7. 기타 훅(Other Hooks): 주로 라이브러리 작성자에게 유용하며, 일반적인 애플리케이션 코드에서는 자주 사용되지 않습니다. useDebugValue, useId, useSyncExternalStore가 여기에 속합니다.
  8. 사용자 정의 훅(Your own Hooks): 사용자가 자신의 JavaScript 함수로 커스텀 훅을 정의할 수 있습니다.

8가지의 Hooks로 나눌 수 있으며 이 중 우리가 알아볼 것은 사용자 정의 훅 즉 Custom Hook이다.

Custom Hook이란?

Custom Hooks: Sharing logic between components

커스텀 훅: 컴포넌트간의 로직 공유

커스텀 훅의 경우 여러 곳에서 반복적으로 사용되는 로직을 분리하여 재사용성을 높이고 가독성을 향상시킬 수 있습니다.

예시 1) 간단한 적용 예

다음을 통해 간단한 사례로 커스텀 훅을 사용하는 예시에 대해 설명하겠습니다.

function InputComponent({ inputRef }) {
  return <input ref={inputRef} />;
}

function ButtonComponent({ onClick }) {
  return <button onClick={onClick}>Focus on Input</button>;
}

function ParentComponent() {
  const inputRef = useRef();

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <>
      <InputComponent inputRef={inputRef} />
      <ButtonComponent onClick={focusInput} />
    </>
  );
}

위와 같이 인풋과 버튼 그리고 그 두 가지를 합친 컴포넌트가 있다고 가정하겠습니다. 단순하게 버튼을 클릭 시 input에 포커스를 주는 기능을 하고 있습니다. 이와 같은 기능을 커스텀 훅을 통해 분리한다면 다음과 같아집니다.

// 분리된 커스텀 훅
function useInputFocus() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current && inputRef.current.focus();
  };

  return [inputRef, focusInput];
}

//실제 사용 예시
function InputComponent({ inputRef }) {
  return <input ref={inputRef} />;
}

function ButtonComponent({ onClick }) {
  return <button onClick={onClick}>Focus on Input</button>;
}

function ParentComponent() {
  const [inputRef, focusInput] = useInputFocus();

  return (
    <>
      <InputComponent inputRef={inputRef} />
      <ButtonComponent onClick={focusInput} />
    </>
  );
}

이와 같이 useRef, useEffect 등 여러 훅을 사용하는 로직을 분리하는데 사용합니다. 앞으로 useInputFocus를 통해 간편하게 인풋과 포커스를 한줄에 처리할 수 있게 되었습니다.

예시 2) 실제 적용한 useStickyTab 함수

먼저 훅으로 분리되지 않은 함수의 예시를 보여드리겠습니다. 다음과 같이 스크롤 시 특정 높이에 붙을 경우 자동으로 탭의 형식을 변경해주는 방식입니다.

(프로젝트의 특성상 CSS의 제작을 퍼블리셔에게 위임했는데 overflow등의 속성값의 남발로 인해 CSS의 Sticky의 옵션이 적용되지 않아 직접 작성해야 되는 상황 이였습니다.)

// 작성되었던 코드
const LifestyleTabSwiper = () => {

// 하단부터 탭이 LIFESTYLE_SCROLL_HOLD 와 겹치는 부분을 IntersectionObserver로 감지하여 클래스를 변경함
  const LIFESTYLE_SCROLL_HOLD = 50;
  const [isTabSticky, setIsTabSticky] = useState(false);
  const tabRef = useRef(null);

  useEffect(() => {
    if (!tabRef?.current) return;
    const tabElement = tabRef.current;

    const observerCallback = (entries) => {
      entries.forEach((entry) => {
        const shouldStick = !entry.isIntersecting;

        if (shouldStick !== isTabSticky) {
          tabElement.classList.toggle("is--scroll", shouldStick);
          setIsTabSticky(shouldStick);
        }
      });
    };

    const observerOptions = {
      root: null,
      rootMargin: `-${LIFESTYLE_SCROLL_HOLD}px 0px 0px 0px`,
      threshold: 1,
    };

    const observer = new IntersectionObserver(
      observerCallback,
      observerOptions
    );
    observer.observe(tabElement);

    return () => observer.unobserve(tabElement);
  }, [isTabSticky, tabRef]);

  return (
    <article className="life--styling--tabs">
      <section>
        <div className="tab--wrapper type--2">
          <div className="tab--header--wrapper" ref={tabRef}>
            <LifestyleTabList
              data={data}
              currentTab={currentTab}
              goToTab={goToTab}
            />
          </div>
          ... 이하 줄임

작성하고 정상 기능하지만 혹여나 다른 탭에도 같은 기능이 필요하면 긴 코드를 다시 써야하고 너무 길어져 흐름을 읽기 어려워졌습니다. 이제 이 sticky 기능을 훅으로 분리해보겠습니다.

// 분리된 커스텀 훅
export function useStickyTab(LIFESTYLE_SCROLL_HOLD) {
  const [isTabSticky, setIsTabSticky] = useState(false);
  const tabRef = useRef(null);

  useEffect(() => {
    if (!tabRef?.current) return;
    const tabElement = tabRef.current;

    const observerCallback = (entries) => {
      entries.forEach((entry) => {
        const shouldStick = !entry.isIntersecting;

        if (shouldStick !== isTabSticky) {
          tabElement.classList.toggle("is--scroll", shouldStick);
          setIsTabSticky(shouldStick);
        }
      });
    };

    const observerOptions = {
      root: null,
      rootMargin: `-${LIFESTYLE_SCROLL_HOLD}px 0px 0px 0px`,
      threshold: 1,
    };

    const observer = new IntersectionObserver(
      observerCallback,
      observerOptions
    );
    observer.observe(tabElement);

    return () => observer.unobserve(tabElement);
  }, [isTabSticky, tabRef]);

  return tabRef;
}
// 기존 코드에서 사용하기
const LifestyleTabSwiper = () => {
  const tabRef = useStickyTab(50);

  return (
    <article className="life--styling--tabs">
      <section>
        <div className="tab--wrapper type--2">
          <div className="tab--header--wrapper" ref={tabRef}>
            <LifestyleTabList
              data={data}
              currentTab={currentTab}
              goToTab={goToTab}
            />
          </div>
          ...이하 줄임

이제 어느 함수에서도 고정을 원하는 tabRef가 생기면
const tabRef = useStickyTab(높이);

를 통해 편리하게 사용할 수 있게 되었습니다.

사용 시 주의해야 할 점

일반적인 훅의 사용시 주의해야 될 점 및 커스텀 훅을 사용시 주의사항을 정리해 보았습니다.

  1. 함수 컴포넌트의 최상위에서만 훅 사용: 훅은 함수 컴포넌트의 최상위 레벨에서만 호출해야 합니다. 반복문, 조건문, 중첩된 함수 내부에서 훅을 호출하면 안 됩니다. 이 규칙은 훅의 호출 순서가 보장되도록 합니다.
  2. 리액트 함수 컴포넌트 내에서만 훅 사용: 훅은 리액트 함수 컴포넌트나 커스텀 훅 내에서만 호출해야 합니다. 일반 JavaScript 함수에서는 훅을 사용할 수 없습니다.
  3. 의존성 배열 관리에 주의: useEffect, useMemo, useCallback 등의 훅에서는 의존성 배열(dependency array)을 정확하게 관리해야 합니다. 의존성 배열에 포함된 값이 변경될 때만 훅이 실행됩니다. 배열을 잘못 관리하면 메모리 누수나 불필요한 렌더링이 발생할 수 있습니다.
  4. 훅의 규칙 준수: 리액트 팀은 훅을 올바르게 사용하기 위해 두 가지 주요 규칙을 제시했습니다: "훅은 항상 최상위에서만 호출해야 한다"와 "훅은 리액트 함수 컴포넌트나 커스텀 훅 내에서만 호출해야 한다".
  5. 상태 업데이트에 주의: 상태 업데이트 함수는 비동기적으로 작동합니다. 따라서, 최신 상태 값을 필요로 하는 경우에는 주의가 필요합니다. 이럴 때는 함수형 업데이트를 사용할 수 있습니다.
  6. 메모리 누수 방지: 사이드 이펙트를 관리하는 useEffect 훅에서는 컴포넌트가 언마운트될 때 정리(clean-up) 작업을 해야 합니다. 예를 들어, 구독(subscriptions), 타이머, 진행 중인 요청 등을 정리해야 메모리 누수를 방지할 수 있습니다.
  7. 사용자 정의 훅의 네이밍: 커스텀 훅을 만들 때는 use로 시작하는 이름을 사용하는 것이 좋습니다. 이는 훅의 사용법을 명확하게 하고, 리액트의 린트 규칙과도 일치합니다.