Development/ReactJs

[React] 리액트 Hooks - State, Context, Ref, Effect, Performance, Custom

재은초 2025. 3. 8. 22:24
반응형

Hook이란?

  • Hook은 React v16.8 부터 새롭게 도입된 기능으로, 이를 활용하면 클래스 컴포넌트를 작성할 필요 없이 함수 컴포넌트에서도 state 관리와 생명 주기(lifecycle) 메소드 등 여러 다양한 React 기능들을 사용할 수 있다.
  • Hook은 state, context, ref, lifecycle 등과 같은 다양한 React 개념을 사용자가 손쉽게 사용할 수 있도록 좀 더 직관적인 API(내장 Hook)를 제공한다. 또한, 컴포넌트 사이의 state 관련 로직을 재사용하기 위해 사용자가 직접 자신만의 Hook을 만들어 사용할 수도 있다.

Hook의 특징

  • Hook을 사용하면 컴포넌트로부터 state 관련 로직을 추상화할 수 있으며, 이를 활용하여 독립적인 테스트와 로직의 재사용이 가능해진다.
  • 또한, state 관련 로직과 사이드 이펙트(side effect) 등이 포함된 복잡한 컴포넌트를 유지보수가 쉬워지도록 비슷한 동작을 하는 컴포넌트들로 나누어 관리할 수 있다.
  • Hook은 이전 버전의 React와도 완벽하게 호환되므로 클래스 컴포넌트 기반으로 구현된 기존의 React 프로젝트에도 Hook을 점진적으로 도입할 수 있다. 즉, 기존에 작성한 코드는 그대로 유지한 채 새롭게 작성하는 컴포넌트부터 Hook을 사용하면 된다.
  • 만약 여러분이 현재 프로젝트에 Hook을 도입하여 얻는 장점이 그리 크지 않다고 판단된다면, 클래스 컴포넌트를 계속해서 사용해도 무방하다.

Hook의 사용 규칙

  • Hook을 사용할 때는 반드시 다음 두 가지 규칙을 지키면서 사용해야만, Hook이 제대로 동작할 수 있다.
    • Hook은 반복문이나 조건문, 중첩된 함수 등에서 호출해서는 안되며, 반드시 컴포넌트의 최상위 레벨에서 호출해야 한다.
    • Hook은 일반 자바스크립트 함수에서 호출해서는 안되며, React의 함수 컴포넌트 내에서만 호출해야 한다.
  • 참고로 사용자 정의 Hook 내에서도 Hook을 호출할 수 있다.

Hooks의 종류

  • React에서 기본적으로 제공하고 있는 내장 Hook의 종류는 다음과 같이 구분할 수 있다.
    • State Hooks
    • Context Hooks
    • Ref Hooks
    • Effect Hooks
    • Performance Hooks
  • React에서 제공하는 내장 Hooks 외에도 여러분은 자바스크립트 함수를 활용하여 자신만의 사용자 정의 Hook을 정의하여 사용할 수 있다.

 

State Hooks

  • React 컴포넌트는 state를 활용하여 가변적인 상태(state)를 기억할 수 있다. 예를 들어, Form 컴포넌트는 사용자 입력을 저장하기 위해 state를 사용할 수 있으며, Counter 컴포넌트는 현재 카운터를 저장하기 위해 state를 사용할 수 있다.
  • React에서 함수 컴포넌트에 state를 추가하려면 다음 Hook 중 하나를 사용하면 된다.
    • useState는 사용자가 직접 업데이트할 수 있는 state 변수를 선언한다.
    • useReducer는 reducer 함수 내부의 업데이트 로직을 사용하여 state 변수를 선언한다.

useState Hook

  • useState는 가장 기본적인 Hook으로 사용자가 직접 업데이트할 수 있는 state 변수를 선언하고 관리할 수 있다.
  • useState는 처음 렌더링을 수행할 때 초기 상태 값(initialState)을 인수로 전달 받고, 최신 state의 값을 유지하는 변수와 그 값을 업데이트할 수 있는 함수를 반환한다.
// useState 문법
const [state, setState] = useState(initialState)
// Counter.js

