본문 바로가기

카테고리 없음

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

함수형 컴포넌트가 상태를 사용하거나,
클래스형 컴포넌트의 생명주기 메서드를 대체하는 등의 작업을 위해 훅 (hook) 을 사용하기 시작했다. 

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

useState, useEffect, useMemo, useCallback useRef, useContext,
useReducer, useImperativeHandle, useLayoutEffect, useDebugValue.. 

 

useState

useState : 함수형 컴포넌트 내부에서 상태를 정의하고 관리

import {useState} from 'react';

const [state, setState] = useState(initialState)

useState의 초기값이 복잡하거나 무거운 연산을 포함하고 있다면,

익명함수 () => 를 통해 변수 대신 함수를 실행해 값을 반환하는 것을 권장한다. (게으른 최적화)

이를 통해 initialState부문은 state가 처음 만들어질 때만 사용되고, 이후 랜더링이 발생하여도 무시된다. 

 

게으른 최적화 사용 상황 예시

  • localStorage나 sessionStorage에 대한 접근
  • map, filter, find와 같은 배열에 대한 접근
  • 초깃값 계산을 위해 함수 호출이 따로 필요할 때

useEffect

"애플리케이션 내 컴포넌트의 여러 값들을 활용해 동기적으로 부수 효과를 만듬"

렌더링 되거나, 값이 변경될 때, 특정 동작을 수행하도록 설정!

 

effect가 컴포넌트의 사이드 이펙트(부수효과)를 의미한다는 것을 명심하자.

function Component() {
// ...
useEffect(() => {
// do something
}, [props, state])
// ...
}

(콜백 함수, 의존성 배열)을 받아 의존성 배열의 값이 변경될 때마다 첫 번째 인수인 콜백을 실행한다.

(값이 변경되었는지는 렌더링 시 이전값과 얕은 비교를 수행)

 

클래스형 컴포넌트의 생명주기 메서드와 비슷한 작동을 할 수 있다.

클린업 함수를 반환할 수 있는데, 이는 컴포넌트가 언마운트될 때 실행된다.

 

*클린업 함수란?

함수형 컴포넌트가 리렌딩되었을 때 의존성 변화가 있었을 당시 이전의 값을 기준으로 실행한다.

특정 이벤트의 핸들러가 무한히 추가되는 것을 방지하고,
이전 상태 정리, 네트워크 요청 정리 및 리소스 해제의 역할을 한다.

 

의존성 배열 값에 따른 동작

  • 빈 배열 ([]): 최초 렌더링 직후에만 콜백이 실행되고, 이후에는 실행되지 않습니다.
  • 의존성 배열에 값이 있음: 배열에 있는 값이 변경될 때마다 콜백 함수가 실행됩니다.
  • 의존성 배열이 없음: 컴포넌트가 렌더링될 때마다 콜백이 실행. 렌더링 여부 확인을 위해 주로 사용

 

! 사용 시 주의점

  • 주식은 최대한 자제하라 ( eslint-disable-line reac hooks / exhaustive-deps과 같은 에러 발생 가능)
  • useEffect의 첫 번째 인수에 함수명을 부여하라 (익명함수가 아닌 기명함수를 사용)
  • 거대한 useEffect를 만들지 마라 (JS 실행 성능에 영향을 미치므로, 가급적 간결하고 가볍게)
  • 불필요한 외부 함수를 만들지 마라 (내부에서 사용할 부수 효과라면 내부에서 정의해 사용하자)
  • 콜백 함수로는 비동기 함수를 바로 넣을 수 없다! -> race condition

 

useMemo

비용이 큰 연산에 대한 결과를 저장해두고, 해당 값을 반환 (메모이제이션)

import{useMemo} from 'react';

const memoizedValue = useMemo(() => expensiveComputation(a,b), [a,b])

(값 반환을 위한 생성 함수,  해당 함수가 의존하는 값의 배열) 을 인수로 가진다.

 

의존성 배열의 값이 변경되지 않았다면, 기존 기억하고 있던 값을 반환

변경되었다면, 첫 번째 인자인 함수를 재실행후 반환 및 다시 기억

(값 뿐만 아니라 컴포넌트도 기억할 수 있지만, 그 때는 React.memo를 사용하는 것이 더 권장됨)

 

useCallback

useMemo가 값을 기억한 것과 달리, useCallback은 인수로 받은 콜백 자체를 기억한다. (함수에 대한 메모이제이션)

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

