본문 바로가기
Front-End Study/모던 리액트 딥다이브 스터디

모던 리액트 딥다이브 - 7회차 [3-1, 3-2, 3-3]

by 코딩기 2024. 6. 19.
728x90

3. 리액트 훅 깊게 살펴보기

함수 컴포넌트가 상태 사용 및 생명주기 메서드 대체를 위해 사용하는 것이 바로 훅(hook) 이다.


3.1 리액트의 모든 훅 파헤치기

✨클래스 컴포넌트만 영유하던 리액트의 핵심적인 기능들을 함수 컴포넌트가 사용할 수 있게 하여 함수의 시대를 열어준 귀중한 친구.


3.1.1 useState


🔖 함수 컴포넌트 내부에서 상태를 정의 및 관리할 수 있게 해주는 훅

// 기본적인 형태
const [num, setNum] = useState(0);

setNum(1);
  • 구조분해할당을 통해 가져오며, 첫번째 원소는 state 값 자체, 두번째 요소는 state를 변경하는데 사용하는 setter 함수를 가진다.

❗만약 useState 의 형태를 사용하지 않는다면?

  • 기본적으로 리액트는 "상태 변화"를 감지하고, 이에 따라 리렌더링을 발생시키며, "클로저"를 통해 state 값을 계속 기억하고 리렌더링 시 이를 사용한다.

  • 따라서 우리가 사용하는 useState 의 올바른 효과를 기대하기 위해서 알맞게 이를 사용해야 한다.

☝️state는 상수가 아닌 함수다.

// useState 훅 구현 예시
const react = () => {
  let state = [];
  let setters = [];
  let cursor = 0;

  const createSetter = (cursor) => {
    return (newValue) => {
      state[cursor] = newValue;
    };
  };

  const useState = (initialValue) => {
    if (state[cursor] === undefined) {
      state.push(initialValue);
      setters.push(createSetter(cursor));
    }

    const resState = state[cursor];
    const resSetter = setters[cursor];
    cursor++;

    return [resState, resSetter];
  };

  return useState;
};

// 새로운 useState 훅과 렌더 함수 생성
import { useState } from "react";

// 예제 컴포넌트
const MyComponent = () => {
  const [count, setCount] = useState(0);
  // 상태 업데이트를 예제에서 직접 호출
  const handleClick = () => {
    setCount((prevCount) => prevCount + 1);
  };

  return (
    <>
      <div>{count}</div>
      <button onClick={handleClick}>클릭!</button>
    </>
  );
};
  • state 는 값이므로 변수로 선언

  • state 가 함수 안에 선언될 경우 변수기 때문에 호출 시마다 초기화 되므로, 클로저를 이용해 기억

  • state 를 여러 컴포넌트에서 사용해야하기 때문에, 배열 형태로 만들어 사용

  • 첫 사용 시 state 를 설정하고 setter 함수 작성 (createSetter)

  • 다음 사용부터 setter 함수는 기존에 작성된 함수로 계속 사용(비용 감소)


🏷️ 게으른 초기화

  • useState 에 변수 대신 함수를 넘기는 것

    const [count, setCount] = useState(Number.parseInt(window.localStorage.getItem(cachekey)));
  • 게으른 초기화를 사용할 경우, 함수는 state 가 처음 만들어질 때만 실행되므로, 무거운 연산이나 복잡한 함수의 재실행을 방지하여 성능을 향상시킬 수 있다.

  • Storage 에 대한 접근 이나 고차 함수의 배열에 접근하는 경우 등, 비용이 큰 경우 한번씩 사용해주자

  • 링크 : useState의 동작원리


3.1.2 useEffect


🔖 앱 내 컴포넌트의 여러 값들을 활용해 동기적으로 부수 효과를 만들기 위한 훅

☝️ 부수 효과가 '언제' 보다 '어떤' 상태값과 함께 실행되는지가 중요

// 기본적인 형태
useEffect(() => {
  // 실행할 효과
}, []);
  • 첫번째로 실행할 부수효과를 포함한 함수를, 두번째로 의존성 배열을 전달

  • ❗의존성 배열은 값이 없을 수도, 있을 수도 있다.

  • useEffect 는 렌더링 시마다 함수를 실행하는 리액트의 특성을 활용하여, 의존성에 있는 값의 변화를 통해 부수 효과를 가진 함수를 실행한다.