import { useState } from "react";

const Counter = () => {
  // 0을 초기값으로 하는 state와 setState() 함수 생성
  const [state, setState] = useState(0);

  return (
    <div>
      <h1>State 값 : {state}</h1>
      {/* setState() 함수를 사용하여 state의 값을 1씩 증가시킴 */}
      <button onClick={() => setState(state + 1)}>1씩 증가</button>
    </div>
  );
};

export default Counter;

useReducer Hook

  • useReducer는 useState보다 좀 더 복잡한 상황에서 state를 사용할 수 있도록 컴포넌트와 state의 업데이트 로직을 서로 분리시켜 관리할 수 있다.
  • useReducer의 첫 번째 인수는 reducer 함수를 전달 받으며, 두 번째 인수는 해당 reducer의 기본값을 전달 받는다. 그리고 현재 state 값과 action을 발생시키는 dispatch 함수를 반환한다.
  • useReducer가 반환하는 dispatch 함수를 사용하면 state를 다른 값으로 업데이트하고 리렌더링하도록 설정할 수 있다. 이때 dispatch 함수에는 인수로 action 값을 전달해야 한다.
  • useReducer를 사용했을 때 가장 큰 장점은 컴포넌트에서 state 업데이트 로직을 컴포넌트 외부로 분리시킬 수 있다는 점이다.
// useReducer 문법
const [state, dispatch] = useReducer(reducer, initialArg, init?)
// Counter.js
import { useReducer } from "react";

// 컴포넌트와 분리된 state 업데이트 로직
const reducer = (state, action) => {
  if (action.type === "increment") {
    return {
      count: state.count + 1
    };
  }
};

const Counter = () => {
  // reducer 함수와 count의 기본값을 0으로 전달하여 state를 생성함
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <div>
      <h1>State 값 : {state.count}</h1>
      {/* dispatch 함수에 action.type값으로 'increment'를 전달하여 리렌더링시킴 */}
      <button onClick={() => dispatch({ type: "increment" })}>1씩 증가</button>
    </div>
  );
};

export default Counter;

 

Context Hooks

Context란?

  • React에서 컴포넌트가 데이터를 다루는 방법에는 우리가 앞서 살펴본 props와 state 외에도 context라는 기능이 있다.
  • context는 데이터의 흐름이 부모 컴포넌트로부터 자식 컴포넌트에게 전달되는 props와 state와는 달리 데이터의 흐름과 상관없는 전역적인 데이터를 다룰 때 사용할 수 있다. 즉, context를 사용하면 사용자의 계정 정보나 설정 파일 등 해당 애플리케이션에 포함된 모든 컴포넌트에서 접근할 필요가 있는 데이터를 손쉽게 관리할 수 있다.
  • 조금 복잡한 설정을 통해 Redux와 같은 전역 상태 관리 라이브러리를 활용하여 context를 사용할 수도 있지만, Context API나 useContext Hook을 사용하여 좀 더 손쉽게 context를 사용할 수 있다.

Prop Drilling

  • React에서는 컴포넌트에 데이터를 전달해야 할 경우에 일반적으로 prop을 사용하여 데이터를 전달하게 된다. 하지만 prop은 데이터를 상위 컴포넌트에서 하위 컴포넌트 방향으로만 전달할 수 있다. 따라서 컴포넌트 트리의 한 부분에서 다른 부분으로 데이터를 전달해야 할 경우 prop을 전달하기 위한 하위 컴포넌트들을 거치면서 데이터가 전달되며, 이러한 과정을 Prop Drilling이라고 부른다.
  • Prop Drilling은 prop을 전달할 때 거쳐야 할 컴포넌트의 개수가 적으면 전혀 문제가 되지 않는다. 하지만 prop이 거쳐야 할 컴포넌트의 개수가 10개, 20개가 넘어가게 되면 코드를 통해 해당 prop을 추적하는 것이 어려워지며, 코드의 유지보수 또한 힘들어진다. 또한, 거쳐야 하는 모든 컴포넌트에서 prop을 설정해줘야 하기 때문에 개발자가 실수를 할 확률도 높아진다.
