리액트 hook 총 정리

박민우·2024년 3월 7일
2

🎉 React

목록 보기
3/4
post-thumbnail

평소에 자주 사용해오던 리액트 Hook을 전체적으로 정리해본 적은 없어서 이번 기회에 한 번 전체적으로 정리해보려고 합니다🫠


📌 리액트 훅이란 ?

React에서는 16.8 버전부터 Hook이란 개념이 새로 추가되었습니다. Hook은 함수형 컴포넌트에서 React state와 생명주기 기능을 연동할 수 있게 해주는 함수입니다.

리액트에서 제공하는 내장 훅(useState, useEffect, ... ) 과 사용자가 직접 정의할 수 있는 Custom Hooks가 있습니다.


어떤 문제를 해결할 수 있나요?

함수형 컴포넌트에 Hook이 나오기 전까지는, 일반적으로 클래스형 컴포넌트를 많이 사용했습니다. 하지만 클래스형 컴포넌트에서 상태(state)를 사용하고, 생명주기 메서드를 사용하는 방식은 많은 문제들과 불편함을 가지고 있었습니다.

  1. 컴포넌트 간의 상태 로직을 재사용하기 어렵다.

  2. 복잡한 컴포넌트들의 이해가 어렵다.

    => render props나 HOC(Higher Order Component)와 같은 패턴들

  3. Class 문법 자체(ex) Class 함수 내에서의 this)가 어렵다.


이러한 문제들을 해결할 수 있는 함수형 컴포넌트에서의 Hook이 등장했습니다!

  1. 컴포넌트 간의 계층을 바꾸지 않고 상태 로직을 재사용할 수 있습니다.
  2. 하나의 컴포넌트를 생명주기가 아닌 기능을 기반으로 하여 작은 함수 단위로 나눌 수 있습니다.
  3. Class 문법 없이도 React 기능을 사용할 수 있게끔 해준다.