🏷️ 클린업 함수의 목적

  • 기본적으로 useEffect 내에서 이벤트를 등록하고 지울 때 사용

    const ExampleComponent = () => {
      const [count, setCount] = useState(0);
    
      const handleClick = () => {
        setCounter((prev) => prev + 1);
      };
    
      useEffect(() => {
        // 사이드 이펙트: 이벤트 리스너 추가
        const call = () => {
          console.log("Window resized");
        };
    
        window.addEventListener("click", call);
    
        // 클린업 함수: 이벤트 리스너 제거
        return () => {
          window.removeEventListener("click", call);
        };
      }, []); // 빈 배열은 이 이펙트가 처음 렌더링될 때 한 번만 실행됨
    
      return (
        <div>
          <p>Count: {count}</p>
          <button onClick={handleClick}>클릭</button>
        </div>
      );
    };
    • useEffect 는 컴포넌트가 DOM에서 사라짐을 의미하는 언마운트와는 개념이 조금 다르다.

    • 콜백이 실행 시, 이전의 클린업 함수가 존재한다면 클린업을 실행한 후 콜백을 실행한다.

    • 이를 활용해 무한히 DOM 이벤트 핸들러가 추가되는 것을 방지한다.


🏷️ 의존성 배열

  • 빈 배열 혹은 사용자가 원하는 값을 넣은 배열을 사용

  • 빈 배열일 시 최초 렌더링 후 실행 ❌

  • 의존성 배열 자체가 없다면 렌더링 시마다 실행이 필요하다고 판단, 렌더링 발생 시마다 실행

    function Component() {
      // 1
      console.log("실행");
    
      // 2
      useEffect(() => {
        console.log("실행");
      });
    }
  • 둘은 차이가 없다고 생각할 수 있으나, useEffect 는 컴포넌트가 렌더링 완료된 후 실행되므로, 1번같은 경우 컴포넌트 렌더링에 악영향을 끼침 (의미없는 코드)

let currentEffectIndex = 0;
const effects = [];

const runEffects = () => {
  effects.forEach((effect, index) => {
    if (effect.cleanup) {
      effect.cleanup(); // 이전 클린업 함수 실행
    }
    const cleanup = effect.effect(); // 새로운 이펙트 실행
    effects[index].cleanup = cleanup; // 새로운 클린업 함수 저장
  });
  currentEffectIndex = 0;
};

const useEffect = (effect, dependencies) => {
  const hasNoDeps = !dependencies;
  const deps = effects[currentEffectIndex]?.dependencies;
  const hasChangedDeps = deps ? !dependencies.every((dep, i) => dep === deps[i]) : true;

  if (hasNoDeps || hasChangedDeps) {
    effects[currentEffectIndex] = { effect, dependencies, cleanup: null };
  }
  currentEffectIndex++;
};
  • 의존성 배열의 현재와 이전 값을 '얕은 비교'하여 callback 으로 선언한 부수 효과 실행

🏷️ 주의할 점

  • eslint-disable-line react-hooks/exhaustive-deps 주석은 놉!

    • useEffect 인수 내부에 의존성 배열에 포함되지 않은 값을 사용하는 경우 발생

    • 빈 배열이든 아니든, useEffect 를 활용한 부수 효과를 정말로 사용하는 게 맞는지 고민하고 사용해야 한다.

  • 첫번째 인수인 함수에 이름 지어주기~

    • useEffect 의 목적을 분명히 하고 책임을 최소한으로 좁히기 위해 함수명을 부여하는 것이 좋다.
  • 돼지같은 useEffect 만들지 않기!!

    • 부수 효과가 커질 수록, 의존성 배열의 요소가 많을 수록 useEffect에 의해 실행되는 부수 효과의 관리가 힘들어지고 JS 실행 성능에 악영향을 미친다.
  • 불필요한 외부 함수는 안돼요~

    • useEffect 내에서 사용하는 부수 효과라면 그냥 안에서 직접 작성하고 사용하는 것이 바람직하다.
  • 링크 : useEffect의 경쟁상태