// Prop Drilling 예시

const App = () => {
  return <FirstComponent content="Hello, React!" />;
};

const FirstComponent = ({ content }) => {
  return <SecondComponent content={content} />;
};

const SecondComponent = ({ content }) => {
  return <ThirdComponent content={content} />;
};

const ThirdComponent = ({ content }) => {
  return <ComponentRequiringData content={content} />;
};

const ComponentRequiringData = ({ content }) => {
  return <h1>{content}</h1>;
};

export default App;
  • 이러한 Prop Drilling을 피하기 위해서는 전역 상태 관리 라이브러리를 사용하거나 Context API를 활용하여 데이터가 필요한 컴포넌트에서 직접 해당 값에 접근할 수 있도록 구현할 수 있다.

Context API를 활용하여 Context 사용하기

  • React 패키지의 createContext() 함수를 불어와 호출함으로써 context를 생성할 수 있다.
// Context 생성하기
import { createContext } from "react";

const SomeContext = createContext(defaultValue)
  • 이렇게 생성된 context 객체에는 Provider라는 컴포넌트가 포함되어 있다.
  • Provider 컴포넌트에 value prop으로 데이터를 전달하면 Provider 컴포넌트의 하위 컴포넌트에서는 해당 값에 바로 접근할 수 있다. 이렇게 Provider 컴포넌트로부터 값을 전달 받을 수 있는 컴포넌트의 개수에는 제한이 없으며, Provider 컴포넌트의 하위 레벨에 또 다른 Provider 컴포넌트를 배치하는 것도 가능하다. 단, 이 경우에는 하위 레벨의 Provider 값이 우선 시 된다.
// Context 설정하기
<SomeContext.Provider value={/* 특정 값 */}>
  // SomeContext에 접근할 수 있는 하위 컴포넌트들의 위치
</SomeContext.Provider>
  • 이렇게 설정한 context를 구독하기 위해서는 Consumer 컴포넌트를 사용한다.
  • Consumer 컴포넌트의 children은 반드시 함수이어야 하며, 이 함수는 context의 현재값을 인수로 전달받고 React 엘리먼트를 반환한다.
  • 이 함수가 전달 받는 value 매개변수의 값은 해당 context의 Provider 컴포넌트 중에서 Consumer 컴포넌트와 가장 가까운 상위 Provider 컴포넌트의 value prop 값과 동일한 값으로 설정된다.
  • 만약 Consumer 컴포넌트의 상위 레벨에 Provider 컴포넌트가 존재하지 않는다면 value 매개변수의 값은 createContext() 함수를 호출할 때 전달한 초깃값(defaultValue)으로 자동 설정된다.
// Context 구독하기
<HelloContext.Consumer>
  {value => /* Context 값을 이용한 렌더링 */}
</HelloContext.Consumer>

 

  • 이제 앞서 살펴본 Prop Drilling 예제를 Context API를 사용하도록 변경해 보자.
// App.js
import { createContext } from "react";

const HelloContext = createContext();

const App = () => {
  return (
    <HelloContext.Provider value="Hello, React!">
      <FirstComponent />
    </HelloContext.Provider>
  );
};

const FirstComponent = () => {
  return <SecondComponent />;
};

const SecondComponent = () => {
  return <ThirdComponent />;
};

const ThirdComponent = () => {
  return <ComponentRequiringData />;
};

const ComponentRequiringData = () => {
  return (
    <HelloContext.Consumer>{(value) => <h1>{value}</h1>}</HelloContext.Consumer>
  );
};

export default App;

useContext Hook을 활용하여 Context 사용하기

  • React Hook 중에서 useContext Hook을 활용해도 context를 손쉽게 사용할 수 있다. 단, useContext Hook은 함수 컴포넌트에서만 사용이 가능하고, 만약 클래스 컴포넌트에서 context를 사용하고 싶다면 Context API를 활용해야 한다.
  • useContext를 사용할 때도 Context API와 동일하게 createContext() 함수를 호출하여 context를 생성한다. 이렇게 생성된 context를 읽고 구독하려면 해당 컴포넌트에서 useContext를 호출하면 된다.