훅 규칙

  1. 같은 훅을 여러 번 호출할 수 있습니다.

    function Form() {
      // useState 여러번 호출 가능
      const [name, setName] = useState('Mary');
      const [surname, setSurname] = useState('Poppins');
      
      // 이하 생략
  2. 컴포넌트 최상위(at the top level)에서만 호출할 수 있습니다. 반복문, 조건문, 중첩된 함수 내에서 훅을 사용할 수 없습니다.

    => 컴포넌트 최상위에서 훅을 호출하면, 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장됩니다.

    이는 React가 useState와 useEffect 등의 훅이 여러 번 호출되는 중에도 훅의 상태를 올바르게 유지할 수 있도록 해줍니다.

    훅의 호출 순서가 같아야 하는 이유는?

    => React가 특정 state가 어떤 useState 호출에 해당하는지 알 수 있는 이유는 React가 Hook이 호출되는 순서에 의존하기 때문입니다. 모든 렌더링에서 Hook의 호출 순서는 같기 때문에 state를 구분할 수 있습니다. 더 자세한 내용은 여기를 참고해주세요.

  3. Hook은 React 함수 내에서만 호출할 수 있습니다. 리액트 훅은 함수형 컴포넌트에서 호출해야하며, 추가적으로 custom hooks에서 또한 호출할 수 있습니다.

  4. 비동기 함수(async 키워드가 붙은 함수)는 콜백함수로 사용할 수 없습니다.

    export default function App(){
      // useEffect Hook 내부에 비동기 함수가 들어가므로 에러 발생
      useEffect(async () => {
        await Promise.resolve(1)
      }, [])
      
      return {
        <div>
          <div>Test</div>
        </div>
      }

    ❓ 그 이유는 무엇인가 ?


📌 리액트 훅의 종류

리액트는 기본적으로 컴포넌트 상태를 관리할 수 있는 useState와 컴포넌트 생애주기에 개입할 수 있는 useEffect , 컴포넌트 간의 전역 상태를 관리하는 useContext 훅을 제공하고 있습니다. 추가로 제공하는 Hook은 기본 Hook의 동작 원리를 모방해서 만들어졌습니다.

추가적인 훅들의 기본적인 역할은 다음과 같습니다.

  • useReducer : 상태 업데이트 로직을 reducer 함수에 따로 분리합니다.
  • useRef : 컴포넌트나 HTML 엘리먼트를 레퍼런스로 관리합니다.
  • useImperativeHandle : useRef의 레퍼런스를 상위 컴포넌트로 전달합니다.
  • useMemo, useCallback : 의존성 배열에 적힌 값이 변할 때만 값 또는 함수를 다시 정의합니다.
  • useLayoutEffect : 모든 DOM 변경 후 브라우저가 화면을 그리기 이전 시점에 effect를 동기적으로 실행합니다.
  • useDebugvalue : 사용자 정의(custom) Hook의 디버깅을 도와줍니다.

📌 useState

리액트 훅의 기본이자 가장 많이 사용하는 훅인 useState는, 컴포넌트의 상태(state)를 관리할 수 있는 훅입니다.

const [number, setNumber] = useState(1);

위 코드에서는 number 라는 상태를 useState 훅을 통해 관리하고 있습니다. useState 훅의 인자로 전달된 1 이라는 값이 number의 초기값이 되고, 훅을 통해 반환되는 [number, setNumber]에서 number를 통해 상태의 값에 접근할 수 있고, setNumber 메서드를 호출함으로써 상태 값을 변경할 수 있습니다.


상태값 변경시 리렌더링 발생

const [number, setNumber] = useState(1);
setNumber(2);

useState 에서 반환된 배열의 두번째 값인 setter 함수를 호출하면 상태 값을 변경할 수 있고, 상태 값이 변경되면 해당 컴포넌트는 다시 렌더링됩니다.

=> useState(초기값)에서 인자로 전달된 값을 상태의 초기값으로 사용합니다. 하지만 이후 setter 함수에 의해 상태의 값이 변경되었다면, 다음 렌더링에서는 그 상태를 유지합니다.

setState 호출 => 상태 변경 => 리렌더링(변경된 상태값 사용)

useState 이것만은 알고 쓰자
=> useState의 기본적인 사용법 외에도 useState 사용 시 주의할 점에 대해 자세히 정리한 글입니다.


📌 useEffect

컴포넌트 내의 상태의 변화가 있을 때, 이를 감지하여 특정 작업을 해줄 수 있는 훅입니다.

일반적으로 sideEffect의 처리를 위해서 사용된다고 말합니다.

sideEffect란?

=> 컴포넌트가 화면에 렌더링된 이후에 비동기로 처리되어야 하는 부수적인 효과들을 말합니다. 예를 들어 API를 호출하는 경우 데이터를 비동기적으로 가져와야 하는데, 만약 그렇지 않다면 데이터를 가져오는 시간 동안 렌더링이 지연될 수도 있기 때문입니다.

sideEffect란 다음과 같은 경우를 모두 포함합니다.

  • 함수에서 함수 안의 내용물만으로 결과값을 만드는 것 외에 다른 행위들
  • 함수의 output를 만들기 위해 외부에 값을 사용하는 것
  • 외부 변수를 함수 안에서 변경시키는 것

사용 방법

useEffect(()=>{
   // 
}, []) // 의존성 배열

useEffect의 두번째 인자로 어떤 값을 전달하는 지에 따라, 첫번째 인자로 전달된 콜백 함수(effect)가 언제 실행되는 지가 결정됩니다.


1. 아무것도 전달하지 않으면

useEffect(()=>{
   
}) // 아무것도 전달 x

기본적으로는 첫 렌더링(마운트)그 이후의 모든 업데이트에 대해서 effect를 수행하게 됩니다. 이는 클래스 컴포넌트의 componentDidMount, componentDidUpdate를 합쳐놓은 것과 같습니다.

=> 불필요한 렌더링을 줄이고 싶다면 useEffect의 두 번째 인자인 의존성 배열 을 넣어주면 됩니다.


2. 빈 배열 [] 전달 시,

useEffect(() => {
  console.log("Component Loaded");
}, []);
// 컴포넌트가 마운트 됐을때만 실행된다. (componentDidMount)

맨 처음 컴포넌트 생성 시, 즉 마운트 될 때만 실행됩니다.

=> 물론, 마운트 될 때 실행된다는 것의 의미는 마운트 시 발생하는 렌더링 이후에 실행된다는 의미입니다.


3. 변수 전달 시, [count]

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]);
// count가 바뀔 때만 effect를 재실행