const Counter = () => {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;

 

 => 함수의 재생성을 막아 불필요한 리소스 또는 리렌더링을 방지할 수 있다.

 

 

useRef

useState와 동일하게 컴포넌트 내부에서 렌더링이 이러안도 변경 가능한 상태값을 저장한다는 공통점이 있다.

차이점으로는 current를 통해 값에 접근이 가능하며, 값 변경시에도 렌더링을 발생시키지 않는다는 점이 있다.

 

왜? 필요할까  사용하지 않고 외부에서 값을 선언하여 관리하였다면..

  1. 컴포넌트가 실행되어 렌더링되기 전에도 변수가 메모리 공간을 차지함
  2. 컴포넌트 인스턴스 하나당 하나의 값을 필요로 하는데, 충족시킬 수 없음
function RefComponent() {
	const inputRef = useRef()
	
    // 이때는 미처 렌더링이 실행되기 전(반환되기 전)이므로 undefined를 반환한다.
	console.log(inputRef.current) // undefined
    
	useEffect(() => {
		console. log(inputRef.current) // <input type="text"x/input>
	}, [inputRef])
	
    return <input ref={inputRef} type="text" />
}

 DOM에 접근하고 싶을 때  useRef를 주로 사용한다.

 

useContext

상위 컴포넌트에서 만들어진 Context를 함수형 컴포넌트에서 사용할 수 있도록 해줌 (props값을 하위로 전달)

import React, { createContext, useContext } from 'react';

// Context 생성
const MyContext = createContext<{ hello: string } | undefined>(undefined);

function ParentComponent() {
  return (
    <>
      {/* 최상위 Provider */}
      <MyContext.Provider value={{ hello: 'react' }}>
        {/* 중첩된 Provider */}
        <MyContext.Provider value={{ hello: 'javascript' }}>
          <ChildComponent />
        </MyContext.Provider>
    </>
  );
}

function ChildComponent() {
  // Context 값 사용
  const value = useContext(MyContext);

  // 최하위 Provider의 값인 'javascript'가 출력됨
  return <div>{value ? value.hello : '값 없음'}</div>;
}

여러 개의 Provider가 있다면, 가장 가까운 Provider의 값을 가져옴 

주입된 상태를 사용할 수 있게 해줄 뿐, 렌더링 최적화에는 영향 X. 

 

Provider에 의존성이 생기는 것을 인지하자. 

=> 컨텍스트가 미치는 범위는 필요한 환경에서 최대한 좁게 만들어야 한다!

 

useReducer

useState의 심화 버전, 보다 복잡한 상태값에 대해 미리 정의한대로 관리할 수 있다.

const [state, dispatch] = useReducer(reducer, initialState, init);

 

  • state: 현재 useReducer가 관리하고 있는 상태 값
  • dispatcher: 상태 값을 업데이트하는 함수입니다. setState와 달리 action 객체를 전달받아 상태를 업데이트
  • reducer: 상태 업데이트 로직을 정의하는 함수
    state와 action 두 인자를 받아 새로운 상태를 반환. 이 반환된 상태값이 상태의 새로운 값이 됩니다.
  • initialState: 상태의 초기값
  • init: 초기 상태를 설정하는 함수로, 선택적으로 사용 (게으른 초기화 적용)
    기본적으로는 initialState가 그대로 초기 상태로 사용되지만, init 함수가 제공되면 초기값을 커스터마이즈
import React, { useReducer } from 'react';

// 초기 상태값
const initialState = { count: 0 };

// init 함수
function init(initialState) {
  return { count: initialState.count + 5 };
}

// reducer 함수
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('Unknown action type');
  }
}

function Counter() {
  // useReducer에 init 함수 적용
  const [state, dispatch] = useReducer(reducer, initialState, init);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
}

export default Counter;

 

useImperativeHandle

부모 컴포넌트가 자식 컴포넌트의 내부 기능을 제어할 수 있도록 도와주는 훅
부모 컴포넌트에서 자식 컴포넌트의 참조(ref)를 통해 특정 메서드나 속성에 접근할 수 있게 해줌

주요 사용 시점
- 자식 컴포넌트의 특정 기능이나 속성을 부모 컴포넌트에서 노출시키고 싶을 때.
- 보통 `forwardRef`와 함께 사용하여, 자식 컴포넌트의 특정 내부 동작을 제어해야 할 때 사용

import React, { useImperativeHandle, forwardRef, useRef } from 'react';

// 자식 컴포넌트
const ChildComponent = forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));

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

// 부모 컴포넌트
function ParentComponent() {
  const childRef = useRef();

  const handleClick = () => {
    childRef.current.focus();
  };

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleClick}>Focus Child Input</button>
    </div>
  );
}

export default ParentComponent;


위 예시에서 부모 컴포넌트는 `useImperativeHandle`을 통해 자식 컴포넌트의 `focus` 메서드를 직접 호출할 수 있습니다.

 