3.1.3 useMemo


🔖 비용이 큰 연산 결과를 메모이제이션 하고 , 저장된 값을 가져와 반환하는 훅

import React, { useState, useMemo } from "react";

const ExampleComponent = () => {
  const [count, setCount] = useState(0);
  const [numbers, setNumbers] = useState([10, 20, 30, 40, 50]);

  // useMemo를 사용하여 계산 비용이 큰 연산을 최적화
  const sortedNumbers = useMemo(() => {
    console.log("Sorting numbers...");
    return numbers.sort((a, b) => a - b);
  }, [numbers]); // numbers가 변경되지 않았다면 저장된 값 사용

  const incrementCount = () => {
    setCount(count + 1);
  };

  const addNumber = () => {
    setNumbers([...numbers, Math.floor(Math.random() * 100)]);
  };

  return (
    <div>
      <h1>useMemo Example</h1>
      <p>Count: {count}</p>
      <button onClick={incrementCount}>Increment Count</button>
      <button onClick={addNumber}>Add Number</button>
      <ul>
        {sortedNumbers.map((number, index) => (
          <li key={index}>{number}</li>
        ))}
      </ul>
    </div>
  );
};

export default ExampleComponent;
  • 컴포넌트도 감쌀 수 있지만, 그냥 React.memo 를 사용하자

3.1.4 useCallback


🔖 특정 함수를 다시 만들지 않고 재사용하는 훅

import React, { useState, useCallback } from "react";

const ChildComponent = React.memo(({ onClick }) => {
  console.log("ChildComponent render");
  return <button onClick={onClick}>Increment Parent Count</button>;
});

const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // useCallback을 사용하여 콜백 함수 메모이제이션
  const incrementCount = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []); // 의존성 배열이 비었으므로 첫 렌더링 시 함수 참조 후 리렌더링 시에도 동일 함수 참조

  const handleTextChange = (e) => {
    setText(e.target.value);
  };

  return (
    <div>
      <h1>useCallback Example</h1>
      <p>Count: {count}</p>
      <input type="text" value={text} onChange={handleTextChange} />
      <ChildComponent onClick={incrementCount} />
    </div>
  );
};

export default ParentComponent;
  • React.memo 를 사용하여 컴포넌트를 메모이제이션 했지만, ChildComponent 에 내려준 onClick이 실행될때마다 increment 함수가 재생성되어 의미가 없다.

  • useCallback 을 사용해 함수를 메모이제이션하면, 함수가 재생성되지 않아 리렌더링을 방지한다.

  • 기본적으로 useCallbackuseMemo 를 통해서도 구현 가능하다.


useMemouseCallback 의 차이는 메모이제이션의 목표가 '변수'냐, '함수'냐의 차이일 뿐이다.


3.1.5 useRef


🔖useState 와 동일하나 변경되도 리렌더링되지 않으며, 객체의 current 값으로 내부에 접근 가능한 훅

  • 고정된 값의 컨트롤은 함수 외부에 선언 및 관리도 동일하나, 불필요한 메모리 소모와 컴포넌트가 여러개일 시, 동일한 값을 가리키게 되어버리는 단점이 있다.

  • useRef 는 이러한 단점을 '리액트적 관점'에서 완벽하게 커버한다.


🏷️useRef 의 일반적인 사용 예시

  • DOM에 접근하는 경우

    function Component() {
      const inputRef = useRef(null);
    
      useEffect(() => {
        console.log(inputRef.current);
      }, []);
    
      return <input ref={inputRef} type="text'/>
    }
    • 일반적으로 return 문을 통해 실행된 DOM 트리에 접근하므로, 렌더링된 후에 적용된다.

    • 값을 변경해도 렌더링 시키지 않기 위해, 오히려 객체의 값 자체를 변경시켜 렌더링을 방지하는 형식으로 구현된다.


3.1.6 useContext


🔖 props drilling 을 방지하기 위해 사용하는 Context API 를 사용하기 위한 훅

import React, { createContext, useContext, useState } from "react";