맨 처음 마운트 되었을 때와, count state의 값이 바뀔 때만 실행됩니다.


clean up

useEffect(()=>{
    console.log("Component Loaded");
    const handleScroll = () => {
        console.log(window.scrollY);
    };
    
    document.addEventListener("scroll", handleScroll);
    
    return () => document.removeEventListener('scroll', handleScroll);
}, []);

window에 스크롤에 대한 event를 등록하고, 컴포넌트가 사라질 때, 이를 해제해줄 수 있습니다.

=> useEffect 안의 첫 번째 인자인 콜백함수 내 return 문해당 컴포넌트가 제거될 때(unmount될 때) 실행됩니다.

❓ 왜 컴포넌트가 제거 될 때 return 문이 실행될까?

=> useEffect는 원래 컴포넌트가 살아있는 동안 컴포넌트 내 특정 변수나 상태가 변화될 때마다 실행되야 하므로, 이를 감지하기 위해서는 컴포넌트가 살아있는 동안 계속 감지하면서 있다가, 컴포넌트가 제거될 때 return 하는 건가 ?

=> 의존성 배열로 아무것도 전달하지 않던, []를 전달하던, [count]를 전달하던, useEffect 내의 return 문은 무조건 컴포넌트가 제거(unmount) 될 때 한번만 실행됩니다.


useEffect 실행 타이밍

useEffect로 전달된 함수는 컴포넌트 렌더링 - 화면 업데이트 - useEffect 함수 실행 순으로 실행됩니다. 즉, useEffect실행이 최초 렌더링 이후 에 된다는 것을 유의해야합니다.

만약 화면을 다 그리기 이전에 동기화 되어야 하는 경우에는, useLayoutEffect()를 활용하여 컴포넌트 렌더링 - useLayoutEffect 실행 - 화면 업데이트 순으로 effect를 실행시킬 수 있습니다.


📌 useRef

2가지 경우에 useRef를 사용할 수 있습니다.

  1. DOM에 직접 접근할 때 사용합니다.

    => 리액트는 DOM으로의 직접 접근을 막고 있기 때문에 ref 를 통해 접근해야합니다.

  2. 변수의 값이 변하더라도 리렌더링을 유발하고 싶지 않을 때 사용합니다.

    => ref 안에 저장되어 있는 값이 변경되어도 컴포넌트는 다시 렌더링되지 않습니다. 또한, 컴포넌트가 아무리 렌더링 되어도 ref 안에 저장되어 있는 값은 변화되지 않고 그대로 유지됩니다.

    => ref의 값은 컴포넌트의 전 생애주기를 통해 유지되기 때문입니다. 컴포넌트가 브라우저에 마운팅 된 시점부터 마운트가 해제될 때까지 같은 값을 계속해서 유지할 수 있습니다.

    => 변경 시 렌더링을 발생시키지 말아야 하는 값을 다룰 때 편리합니다.

    useState를 통해 생성된 state는 그 값이 변경될 때마다 다시 렌더링됩니다.

    VS

    useRef는 값이 변경되더라도 다시 렌더링되지 않습니다.


1. dom에 접근

const inputRef = useRef();

<input ref = {inputRef}/>

그럼 이제 inputRef.currentinput 태그에 접근할 수 있습니다.


하위 컴포넌트의 dom에 접근하고 싶다면