useLayoutEffect

"이 함수의 시그니처는 useEffect와 동일하나. 모든 DOM의 변경 후에 동기적으로 발생핸다."

`useLayoutEffect`는 리액트 훅 중 하나로,  DOM이 업데이트된 후, 브라우저가 화면을 그리기 전에 실행
이는 `useEffect`와 유사하지만, 화면에 변화가 일어나기 전에 동기적으로 실행된다는 점에서 차이가 있다.
레이아웃을 변경하거나, DOM을 직접 조작할 때 주로 사용

주요 사용 시점
- DOM 업데이트가 완료된 직후, 렌더링 이전에 동기적으로 코드를 실행해야 할 때.
- 레이아웃을 수정하거나, DOM을 직접 변경할 필요가 있을 때.
- 애니메이션을 처리하거나 레이아웃을 기반으로 동작해야 할 경우.

반드시 필요한 경우만 사용하자...!

import React, { useLayoutEffect, useState, useRef } from 'react';

function LayoutEffectExample() {
  const [width, setWidth] = useState(0);
  const divRef = useRef();

  // DOM이 렌더링된 후에 실행되며, 화면이 그려지기 전에 레이아웃을 측정
  useLayoutEffect(() => {
    const divWidth = divRef.current.offsetWidth;
    setWidth(divWidth);
  }, []);

  return (
    <div>
      <div ref={divRef} style={{ width: '100px', height: '100px', background: 'lightblue' }}>
        Box
      </div>
      <p>Box Width: {width}px</p>
    </div>
  );
}

export default LayoutEffectExample;

 

실행 순서

리액트가 DOM을 업데이트 -> useLayoutEffect를 실행 -> 브라우저에 변경 사항을 반영 -> useEffect를 실행 (있다면)

 

`useLayoutEffect`는 `div` 요소가 화면에 렌더링되기 전에 그 요소의 너비를 측정해 상태로 저장합니다. 
`useEffect`를 사용하면 렌더링 후에 측정되지만, `useLayoutEffect`는 화면이 그려지기 전에 동기적으로 실행됩니다.

 

useDebugValue


`useDebugValue`는 주로 커스텀 훅을 만들 때 사용되며, 리액트 개발자 도구에서 디버깅 정보를 제공하기 위한 훅.
이 훅은 개발 시에만 동작하며, 실제 런타임에서는 아무런 영향을 주지 않는다.
이를 통해 커스텀 훅이 리액트 개발자 도구에서 어떤 값을 관리하고 있는지 쉽게 파악할 수 있다.

주요 사용 시점:
- 커스텀 훅을 만들고, 이를 디버깅할 때 훅의 상태나 동작을 개발자 도구에서 확인하고 싶을 때.
- 특정 상태나 값의 디버깅 정보를 제공하여 커스텀 훅의 동작을 추적할 때.

import React, { useState, useDebugValue } from 'react';

// 커스텀 훅
function useCount(initialValue) {
  const [count, setCount] = useState(initialValue);

  // 디버깅용 값 설정
  useDebugValue(count > 5 ? 'High' : 'Low');

  return [count, setCount];
}