// useContext 문법
const value = useContext(SomeContext)
  • useContext는 context 객체를 인수로 전달 받아 해당 context의 현재 값을 반환하며, useContext를 호출한 컴포넌트는 해당 context의 값이 변경될 때마다 리렌더링된다.
/* styles.css */

/* light와 dark 공통 스타일 */
.box-light,
.box-dark {
  border: 1px solid black;
  padding: 10px;
  margin-bottom: 10px;
}
/* light 스타일 */
.box-light {
  color: black;
  background: white;
}
/* dark 스타일 */
.box-dark {
  color: white;
  background: black;
}
// App.js
import { createContext, useContext, useState } from "react";
import "./styles.css";

// ThemeContext 객체를 생성하고, 기본값을 'light'로 설정함
const ThemeContext = createContext("light");

const App = () => {
  // theme를 저장하기 위한 state를 생성하여 초깃값을 'light'로 설정함
  const [theme, setTheme] = useState("light");
  return (
    <>
      {/* ThemeContext를 구독하고 있는 하위 레벨의 컴포넌트들에게 value값을 전달함 */}
      <ThemeContext.Provider value={theme}>
        <Box text="Hello, React!">테마를 변경해 봅시다!</Box>
      </ThemeContext.Provider>
      <button
        onClick={() => {
          setTheme(theme === "dark" ? "light" : "dark");
        }}
      >
        테마 변경하기
      </button>
    </>
  );
};

const Box = ({ text, children }) => {
  //ThemeContext를 theme라는 이름으로 구독함
  const theme = useContext(ThemeContext);
  const className = "box-" + theme;
  return (
    <section className={className}>
      <h1>{text}</h1>
      {children}
    </section>
  );
}

export default App;
  • React에서는 context를 사용할 경우 컴포넌트의 재사용을 어렵게 만들기 때문에 꼭 필요한 경우에만 사용할 것을 권장하고 있다.

 

Ref Hooks

  • React 컴포넌트는 렌더링에 사용되지 않는 일부 데이터를 가지고 있을 수 있으며, 이러한 데이터를 저장하기 위해서 ref를 사용한다. ref는 state와는 달리 해당 값이 업데이트되도 컴포넌트가 리렌더링되지 않는다.
  • useRef Hook은 함수 컴포넌트에서 이러한 ref를 참조할 수 있게 해 줍니다.

useRef Hook

  • useRef는 ref를 활용하여 특정 DOM 노드를 선택하거나 컴포넌트 내의 변수를 관리할 수 있도록 해 준다.
  • useRef는 인수로 전달된 값(initialValue)으로 초기화 된 변경 가능한 ref 객체를 반환하며, 이 객체는 컴포넌트의 전 생명 주기 동안 유지된다. ref 객체는 current라는 프로퍼티 하나만을 가지고 있으며, 이 current 값이 실제 엘리먼트를 가리키게 된다.
// useRef 문법
const ref = useRef(initialValue)
// App.js
// useRef를 사용하여 동영상의 재생 여부를 저장하고 이를 유지하는 예제
import { useState, useRef } from "react";
import video from "./flower.mp4";

const VideoPlayer = () => {
  // false를 초기값으로 하는 state 생성
  const [isPlaying, setIsPlaying] = useState(false);
  // null을 초기값으로 하는 ref 객체 생성
  const ref = useRef(null);

  const handleClick = () => {
    // ref.current를 사용하여
    // 동영상이 재생 중이면 버튼의 동작을 pause로 설정하고,
    // 정지 중이면 버튼의 동작을 play로 설정함
    if (isPlaying) {
      ref.current.pause();
    } else {
      ref.current.play();
    }

    setIsPlaying(!isPlaying);
  };

  return (
    <>
      <video
        width="240"
        ref={ref}
        onPlay={() => setIsPlaying(true)}
        onPause={() => setIsPlaying(false)}
      >
        <source src={video} type="video/mp4" />
      </video>
      <br />
      <button onClick={handleClick}>{isPlaying ? "Pause" : "Play"}</button>
    </>
  );
};