// 1. Context 생성
const ThemeContext = createContext();

const ThemeProvider = ({ children }) => {
  // 2. 제공할 상태 정의
  const [theme, setTheme] = useState("light");

  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
  };

  return (
    // 3. Context Provider로 상태와 함수를 제공
    <ThemeContext.Provider value={{ theme, toggleTheme }}>{children}</ThemeContext.Provider>
  );
};

const ThemedComponent = () => {
  // 4. useContext를 사용하여 Context 값 사용
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div
      style={{
        background: theme === "light" ? "#fff" : "#333",
        color: theme === "light" ? "#000" : "#fff",
        padding: "20px",
      }}
    >
      <p>Current Theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
};

const App = () => {
  return (
    <ThemeProvider>
      <ThemedComponent />
    </ThemeProvider>
  );
};

export default App;
  • createContextContext 생성

  • 상태와 setter 를 정의하고 Provider 를 통해 제공

  • Context.provider 를 이용해 제공할 함수를 감싼 후, 해당 함수에서 useContext 를 통해 선언 및 사용


❗다수의 provideruseContext 사용 시, 별도의 함수로 감싸 사용하는 것이 타입 추론과 에러 방지에 용이하다.

🏷️Context 사용 시 주의점

  • Context 를 사용한다면 provider와의 의존성이 어떻게든 생기게 되므로, 재사용성이 감소하고 provider 하위에 존재하지 않을 경우 에러가 발생할 수 있다.

  • Context가 많아질 수록 루트 컴포넌트에 Context 를 넣는 것은 좋지 않다. (리소스 낭비)

  • Context의 사용으로 최적화에 이슈가 발생할 수 있으므로, Context 를 사용한 컴포넌트 렌더링의 전체 과정을 항상 유념하자.


☝️따로 찾아본 Context.provider로 모달창 만들기

const ModalProvider = ({ children }: { children: ReactNode }) => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [modalContents, setModalContents] = useState(<></>);

  const openModal = (children: ReactNode) => {
    setIsModalOpen(true);
    setModalContents(children);
  };

  const closeModal = () => setIsModalOpen(false);

  const onDimmerClick = (event: MouseEvent) => {
    if (event.currentTarget !== event.target) return;

    closeModal();
  };

  return (
    <ModalContext.Provider value={{ isModalOpen, openModal, closeModal }}>
      {children}

      {isModalOpen && <Dimmed onClick={onDimmerClick}>{modalContents}</Dimmed>}
    </ModalContext.Provider>
  );
};

3.1.7 useReducer


🔖useState와 기본적으로 비슷하나 좀 더 복잡한 상태값을 정의한 시나리오대로 사용 가능한 훅

  • 반환값은 useState와 동일한 두가지 요소를 가진 배열이다.

    • statedispatcher 를 가진다.
  • useState 와 달리 2 ~ 3개의 인수를 필요로 한다.

    • 액션을 정의하는 reducer, 초기값인 initialState, 초기값을 지연생성 시키는 init (필수 ❌)
// 1. 리듀서 함수 정의
const initialState = { count: 0 };

const reducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
};

const Counter = () => {
  // 2. useReducer 훅 사용
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <h1>Count: {state.count}</h1>
      <button onClick={() => dispatch({ type: "increment" })}>Increment</button>
      <button onClick={() => dispatch({ type: "decrement" })}>Decrement</button>
    </div>
  );
};
  • 궁극적인 목적은 복잡한 형태의 state를 사전에 정의한 dispatch 함수로만 수정할 수 있게 하여 안전한 접근과 업데이팅을 구현하는 것

  • reducer 함수를 정의하고 인수로 stateaction을 받아 조건식의 형태로 각각의 action 에 해당하는 로직을 구현한다.

  • 세번째 인수인 init (게으른 초기화)는 굳이 사용하지 않아도 무방, 대신 useState와 동일한 효과 및 초기화에 사용할 수 있는 장점이 있다.


🏷️useReduceruseState 의 차이

import React, { useState } from "react";

// useState를 사용한 카운터 훅
const useCounter = (initialValue = 0) => {
  const [count, setCount] = useState(initialValue);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(initialValue);

  return { count, increment, decrement, reset };
};