EX)

App 컴포넌트에서 => 하위 컴포넌트인 Input 컴포넌트의 dom에 접근하고 싶다면,

=> App 컴포넌트에서 Input 컴포넌트의 dom에 ref를 통해 접근할 수 있다.

=> forwardRef 이용

function App() {
    const inputRef = useRef(); // useRef 선언
    
    return (
    	<div>
    		<Input ref={inputRef}/> // ref 전달 
			<button onClick={()=>inputRef.current.focus()}> Focus </button>
        </div>
	);
}
const Input = React.forwardRef(( _, ref ) => {
    return (
    <>
    	Input: <input ref={ref} />
    </>
    );
});

export default Input;

2. 리렌더링 유발하지 않는 변수로 사용

useRef 함수는 current 속성을 가지고 있는 객체를 반환하는데, 인자로 넘어온 초기값을 current 속성에 할당합니다. 이 current 속성은 값을 변경해도 상태를 변경할 때 처럼 React 컴포넌트가 다시 랜더링되지 않습니다. React 컴포넌트가 다시 랜더링될 때도 마찬가지로 이 current 속성의 값이 유실되지 않습니다.

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

function ManualCounter() {
  const [count, setCount] = useState(0);
  const intervalId = useRef(null);
  console.log(`랜더링... count: ${count}`);

  const startCounter = () => {
    intervalId.current = setInterval(
      () => setCount((count) => count + 1),
      1000
    );
    console.log(`시작... intervalId: ${intervalId.current}`);
  };

  const stopCounter = () => {
    clearInterval(intervalId.current);
    console.log(`정지... intervalId: ${intervalId.current}`);
  };

  return (
    <>
      <p>자동 카운트: {count}</p>
      <button onClick={startCounter}>시작</button>
      <button onClick={stopCounter}>정지</button>
    </>
  );
}

위의 예제에서는 intervalId라는 data가 렌더링 시마다 변하지 않도록 하고 싶습니다. 그래서 ntervalIduseRef 훅을 이용해 초기화해주었습니다.

❓ useRef 작동 원리 알아보기

useRef는 선언된 컴포넌트 내에서 그냥 변수에 값을 할당하는 게 아니라, 변수에 참조값을 할당하고, 그 참조값이 값을 가리키고 있어서, 즉 한 단계 더 거쳐서 저장해서 값이 변하더라도 참조값은 변하지 않으므로 그래서 렌더링이 일어나지 않는 것 아닐까

=> const intervalId = useRef(null)에서 IntervalId가 참조값이고, intervalId.current를 통해 그 실제 값을 접근할 수 있는 것 같다 !


📌 useMemo, React.memo

useMemo

원래, 컴포넌트 내의 변수는 리렌더링 시에 다시 정의됩니다. 하지만 useMemo를 사용하면, 매 리렌더링 시마다 다시 정의되는 것이 아니라, 특정 값이 변할 때만 정의됩니다.

즉, useMemo변수의 최적화를 위한 훅입니다.

컴포넌트는 사실 함수입니다. 즉, 컴포넌트란 JSX를 반환하는 함수에 불과합니다. 컴포넌트가 렌더링 된다는 것은 누군가가 이 함수 컴포넌트를 호출해서 실행하는 것을 의미합니다. 이는 함수가 실행될 때마다 내부에 선언되어있던 변수, 함수 등이 매번 다시 선언되거나 다시 실행이 된다는 것을 의미합니다.

컴포넌트가 리렌더링 되는 조건

  1. 컴포넌트의 state가 변경

  2. 컴포넌트의 props가 변경

  3. 부모 컴포넌트 리렌더링

    => 부모 컴포넌트가 리렌더링될 때 새로운 prop이 들어오지 않더라도 부모 컴포넌트가 리렌더링되면 자식 컴포넌트 역시 리렌더링 된다.