export default VideoPlayer;
  • useRef를 사용하여 생성한 ref객체를 JSX 노드의 ref 속성으로 전달하면, React는 전달 받은 ref 객체를 해당 노드의 current 프로퍼티를 설정할 것이다. 즉, 다음 렌더링 시에도 useRef는 동일한 ref 객체를 반환하게 되는 것이다.
  • 이처럼 ref 객체는 current 프로퍼티에 의해 변경할 수 있고 어떤 값이든 저장할 수 있는 일반 컨테이너라고 생각하면 된다.

 

Effect Hooks

사이드 이펙트(side effects)

  • 일부 React 컴포넌트는 화면에 표시되는 동안 네트워크, 브라우저 API 또는 다른 라이브러리와 연결된 상태를 유지해야 하는 경우가 발생할 수 있으며, 이렇게 React에 의해 제어되지 않는 시스템을 외부 시스템(external system)이라고 부른다.
  • 대부분의 React 컴포넌트는 순수 함수(pure function)처럼 동작하며, 동일한 props를 인수로 전달 받으면 언제나 동일한 JSX 노드를 반환한다. 반면에 함수 내의 코드가 실행되면서 함수 외부에 존재하는 값이나 상태 등을 변경시킴으로써 다른 곳에서 예기치 못한 결과를 발생시키는 경우도 있을 수 있다.
  • React에서는 이처럼 컴포넌트가 화면에 렌더링된 후에 외부 시스템과 연결된 상태에서 비동기적으로 처리되어야 하는 작업들을 사이드 이펙트 또는 이펙트(effect)라고 부른다. 이러한 사이드 이펙트는 외부 시스템과 함께 수행되기 때문에 그 결과를 정확히 예측할 수 없다.
  • 대표적인 사이드 이펙트 작업은 다음과 같다.
    • 컴포넌트의 DOM의 직접 수정
    • 데이터를 가져오기 위한 외부 API 연동
    • setTimeout(), setInterval() 등을 사용한 호출 스케쥴링

useEffect Hook

  • 컴포넌트 내에서 이러한 사이드 이펙트를 직접 수행하는 것은 컴포넌트의 렌더링에 방해가 될 수 있다. 따라서 사이드 이펙트는 렌더링 과정과는 분리되는 것이 좋으며, 컴포넌트의 렌더링이 끝난 후에 처리되는 것이 바람직하다.
  • 실제 React에서는 특정 데이터를 가져오기 위해 외부 API 등을 호출하는 경우 화면에 렌더링할 수 있는 데이터를 먼저 모두 렌더링한 후에 실제 데이터는 비동기로 가져오는 것을 권장한다.
  • useEffect는 클래스 컴포넌트에서 여러 생명 주기(life cycle) 메소드를 사용하여 복잡한 과정을 통해 처리했던 사이드 이펙트 로직을 하나의 Hook으로 손쉽게 처리할 수 있도록 해준다.
// useEffect 문법
useEffect(setup, dependencies?)
  • useEffect의 첫 번째 인수는 setup 함수이며, 이 함수는 컴포넌트가 렌더링된 이후에 호출된다. setup 함수는 setup 코드를 통해 외부 시스템과 연결하고, 해당 시스템과의 연결을 종료할 수 있는 cleanup 함수(정리 함수)를 반환한다.
  • 두 번째 인수는 종속성 배열(dependency array)로 사이드 이펙트가 의존하는 모든 값을 포함하고 있는 배열이다. React는 필요할 때마다 setup 및 cleanup 함수를 호출하며, 이 과정을 여러 번 반복할 수 있다.
  • useEffect는 렌더링 될 때마다 기본적으로 실행되지만, 두 번째 인수의 종류와 setup 함수의 반환문의 존재 여부에 따라 실행되는 조건이 달라진다.

