김종이
Study

[모던 리액트 Deep Dive] 03. 리액트 훅 깊게 살펴보기

useState

리액트의 렌더링은 함수형 컴포넌트가 반환한 return의 값을 비교해 실행된다. 매번 렌더링이 발생 될 때마다 함수는 다시 새롭게 실행되고, 새롭게 실행되는 함수에서 변수는 초기화 되기 때문에 내부적으로 클로저를 사용해 지역변수 값을 저장하는 useState를 사용한다.


  • 아무런 값을 넘겨주지 않으면 초기값은 undefined
  • useState 구현체의 내부에서 index를 클로저로 가둬놔서 이후에도 동일한 Index에 접근해 state 값을 가져올 수 있도록 한다.
  • 실제 리액트 코드에는 useState는 useReducer로 구현되어 있다.
  • useSate의 게으른 초기화
    • useState의 인수로 특정한 값을 넘기는 함수를 인수로 넣을 수 있다.
    • 게으른 초기화는 초깃값이 복잡하거나 무거운 연산을 포함할 때 효과적
    • 리렌더링이 발생할 때 이 함수의 실행이 무시되기 때문이다.
    • useState의 내부에 클로저가 존재하며, 초깃값은 최초에만 사용된다.
    • 게으른 초기화를 이용하면 리렌더링이 발생할 때마다 많은 비용을 요구하는 작업을 실행시키지 않는다.

useEffect

useEffect는 컴포넌트의 여러값을 활용해 동기적으로 부수 효과를 만드는 매커니즘 렌더링 시 의존성에 있는 값들을 확인하며 이 의존성의 값이 이전과 다른 것이 하나라도 있다면 부수 효과를 실행하는 함수 useEffect는 state와 props의 변화 속에서 일어나는 렌더링 과정에서 실행되는 부수 효과 함수


  • 클린업 함수의 목적

    • 일반적으로 이벤트를 등록하고 지울 때 사용해야 된다고 알려져 있다.
    • 클린업 함수는 렌더링 뒤에 실행되지만, 렌더링이 실행되고 변경된 값을 읽는 것이 아니라 함수가 정의됐을 당시 선언됐던 이전 값을 바라보고 실행된다.
    • useEffect는 콜백이 실행될 때마다 이전의 클린업 함수가 존재하면 그 클린업 함수를 실행하고 콜백을 실행한다.
    • 이전에 등록했던 이벤트 핸들러를 삭제하는 코드를 클린업 함수에 추가해서 => 특정 이벤트 핸들러가 무한히 추가 되는 것을 방지
    • 클린업 함수는 언마운트 개념이 아닌, 이전 상태를 청소해 주는 개념
  • 직접 실행과 차이점

function Component() {
  console.log("직접 실행");
}
 
function Component() {
  useEffect(() => {
    console.log("useEffect 실행");
  }, []);
}
  • 직접 실행은 컴포넌트가 렌더링 되는 도중에 실행된다. 함수형 컴포넌트의 반환을 지연시키는 행위이기 때문에 무거운 작업일 경우 렌더링을 방해하므로 성능에 악영향을 미친다.

  • useEffect는 컴포넌트의 렌더링이 완료된 이후에 실행된다.

  • 의존성배열

    • useEffect의 의존성배열 비교는 이전 값과 현재 값의 얕은 비교
  • useEffect 사용 시 주의점

    • useEffect는 의존성 배열로 전달한 값의 변경에 의해 실행돼야 하는 훅
    • []과 같은 빈 비열을 의존성으로 할 때, 가급적이면 사용하면 안된다.
      • useEffect의 부수 효과가 실제로 관찰해서 실행돼야 하는 값과는 별개로 작동한다는 것을 의미
      • 컴포넌트의 state나 props와 같은 값의 변경과 useEffect의 부수 효과가 별개로 작동한다는 뜻이기 때문이다.
      • useEffect의 콜백 함수의 실행과 내부에서 사용한 값의 실제 변경 사이에 연결고리가 끊어져 있다는 뜻
 
// ❌ 버그의 위험성을 안고 있는 코드
// log가 아무리 변하더라도 useEffect의 부수 효과가 일어나지 않는다.
// useEffect의 흐름과 컴포넌트의 props.log의 흐름이 맞지 않는다.
 