만약 컴포넌트를 최적화하지 않는다면, 부모 컴포넌트가 변경되기만 하더라도 리렌더링 될 수 있음을 의미합니다. 그리고 리렌더링 시, 연산이 많이 걸린다면 이는 비효율적일 것입니다. 따라서 이를 useMemo를 사용함으로써 해결이 가능합니다.

사용 예시

const ShowSum = ({label, n]}) => {
   // const result = sum(n);
    
   const result = useMemo(()=>sum(n), [n]);
    
   return (
   	<span>
           {label}: {result}
    </span>
   )
}

export default ShowSum;

기록해 둘 표현식인 sum(n) 을 적어두고, 어떤 값이 변할 때만 이를 다시 변경할 것인지를 [ ] 안에 넣어줌 => [n] => n 값이 변할 때만 다시 계산

prop인 label의 값이 계속 바뀐다면, 바뀔 때마다 컴포넌트는 계속 리렌더링되지만, 대신 result는 렌더링 시마다 계산하지 않는 것

=> 의존성 배열에 들어있는 n 이 변할 때만 다시 계산

=> 즉, 특정 변수를 언제 다시 계산(정의)할 것인지를 지정해줄 수 있습니다.

그럼 useMemo를 이용한 변수는 state 인가 ?

useMemo를 사용하여 선언된 변수는 컴포넌트의 state와는 조금 다릅니다. useMemo는 메모이제이션된 값을 반환하는 훅으로, 특정 값이나 연산의 결과를 기억하여, 의존성이 변경되지 않으면 이전에 계산된 값을 재사용합니다.

컴포넌트의 상태(State)는 useState 훅을 사용하여 관리되며, 상태가 변경되면 컴포넌트가 리렌더링됩니다. useMemo는 주로 계산 비용이 높은 연산의 결과를 저장하고 싶을 때 사용됩니다. 이 때, 의존성 배열(dependency array)을 통해 해당 값이 의존하는 변수가 변경될 때만 다시 계산됩니다.

요약하면, useMemo로 선언된 변수는 상태(State)가 아니며, 컴포넌트의 리렌더링과 직접적인 관련이 없습니다. 대신, 특정 값을 메모이제이션하여 필요할 때만 계산을 수행하는 용도로 사용됩니다.

React.Memo

useMemo 훅과 비슷하지만 약간 다른 개념인 React.memo에 대해서도 알아보겠습니다.

위에서 컴포넌트가 리렌더링 되는 조건을 살펴봤습니다.

컴포넌트가 리렌더링 되는 조건

  1. 컴포넌트의 상태가 변경
  2. 부모로부터 받는 props가 변경
  3. 부모 컴포넌트의 상태가 변경

위에서 알 수 있는 건, 부모 컴포넌트의 상태가 변경되기만 하더라도 자식 컴포넌트가 리렌더링 될 수 있다는 것입니다. 하지만, 부모 컴포넌트의 상태만 변경되고 자식 컴포넌트는 변경된 것이 없는데, 리렌더링된다면 이는 비효율적일 것입니다.

여기서 React.memo를 사용한다면, 부모 컴포넌트의 상태가 변경되더라도 자식 컴포넌트가 변경되지 않았을 경우 자식 컴포넌트의 리렌더링을 막을 수 있습니다.

사용 예시

단순히 함수 컴포넌트 식을 React.memo() 로만 감싸주면서 사용할 수 있습니다.

Box.jsx

const Box = React.memo(()=>{
    console.log("Box 컴포넌트 렌더링");
    return <div/>
})

export default Box;

App.jsx

function App(){
    const [count, setCount] = useState(0);
    
    return (
    	<div>
        	{count}
            <button onClick={()=>setCount(count+1)}>+</button>
        	<Box/>
        </div>
    )
}

export default App;

=-> 만약 React.memo를 사용하지 않았다면 button 클릭으로 인해 부모 컴포넌트의 상태인 count 값이 증가할 때마다, 자식 컴포넌트인 Box 컴포넌트가 리렌더링되어 console.log("Box 컴포넌트 렌더링");가 계속 찍혔을 것입니다.