컴포넌트가 마운트 된 직후에만 실행

  • useEffect의 두 번째 인수로 전달되는 종속성 배열에 빈 배열을 전달하면, 컴포넌트가 가장 처음 렌더링 될 때만 setup 함수가 실행되고 이후 ref 값이 업데이트 될 때는 실행되지 않는다. 이 형태는 생명 주기 메소드 중 componentDidMount의 동작과 동일하다.
// App.js
import React, { useState, useRef, useEffect } from "react";

const App = () => {
  const [state, setState] = useState("");
  // 호출 횟수를 체크하기 위한 ref 생성
  let ref = useRef(0);

  // 컴포넌트가 가장 처음 렌더링 될 때만 setup 함수 실행
  useEffect(() => {
    ref.current = ref.current + 1;
    console.log(`setup 함수 실행(${ref.current})`);
  }, []);

  const onChange = (e) => {
    setState(e.target.value);
  };

  return (
    <>
      ✔ 입력 필드 : &nbsp;
      {/* 입력 필드에 텍스트를 입력할 때마다 onChange 이벤트가 발생함 */}
      <input value={state} onChange={onChange} />
      <br />
      ️✔ 입력된 텍스트 : {state}
    </>
  );
};

export default App;
  • 위 예제의 입력 필드에 텍스트를 입력해 봅시다. useEffect의 두 번째 인수로 빈 배열을 전달했기 때문에 ref의 값이 업데이트 되어도 더 이상 setup 함수를 호출하지 않는 것을 콘솔 창에서 확인할 수 있습니다.

컴포넌트가 업데이트 될 때마다 실행

  • useEffect의 두 번째 인수로 전달되는 종속성 배열에 특정 props나 state가 저장되어 있다면, 컴포넌트가 가장 처음 렌더링 될 때와 해당 값이 변경될 때마다 setup 함수가 실행된다. 이 형태는 생명 주기 메소드 중 componentDidUpdate의 동작과 동일하다.
// App.js
// state가 업데이트될 때마다 setup 함수 실행
  useEffect(() => {
    ref.current = ref.current + 1;
    console.log(`setup 함수 실행(${ref.current})`);
    console.log({ state });
  }, [state]);

컴포넌트가 언마운트 되기 직전에만 실행

  • useEffect의 첫 번째 인수로 전달되는 setup 함수가 cleanup 함수를 반환한다면, 컴포넌트가 언마운트 되기 직전에 해당 cleanup 함수가 실행된다. 이 형태는 생명 주기 메소드 중 componentWillUnmount의 동작과 동일하다.
// App.js
// 컴포넌트가 언마운트 되기 직전에 cleanup 함수를 실행함
  useEffect(() => {
    ref.current = ref.current + 1;
    console.log(`setup 함수 실행(${ref.current})`);
    console.log({ state });
    return () => {
      console.log(`cleanup 함수 실행(${ref.current})`);
    };
  }, [state]);

 

Performance Hooks

  • 컴포넌트를 리렌더링할 때 성능을 최적화하는 가장 일반적인 방법은 바로 불필요한 작업을 건너뛰는 것이다. 예를 들어, 앞서 렌더링한 이후 데이터가 변경되지 않았다면 이전에 캐시한 데이터를 재사용하거나 리렌더링을 건너뛰도록 설정하면 성능을 향상 시킬 수 있다.
  • React에서 데이터를 캐시하려면 다음 Hook 중 하나를 사용하면 된다.
    • useMemo를 사용하면 이전에 수행한 계산 결과를 캐시하여 재사용할 수 있다.
    • useCallback을 사용하면 이미 만들어 놓은 함수를 캐시하여 재사용할 수 있다.

useMemo Hook

  • useMemo를 사용하여 이전에 수행한 계산 결과를 캐시해 놓으면, 컴포넌트 내부에서 발생하는 연산 작업을 최적화할 수 있다.
  • useMemo는 렌더링하는 과정에서 특정 값이 바뀌었을 때만 연산을 실행하고, 해당 값이 바뀌지 않았다면 캐시해 놓은 이전 계산 결과를 그대로 재사용하는 방식으로 최적화를 수행한다.
  • useMemo의 첫 번째 인수는 해당 값을 계산하는 함수이며, 두 번째 인수는 배열을 전달 받는다. 두 번째 인수로 전달 받은 배열에 포함된 값이 변경되면 첫 번째 인수로 전달된 함수를 호출하여 해당 값을 계산하고, 만약 값이 변경되지 않았다면 이전에 연산한 값을 그대로 재사용하게 된다.