// ⭕ 오히려 부모 컴포넌트에서 logging 작업을 하고
// 부모 컴포넌트에서 Component가 렌더링 되는 시점을 결정해
// 의존성 배열에 log 값을 넘겨는 것이 useEffect의 주석을 제거해도 동일한 결과를 만들 수 있다.
function Component({ log }: { log: string }) {
useEffect(() => {
	logging(log)
}, []) // es1int-disab1e-line react-hooks/exhaustive-deps
 
 

useMemo, useCallback


  • useMemo

    • 값의 메모이제이션
    • useMemo로 컴포넌트도 감쌀 수 있지만 React.memo를 쓰는 것을 추천
  • useCallback

    • 함수의 메모이제이션
    • useCallback은 useMemo를 사용해 구현할 수 있다.
export function useCallback(callback, args) {
  currentHook = 8;
  return useMemo(() => callback, args);
}

useRef


  • useState와 공통점
    • 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장
  • useState와 차이점
    • useRef는 반환값인 객체 내부에 있는 current에 접근, 변경할 수 있다.
    • useRef는 그 값이 변하더라도 렌더링을 발생시키지 않는다.
  • useRef의 최초 기본 값은 return문에 정의해 둔 DOM이 아닌 useRef()로 넘겨받은 인수
  • useRef의 실제 값은 {current: value}와 같은 객체 형태
// 렌더링에 영향을 미치면 안되기에 의존성 배열에 [] 선언
// => 각 렌더링마다 동일한 객체를 가리키는 결과
export function useRef(initialValue) {
  currentHook = 5;
  return useMemo(() => ({ current: initialValue }), []);
}

useContext

useContext는 상태 관리를 위한 API가 아닌, 상태를 주입해주는 API 부모 컴포넌트가 렌더링되면 하위 컴포넌트가 모두 리렌더링되기 때문에 context의 provider 하위 컴포넌트의 렌더링을 방지하기 위해서 React.memo를 써야한다.


  • 상태관리 API란?

    • 어떠한 상태를 기반으로 다른 상태를 만들어 낼 수 있어야 한다.
    • 필요에 따라 상태를 최적화할 수 있어야 한다.
  • 반면 useContext는?

    • 단순히 props 값을 하위로 전달하는 개념
    • 사용한다고 해서 렌더링 최적화가 되지 않는다.

useReducer

useReducer는 useState의 심화 버전으로, 복잡한 형태의 state를 사전 정의된 dispatcher로만 수정할 수 있게하여 state에 대한 접근을 제한한다.


import React, { useReducer } from "react";
 
const initialState = { count: 0 };
 
function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}
 
function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </div>
  );
}
 
export default Counter;

useLayoutEffect

리액트는 useLayoutEffect의 실행이 종료될 때까지 기다린 다음에 화면을 그린다. 리액트 컴포넌트는 useLayoutEffect가 완료될 때까지 기다리기 때문에 컴포넌트가 잠시 동안 일시 중지되는 것과 같은 일이 발생하게 된다.

  1. 리액트가 DOM을 업데이트
  2. useLayoutEffect 실행
  3. 브라우저에 변경 사항을 반영
  4. useEffect를 실행

  • DOM은 계산됐지만 화면에 반영되기 전에 진행하고 싶은 작업이 있을 때 사용
    • DOM 요소를 기반으로 하는 애니메이션
    • 스크롤 위치를 제어

훅 사용 규칙

컴포넌트 최상위에서만 훅을 호출해야한다. 반복문, 조건문, 중첩된 함수 내에서 훅을 실행할 수 없다. 이는 컴포넌트가 렌더링 될 시에 항상 동일한 순서로 훅이 호출되는 것을 보장한다.


  • 리액트 훅은 파이버 객체의 링크드 리스트의 호출 순서에 따라 저장
  • 고정된 순서에 의존에 훅과 관련된 정보가 저장된다.
  • 이에 이전 값에 대한 비교와 실행이 가능
  • 만약 조건문이 필요하다면 반드시 훅 내부에서 수행해야한다.

사용자 정의 훅 vs 고차 컴포넌트

공통된 코드를 하나로 만들어 재사용할 수 있는 로직을 관리할 수 있는 두가지 방법


  • 사용자 정의 훅

    • 서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용되는 것
  • 고차 컴포넌트

    • 리액트에서 가장 유명한 고차 컴포넌트는 React.memo
    • with로 시작하는 이름을 사용하는 것이 관습
  • 사용자 정의 훅이 필요한 경우

    • 단순히 useEffect, useState와 같이 리액트에서 제공하는 훅으로만 공통 로직을 격리할 수 있는 경우
  • 고차 컴포넌트가 필요한 경우

    • 함수형 컴포넌트가 반환하는 렌더링 결과물에 영향을 미쳐야 하는 공통 로직