const CounterWithState = () => {
  const { count, increment, decrement, reset } = useCounter(0);

  return (
    <div>
      <h1>useState Counter: {count}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
};

// 리듀서 함수 정의
const initialState = { count: 0 };

const reducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    case "reset":
      return { count: action.payload };
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
};

// useReducer를 사용한 카운터 훅
const useCounterReducer = (initialValue = 0) => {
  const [state, dispatch] = useReducer(reducer, { count: initialValue });

  const increment = () => dispatch({ type: "increment" });
  const decrement = () => dispatch({ type: "decrement" });
  const reset = () => dispatch({ type: "reset", payload: initialValue });

  return { count: state.count, increment, decrement, reset };
};

const CounterWithReducer = () => {
  const { count, increment, decrement, reset } = useCounterReducer(0);

  return (
    <div>
      <h1>useReducer Counter: {count}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
};

export default CounterWithState;
  • 결국 동일한 기능을 제공하나, 상태 관리 로직의 복잡도에 따라 취사 선택하는 것이 바람직하다.

3.1.8 useImperativeHandle


🔖실제 개발 과정에선 잘 사용되지 않으나 간혹 쓸모 있는 훅

  • 이를 위해 먼저 React.forwardRef 에 대해 알 필요가 있다.

    // ref를 props로 넘겨주는 경우
    function Child({ ref }) {
      useEffect(() => {
        console.log(ref);
      }, []);
    }
    
    function Parent() {
      const inputRef = useRef(null);
    
      return (
        <>
          <input ref={inputRef} />
          <Child ref={inputRef} />
        </>
      );
    }
  • 위와 같이 사용할 경우, refprops 로 내려줄 수 없다는 경고가 출력된다.

  • 이를 위해 ref 값이 아닌 다른 값으로 inputRef 를 내려줄 수 있다.

    // ref를 props로 넘겨주는 경우(props의 이름 변경)
    function Child({ parentRef }) {
      useEffect(() => {
        console.log(ref);
      }, []);
    }
    
    function Parent() {
      const inputRef = useRef(null);
    
      return (
        <>
          <input ref={inputRef} />
          <Child parentRef={inputRef} />
        </>
      );
    }
  • forwardRef 는 이러한 ref 를 내려줄 때 props 이름의 일관성을 유지하기 위해 사용한다.

    // ref를 props로 넘겨주는 경우(forwardRef 사용)
    const Childe = forward(() => {
      useEffect(() => {
        console.log(ref);
      }, []);
    });
    
    function Parent() {
      const inputRef = useRef(null);
    
      return (
        <>
          <input ref={inputRef} />
          <Child ref={inputRef} />
        </>
      );
    }

🏷️useImperativeHandle 이란?

  • 부모에게서 넘겨받은 ref를 원하는대로 수정할 수 있는 훅이다.

    // 자식
    const ChildComponent = forwardRef((props, ref) => {
      const inputRef = useRef();
    
      useImperativeHandle(ref, () => ({
        focus: () => {
          inputRef.current.focus();
        },
        clear: () => {
          inputRef.current.value = "";
        },
      }));
    
      return <input ref={inputRef} type="text" />;
    });
    
    // 부모
    const ParentComponent = () => {
      const childRef = useRef();
    
      return (
        <div>
          <h1>useImperativeHandle Example</h1>
          <ChildComponent ref={childRef} />
          <button onClick={() => childRef.current.focus()}>Focus Input</button>
          <button onClick={() => childRef.current.clear()}>Clear Input</button>
        </div>
      );
    };
  • 위 예제의 경우 focusclear 를 정의하여 부모에서 current 값 안의 키를 불러와 사용했다.


3.1.9 useLayoutEffect


🔖기본적으로 useEffect 의 동작과 동일하지만, 모든 DOM의 변경 후에 동기적으로 발생하는 훅 (DOM의 변경이지 변경사항 반영이 아니다.)

< useLayoutEffect의 실행 순서 >

  1. 리액트가 DOM을 업데이트

  2. useLayoutEffect 실행

  3. 브라우저 변경사항 반영

  4. useEffect 실행


🏷️ 사용하는 경우

  • DOM이 계산됐으나 화면에 반영되기 전에 하고 싶은 작업이 있을 경우 사용

3.1.10 useDebugValue


🔖일반적으로 dev 모드에서 사용하며, 디버깅 정보를 보여주는 훅

  • 사용자 정의 훅 내부 내용에 관한 정보를 남길 수 있다.

  • 오직 다른 훅 내부에서만 실행 가능하다.

  • 디버깅 관련 정보 제공 시 유용


3.1.11 훅의 규칙


🔖리액트의 훅은 사용 시 몇 가지 규칙이 존재한다..(신경 쓸게 너무 많다잉)

  • 최상위에서만 훅을 호출해야 하며, 반복, 조건문, 중첩된 함수 내에서 훅을 사용하지 못한다.

  • 훅을 호출 가능한건 리액트 컴포넌트 및 사용자 정의 훅 뿐이다.

🏷️파이버의 관점

  • 리액트 훅은 기본적으로 파이버의 링크드 리스트의 호출 순서에 따라 저장

  • 각 훅은 순서에 의존하여 결과값을 저장

  • 순서를 보장받지 못하는 상황(조건 및 반복문, 중첩 함수 등)에서 에러 발생


3.1.12 책 정리 + 주관적인 정리


🔖책 정리

  • 함수 컴포넌트 내에서 훅을 사용하면서 동작에 대해서 고민하거나, 클래스보다 쉽다고 했는데 내용이 많아 땀을 삐질삐질 흘렸을 것이다.

  • 훅을 정확히 이해하고 사용한다면 성능 좋은 리액트 앱을 만드는데 큰 도움이 될 것이다.

🏷️주관적인 정리

  • 너무 많아서 징그럽다... 가 아니라 사용하던 훅이 너무 한정적이어서 놀랐고 이번에 공부한 것을 바탕으로 다양하게 사용해야겠다고 생각했다.

  • 생각보다 여러가지 상황에 맞는 다양한 훅이 있어서 스프린트나 프로젝트를 진행하면서 맞닥뜨렸던 상황들이 생각났고 그때 알고 있었으면 야무지게 사용했을 듯 하다.


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

✨사용자 정의 훅과 고차 컴포넌트를 활용하여 재사용 로직을 관리해보자!


3.2.1 사용자 정의 훅


🔖서로 다른 컴포넌트가 같은 로직을 공유하고자 할 때 사용하는 훅

  • 기존 리액트의 훅을 활용해 개발자가 직접 훅을 정의하는 방식이다.

  • 훅의 이름을 무조건 use 로 시작해야 하며, 이를 바탕으로 사용자 정의 훅이라는 것을 인식한다.

    import { useEffect, useState } from "react";
    
    export default function useIntersectionObserver(ref) {
      const [isIntersecting, setIsIntersecting] = useState(false);
    
      useEffect(() => {
        const observer = new IntersectionObserver(
          (entries) => {
            entries.forEach((entry) => {
              if (entry.isIntersecting) setIsIntersecting(true);
              else setIsIntersecting(false);
            });
          },
          { threshold: 0.5 }
        );
    
        if (ref.current) observer.observe(ref.current);
    
        return () => {
          if (ref.current) observer.unobserve(ref.current);
        };
      }, [ref]);
    
      return isIntersecting;
    }
  • 위의 경우 intersectionObserver DOM API를 이용하여 만든 useIntersectionObserver 훅이다.

  • 해당하는 Element 의 ref 값을 받아와 뷰포트에서 관측되었을 경우, true or false를 반환하여 로직을 실행시킨다.


❗결국 use를 앞에 붙히는 것은 useStateuseEffect 와 같은 리액트의 훅이 바탕이므로, 이를 사용해 커스텀 훅을 만들었다는 것을 간접적으로 몀시한다.


3.2.2 고차 컴포넌트


🔖 컴포넌트 자체 로직을 재사용하기 위한 방법

  • 일종의 고차 함수로, JS의 함수는 일급 객체라는 특성을 이용한 것이다.

👍가장 유명한 것이 바로 React.memo 이다.


🏷️고차 함수 만들어보기

  • 대표적인 고차함수는 Array.prototype.map 이 있다.

  • 리액트를 예로 드는 경우, useStatesetter 또한 실행 가능한 함수를 반환하므로 고차 함수라고 할 수 있다.

🏷️고차 함수를 활용한 리액트 고차 컴포넌트 만들어보기

const checkAuth = () => {
  const isAuthenticated = Math.random() < 0.5; // 임의의 인증 여부 확인 로직
  return isAuthenticated;
};

// 고차 컴포넌트 함수 정의
const withAuth = (WrappedComponent) => {
  return (props) => {
    const [isAuthenticated, setIsAuthenticated] = useState(checkAuth());

    return isAuthenticated ? <WrappedComponent {...props} /> : <p>로그인이 필요합니다.</p>;
  };
};

// 실제 컴포넌트
const Profile = () => {
  return <div>프로필 페이지</div>;
};

// 고차 컴포넌트와 실제 컴포넌트 연결
const AuthProfile = withAuth(Profile);

// 애플리케이션 컴포넌트
const App = () => {
  return (
    <div>
      <h1>고차 컴포넌트 예시</h1>
      <AuthProfile />
    </div>
  );
};

export default App;
  • 고차 컴포넌트는 컴포넌트 전체를 감쌀 수 있어 사용자 정의 훅보다 큰 영향력을 끼친다.

  • 컴포넌트 결과물 자체에 영향을 끼치는 공통된 작업의 처리가 가능하다.


❗사용 시 주의할 점

  • with로 시작되는 이름을 사용해야한다. (일종의 관습)

  • 반드시 컴포넌트를 인수로 받기 때문에, 컴포넌트 자체의 props를 수정, 추가, 삭제 하는 등의 부수 효과를 항상 최소화해야 한다.

  • 컴포넌트를 감싸는 형태다 보니, 항상 최소화해야 결과를 예측하기 쉽다.


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


🔖리액트가 제공하는 훅으로만 로직을 처리할 수 있다면 사용자 정의 훅을 사용하는 것이 좋다. (컴포넌트 내부에 미치는 영향 최소화)

function HookComponent() {
  const { loggedIn } = useLogin();

  useEffect(() => {
    if (!loggedIn) {
      // 할 거
    }
  }, [loggedIn]);
}
  • 부수 효과가 항상 제한적이다.

  • 컴포넌트에 어떤 결과를 끼칠지 예측이 상대적으로 쉽다.

❗고차 컴포넌트를 사용해야 하는 경우

// 훅 사용 시
function HookComponent() {
  const { loggedIn } = useLogin();

  if (!loggedIn) {
    return <LoginComponent />;
  }

  return <>하이</>;
}

// 고차 컴포넌트
const Component = withLoginComponent(() => {
  return <>하이</>;
});
  • 특정 상황에서 아예 다른 일을 하는 컴포넌트를 노출시켜야 하는 경우 등

✨결론적으로 렌더링의 결과물에 직접 영향을 끼쳐야 하는 로직을 구현한다면 고차 컴포넌트를 사용해야 하지만, 무분별한 사용은 악영향을 끼치므로 사용자 정의 훅을 사용해야 한다.


3.2.4 책 정리 + 주관적인 정리


🔖 책 정리

  • 앱의 규모가 커지고, 처리할 로직이 많아지면 중복 작업에 대한 고민도 많아질 것이다.

  • 공통화할 작업과 처리할 방법을 항상 고민하며 이 두가지를 적절히 사용한다면 개발을 효율적으로 할 수 있을 것이다.

🏷️ 주관적인 정리

  • 사용자 정의 훅은 단순히 보조 용도로 구글링을 통해 로직을 검색하며 찾아보곤 했는데, 반복적인 작업을 할 때 구체적으로 생각하여 만들어 보고 싶다는 생각이 들었다.

  • 고차 컴포넌트는 이름과 로직 모두 생소해서 조금 흥미가 생겼고, 컴포넌트 자체를 감싸서 처리하는 방식 또한 조건적인 렌더링을 구현할 때 활용도가 높을 것 같다고 느꼈다.