// useMemo 문법
const cachedValue = useMemo(calculateValue, dependencies)
// App.js
// useMemo를 사용하여 todos와 tab이 변경될 때에만 filterTodos() 함수를 호출하여 재연산하는 예제
import { useState, useMemo } from "react";

// Todo를 30개를 랜덤으로 생성함
const randomTodos = () => {
  const todos = [];
  for (let i = 0; i < 30; i++) {
    todos.push({
      id: i,
      text: `Todo ${i + 1}`,
      completed: Math.random() > 0.5
    });
  }
  return todos;
};

const todos = randomTodos();

// 필터 기능 구현
const filterTodos = (todos, tab) => {
  let startTime = performance.now();
  while (performance.now() - startTime < 2) {
    // while (performance.now() - startTime < 300) {
    // 굉장히 많은 연산으로 느려지는 코드를 시뮬레이션 하기 위해서 300ms를 아무것도 하지 않는 코드
    // 하지만 codesandbox 정책 상 10,000번 이상의 반복문이 되어 에러를 발생시킴.
    // 따라서 이 코드를 로컬에서 실행시킬 때는 startTime < 500으로 수정하여 테스트할 것.
  }
  return todos.filter((todo) => {
    if (tab === "completed") {
      return todo.completed;
    } else if (tab === "incompleted") {
      return !todo.completed;
    }
    return true;
  });
};

const TodoList = ({ todos, tab }) => {
  // todos와 tab이 변경될 때만 filterTodos() 함수를 호출하여 값을 재연산함
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  return (
    <ul>
      {visibleTodos.map((todo) => (
        <li key={todo.id}>{todo.completed ? <s>{todo.text}</s> : todo.text}</li>
      ))}
    </ul>
  );
};

const App = () => {
  const [tab, setTab] = useState("all");
  return (
    <>
      Filter :<button onClick={() => setTab("all")}>모두</button>
      <button onClick={() => setTab("incompleted")}>미완료</button>
      <button onClick={() => setTab("completed")}>완료</button>
      <TodoList todos={todos} tab={tab} />
    </>
  );
};

export default App;

useCallback Hook

  • useCallback을 사용하면 이전에 정의해 놓은 함수를 캐시해 놓음으로써, 렌더링 성능을 최적화할 수 있다. useCallback은 리렌더링 간 함수의 정의를 캐시하여 필요한 때 해당 함수를 재생성하는 방식으로 최적화를 수행한다.
  • useCallback의 첫 번째 인수는 생성하고 싶은 함수의 정의이며, 두 번째 인수는 배열을 전달 받는다. 이 배열에는 어떤 값이 바뀌었을 때 함수를 새로 생성해야 하는지 명시한다.
  • useCallback은 두 번째 인수로 전달 받은 배열에 포함된 값이 변경되면, 첫 번째 인수로 전달된 함수의 정의를 사용하여 해당 함수를 생성한다.
  • 만약 빈 배열을 전달하면 컴포넌트가 렌더링될 때 단 한 번만 함수가 생성되며, 배열에 숫자나 리스트를 포함시키면 해당 값이 변경되거나 리스트에 새로운 요소가 추가될 때마다 함수가 생성된다. 특히 생성하고자 하는 함수 내부에서 state 값을 사용해야 한다면, 두 번째 인수에 해당 state도 같이 포함시켜야 한다.
// useCallback 문법
const cachedFn = useCallback(fn, dependencies)
// Reset.js
import { memo } from "react";

const Reset = ({ handleClick }) => {
  return <button onClick={handleClick}>리셋</button>;
};

export default memo(Reset);
// Counter.js
import { useState } from "react";
import Reset from "./Reset";