📌 useCallback

컴포넌트가 렌더링 될 때마다 함수가 재정의되지 않고, 의존성 배열 내의 값이 변할 때만 재정의되도록 만들어주는 훅입니다.

위에서도 말했듯이, 컴포넌트가 렌더링 된다는 것은 컴포넌트 함수가 다시 호출되는 것이고, 그 안의 함수도 다시 정의됩니다.

하지만, 함수가 재정의된다면 함수는 객체 타입이므로 그 참조값이 바뀔 것입니다. 이 때, 함수가 다시 정의되는 것을 막기 위해 useCallback 훅을 사용할 수 있습니다.

함수가 재정의되는 것을 막아줌으로써, 이 함수가 하위 컴포넌트에게 prop으로 전달된다면 그 하위 컴포넌트의 리렌더링도 방지할 수 있습니다.

=> 참조값이 변하지 않기 때문에 prop으로 전달되는 값도 변하지 않습니다.

사용 예시

CheckBox.jsx

const CheckBox = React.memo(({ label, on, onChange})) => {
  console.log(label, on);
  return (
    <label>
      {label}
      <input type="checkbox" defaultChecked={on} onChange={onChange}/>
    </label>
  )
}

export default CheckBox;

App.jsx

function App(){
  const [foodOn, setFoodOn] = useState(false);
  const [clothesOn, setClothesOn] = useState(false);
  const [shelterOn, setShelterOn] = useState(false);

  const foodChange = useCallback((e)=> setFoodOn(e.target.checked), []);
  const clothesChange = useCallback((e)=> setClothesOn(e.target.checked), []);
  const shelterChange = useCallback((e)=> setShelterOn(e.target.checked), []);
  
  return (
    <div>
      <CheckBox label="Food" on={foodOn} onChange={foodChange} />
      <CheckBox label="Clothes" on={clothesOn} onChange={clothesChange} />
      <CheckBox label="Shelter" on={shelterOn} onChange={shelterChange} />
    </div>
  )
}

export default App;
  • Food check box의 체크를 눌렀을 때, props로 전달된 onChangefoodChange가 실행되고, 이를 통해 App 컴포넌트의 state인 foodOn이 변경될 것입니다.

  • App 컴포넌트의 state가 변경되었으므로, App 컴포넌트는 다시 렌더링 될 것입니다. 여기서 만약 useCallback으로 각 함수 foodChange, clothesChange, shelterChange를 감싸주지 않았다면, 이 함수들은 모두 재정의 될 것입니다.

  • 또한, 이 함수들은 자식 컴포넌트들에게 props로 전달되고, 각 자식 컴포넌트 입장에서는 props가 변경된 것이기 때문에 자식 컴포넌트들도 리렌더링될 것입니다.

    => React.memo로 감쌋지만, props가 변경되는 것이므로 렌더링됩니다.

  • 하지만 useCallback으로 각 함수를 감싸줬기 때문에, App 컴포넌트 내의 함수들인 foodChange, clothesChange, shelterChangeApp 컴포넌트가 리렌더링되어도 재정의되지 않습니다.

💡

useCallback 을 사용할 때 , 2번째 인자에 defendency array 를 넘기고, setData 로 state를 초기화 해줄 때, 함수형 업데이트로 초기화를 해주어야 합니다.

함수형 업데이트로 초기화를 해주어야, defenden array를 비워도 항상 최신의 state를 함수형 업데이트의 인자를 통해서 참조할 수 있게 됩니다.


🙇🏻‍♂️ 참고

[React] React Hooks 정리

React Hooks 이해하기

React Hooks: useRef 사용법

profile
꾸준히, 깊게

1개의 댓글

comment-user-thumbnail
2024년 3월 13일

자주 사용되는 훅부터 메모이제이션까지 깔끔하게 정리하신 것 같아요!! 👍👍 좋은 글 잘 보고 가요!

답글 달기