function DebugValueExample() {
  const [count, setCount] = useCount(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default DebugValueExample;

위 예시에서 useDebugValue는 count가 5 이상이면 'High', 그렇지 않으면 'Low'를 리액트 개발자 도구에 표시
이는 디버깅 목적에만 사용되며, 프로덕션에서는 아무런 영향을 미치지 않음

 

Hooks 정리.

훅 (hook) 설명 사용 상황
useState 상태를 관리하는 기본 훅 입력값, 카운터 등 상태를 관리할 때
useEffect 사이드 이펙트 처리.
마운트 및 업데이트 시 실행되는 로직 정의
데이터 로드, 이벤트 리스너 추가,
DOM 업데이트 등
useMemo 복잡한 계산 결과를 메모이제이션 재계산을 피하여 성능을 최적화 할 때
useCallback 함수 자체를 메모이제이션하여 함수 재생성을 방지 자식 컴포넌트에게 콜백 전달 시 
useRef DOM 요소나 값으 참조를 유지하기 위한 훅 
렌더링과 상관없이 값을 저장할 수 있음
DOM에 직접 접근하거나 렌더링에
영향 없이 참조를 유지해야 할 때
useContext Context API에서 데이터를 가져옴 전역 상태나 공통 데이터를 하위 컴포넌트에 전달할 때 (테마/사용자 인증 정보)
useReducer 복잡한 상태 로직 처리, reducer와 dispatch로 관리  상태 업데이트에 여러 action을 정의할때
useImperativerHandle 자식 컴포넌트에서 부모에게 메서드나 속성을 노출 부모가 자식의 내부  DOM요소나 동작을 제어 (주로 forwardRef와 함께 사용)
useLayoutEffect DOM이 업데이트된 직후,
화면이 그려지기 전에 동기적으로 실행
화면 렌더링 이전에 DOM의 크기나 위치를 측정하거나 조정해야 할 때
useDebugValue 커스텀 훅에서 디버깅 정보를 표시할 때 사용 디버깅 시 추가 정보 표시하고 싶을 때

 

 

3.1.2 훅의 규칙

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

  2. 훅을 호출할 수 있는 것은 리액트 함수형 컴포넌트, 사용자 정의 훅 두 가지 경우뿐이다.
    일반 자바스크립트 함수에서는 훅을 사용할 수 없다.

 

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

중복 코드를 피해 효율적이고, 유지보수가 쉬운 코드를 작성하자. 

재사용 할 수 있는 로직을 관리 하기 - 사용자 정의 훅, 고차 컴포넌트

 

3.2.1 사용자 정의 훅

리액트 훅의 이름은 use로 시작한다는 규칙이 있으며, 사용자 정의 훅도 이를 따라야 한다.

import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const result = await response.json();
        setData(result);
      } catch (error) {
        setError(error.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

export default useFetch;

복잡하고 반복되는 로직은 사용자 훅으로 간단하게 만들어 재사용할 수 있다.

 

3.2.2 고차 컴포넌트

HOC(Higher Order Component)는 컴포넌트 자체의 로직을 재사용하기 위한 방법이다.

리액트가 아닌 JS환경에서 널리 쓰이는 기법

 

React.memo가 가장 자주 쓰인다.

props의 변경이 없음에도 컴포넌트의 렌더링을 방지할 수 있다.

useMemo로 구현시에는 값을 반환받기에 JSX함수 방식이 아니라 {}를 사용한 할당식을 사용한다는 차이가 있다.

목적과 용도가 보다 뚜렷한 memo를 사용하는 것이 좋다!

const MemoizedComponent = React.memo(OriginalComponent);

 

직접 고차 컴포넌트 만들기

import React, { useState, useEffect } from 'react';

// 고차 컴포넌트 정의: 로딩 상태 처리
function withLoading(Component) {
  return function EnhancedComponent({ isLoading, ...props }) {
    if (isLoading) {
      return <p>Loading...</p>;
    }
    return <Component {...props} />;
  };
}

// 기본 컴포넌트: 데이터를 화면에 출력
function DataComponent({ data }) {
  return (
    <div>
      <h1>{data.title}</h1>
      <p>{data.body}</p>
    </div>
  );
}

// HOC 적용된 컴포넌트
const DataComponentWithLoading = withLoading(DataComponent);

function App() {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/posts/1')
      .then((response) => response.json())
      .then((data) => {
        setData(data);
        setIsLoading(false);
      });
  }, []);

  return <DataComponentWithLoading isLoading={isLoading} data={data} />;
}

export default App;

 

! 사용 시 주의점

부수 효과를 최소화해야 한다. 기존에 인수로 받는 컴포넌트의 props를 변경하지 않도록 해야한다.

여러개를 사용시 그 복잡성이 커짐에 주의하자. (최소한으로 사용하자)

 

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

사용자 정의 훅을 사용하는 경우

  • 리액트에서 제공하는 훅으로만 공통 로직을 격리할 수 있을 때
  • 상태 관리 및 비즈니스 로직을 여러 컴포넌트에서 재사용할 때.
  • 렌더링되지 않는 로직(API 호출, 이벤트 핸들러 등)을 재사용할 때.

 

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

  • 렌더링에 영향을 미치는 로직이 존재하는 경우
  • 컴포넌트에서 UI 관련 로직이나 렌더링 최적화를 할 때.
  • 기능을 추가하거나, 특정 조건에 따라 렌더링 여부를 결정할 때.

 

구분 사용자 정의 훅 고차 컴포넌트
주요 사용 목적 상태 관리, 비즈니스 로직 및 사이드 이펙트 처리 렌더링 로직 제어, UI관련 기능 추가, 권한 제어
적용 방식 로직을 함수로 추상화하여
여러 컴포넌트에서 재사용
컴포넌트를 인수로 받아 새 컴포넌트를 반환,
렌더링 로직에 영향을 미침
상황 예시 API 요청, 상태 관리,
이벤트 핸들러, 윈도우 이벤트 처리
UI 최적화, 렌더링 로직 캡슐화,
props기반 기능 추가