const Counter = () => {
  const [state, setState] = useState(0);

  const handleReset = () => {
    setState(0);
  };

  return (
    <>
      <h1>State 값 : {state}</h1>
      <button onClick={() => setState(state + 1)}>1씩 증가</button>
      <Reset handleClick={handleReset} />
    </>
  );
};

export default Counter;
  • 위의 예제에서 Reset 컴포넌트는 메모이제이션 메소드인 memo() 덕분에 state에 의존하지 않는 코드라고 생각되기 쉽지만, 실제로는 state가 변경될 때마다 리렌더링 된다. 이때 아래 코드처럼 useCallback을 사용하면 state가 변경될 때마다 실행되는 불필요한 리렌더링을 방지할 수 있다.
const handleReset = useCallback(() => {
  setState(0);
}, []);

 

Custom Hook

Custom Hook(사용자 정의 Hook)

  • 여러 컴포넌트에서 비슷한 로직을 공유할 경우에는 이를 Custom Hook으로 작성해두면 해당 로직을 손쉽게 재사용할 수 있다.
  • Custom Hook은 보통의 함수처럼 인수로 무엇을 전달 받고 무슨 값을 반환해야 하는지 모든 것을 사용자가 직접 결정할 수 있다. 이러한 Custom Hook은 이전 React 컴포넌트에서는 생각하기 힘들었던 로직 공유의 유연성을 제공해 준다.
  • Custom Hook의 이름은 useCount나 useFileStatus 등과 같이 use로 시작하고 뒤이어 대문자로 이어지는 카멜 표기법(camelCase)에 따라 작성하며, 나머지는 Hook이 기본적으로 지켜야 하는 기본 사용 규칙만 지켜서 사용하면 된다.
  • 하지만 Custom Hook은 다른 내장 Hook들과는 달리 정의할 때는 몇 가지 사항을 추가로 고려해야 한다. 만약 이를 고려하지 않고 무분별하게 Hook을 정의하여 사용한다면 예측하지 못한 결과를 얻을 수 있으며, 이를 해결하기 위한 디버깅마저도 힘들 수 있다.
// usePrevious.js
// 이전 렌더링에서의 값을 기억하여 사용할 수 있도록 
// useEffect와 useRef를 사용하여 구현한 로직을 usePrevious라는 Custom Hook으로 작성한 예제
import { useEffect, useRef } from "react";

const usePrevious = (value) => {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

export default usePrevious;
// App.js
import { useState } from "react";
import usePrevious from "./usePrevious";

const App = () => {
  const [number, setState] = useState(0);
  const prevNumber = usePrevious(number);

  useEffect(() => {
    if (number === 1 && prevNumber === 2) {
      // ...
    }
  }, [number]);
  // ...
}

export default App;
  • 위의 예제에서 App 컴포넌트는 number의 현재 state 값이 1이고 이전 state 값이 2라면 특정 동작을 수행하게 된다. 이전 state 값을 저장하는 로직이 usePrevious라는 Custom Hook으로 추출되어 있기에 App 컴포넌트 뿐만 아니라 다른 컴포넌트에서도 해당 로직을 손쉽게 재사용할 수 있다.

클래스 컴포넌트 vs Hook

  • React에서 Hook을 사용하면 클래스 컴포넌트를 작성하지 않고도 대부분의 기능을 구현할 수 있다. 하지만 useState나 useReducer Hook을 사용하는 방식이 기존의 setState를 사용한 방식보다 좋다는 의미는 결코 아니다.
  • React에서는 Hook의 도입과 상관없이 기존의 클래스 컴포넌트를 앞으로도 계속해서 지원할 예정이라고 공지하였다. 따라서 클래스 컴포넌트로 구현된 기존의 프로젝트를 억지로 함수 컴포넌트와 Hook을 사용한 형태로 전환할 필요는 없다. 단, React에서는 함수 컴포넌트와 Hook의 사용을 권장하고 있다.
  • 앞으로 React 애플리케이션을 개발할 때는 함수 컴포넌트의 사용을 가장 먼저 염두에 두고, 꼭 필요한 상황에서는 클래스 컴포넌트의 사용도 같이 고려하는 것을 권장한다.

 

Reference

반응형