REACT HOOKS Overview

December 11, 2022 - 우원

REACT HOOKS 정리 해보기

미뤄왔던 리액트 훅들을 정리해보고 중요도별로 분류해보도록 하자.

먼저 React Conf 2018에서 Sophie Alpert와 Dan Abramov가 Hook을 소개한 영상이다. 궁금하다면 먼저 영상을 보고 시작하는 것도 좋을 것 같다.

useState

useState는 가장 기본적인 훅이며, 매우 빈번하게 많이 사용되는 훅이다. 타입스크립트가 아닌 자바스크립드 기반의 환경에서는 굳이 useState에 제네릭을 사용하지 않아도 된다. (물론 타입스크립트에서도 사용하지 않아도 된다.) 하지만 난 타입스크립트 기반으로 진행했던 프로젝트에서 대부분 타입을 지정해줬다. 그렇기 때문에 가급적이면 타입을 지정해주면 좋을 것 같다.

사용법은 아래와 같다.

import React, { useState } from 'react';

function Example() {
  // count라는 상태를 선언하고, setCount라는 상태를 바꿀 수 있는 함수를 선언한다.
  // 0으로 초기화 한다.
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      {/* 버튼을 클릭할 때마다 count가 1씩 증가한다. */}
      {/* onClick 이벤트가 발생할 때마다 setCount 함수를 호출하면서 기존의 count에 1을 더해준다. */}
      <button onClick={() => setCount(count + 1)}>
        Click me!
      </button>
    </div>
  );
}

선언부를 보면 useState는 배열을 반환한다. 첫 번째 원소는 현재 상태이고, 두 번째 원소는 상태를 바꿀 수 있는 함수이다. 두 번째 원소는 상태를 변경하는데 메모리상의 주소에 저장되어 있는 변수값을 직접 변경한다. 이어서 리액트는 상태변경을 감지하고 재렌더링을 한다.

alias를 호출해서 상태변경 함수를 거치지 않고 임의로 변경한다면 리액트가 상태변경을 감지하지 못하고 렌더링을 하지 않는다. 그렇기 때문에 부분적인 재렌더링이 필요하다면 반드시 상태변경 함수를 거쳐야 한다.

제네릭을 한번 추가해보자.

import React, { useState } from 'react';

function Example() {
  // count라는 상태를 선언하고, setCount라는 상태를 바꿀 수 있는 함수를 선언한다.
  // 0으로 초기화 한다.
  const [count, setCount] = useState<number | undefined>();

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me!
      </button>
    </div>
  );
}

제네릭으로 타입을 지정해주면 타입을 지정해주지 않은 경우보다 더 정확한 타입을 지정해줄 수 있다. number와 undefined를 OR 조건으로 사용해서 타입을 지정해주었다. 초기화를 하지 않았기 때문에 undefined가 들어가게 된다.

그런데 이때 상태변경이 일어난다면 에러가 발생한다. undefined는 number 타입에 속하지 않기 때문이다. 이런 경우에는 초기화를 해주면 된다. 아니면 상태를 확인하고 상황에 따라서 undefined는 0으로 초기화한다.

import React, { useState } from 'react';

function Example() {
  // count라는 상태를 선언하고, setCount라는 상태를 바꿀 수 있는 함수를 선언한다.
  // 0으로 초기화 한다.
  const [count, setCount] = useState<number | undefined>(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me!
      </button>
    </div>
  );
}

useEffect

useEffect는 리액트 컴포넌트가 렌더링 될 때마다 특정 작업을 수행할 수 있도록 하는 훅이다. useState와 함께 가장 많이 사용되는 훅이다. 특정 변수를 타게팅해서 그 변수가 변경될 때만 특정 작업을 수행하도록 할 수도 있다.

코드를 한번 살펴보자.

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

function Example() {
  const [count, setCount] = useState(0);

  // 클래스 방식의 componentDidMount와 componentDidUpdate가 비슷한 역할을 한다.
  useEffect(() => {
    // 내장 객체 doucment의 title을 변경한다.
    document.title = `당신이 누른 횟수 : ${count}`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useEffect는 기본적으로 렌더링 될 때마다 매번 실행된다. 이를 방지하기 위해 useEffect의 두 번째 인자로 빈 배열을 넣어주면 된다.

// 두 번째 인자로 빈 배열을 넣어주면 componentDidMount와 같은 역할을 한다.
// 두 번째 인자에 빈 배열이 들어가면 컴포넌트가 처음 렌더링 될 때만 실행된다.
useEffect(() => {
  document.title = `당신이 누른 횟수 : ${count}`;
}, []);

useEffect는 렌더링 될 때마다 매번 실행되는 것이 아니라, 특정 값이 업데이트 될 때만 실행되도록 할 수 있다.

// count를 추척한다.
useEffect(() => {
  document.title = `당신이 누른 횟수 : ${count}`;
}, [count]);

useContext

useContext는 Context API를 사용할 때 편리하게 사용할 수 있는 훅이다. Context API는 전역적으로 사용할 수 있는 데이터를 관리할 때 사용한다. Context는 쉽게 말하면 생성될 때의 주변의 모든 지역 변수를 포함하는 맥락이라고 생각하면 된다.

import type { AppProps } from 'next/app';

const defaultValue = {
  name: 'default',
  age: 0,
};

export const MyContext = React.createContext(defaultValue);

function MyApp({ Component, pageProps }: AppProps) {

  return (
    <MyContext.Provider>
      <Component {...pageProps} />
    </MyContext.Provider>
  );
}

export default MyApp;
import React, { useContext } from 'react';
import { MyContext } from '../pages/_app';

function MyComponent() {
  const value = useContext(MyContext);
  return <div>{value.name}</div>;
}

Provider로 감싸지면 내부에 렌더링되는 컴포넌트는 모두 MyContext를 사용할 수 있다.

useReducer

useReducer는 useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트해주고 싶을 때 사용한다. 첫번째 인자는 함수를 받고, 두번째 인자는 state에 해당 함수의 상태값들을 받는다. 그리고 선언과 동시에 dispatch라는 함수를 받는데, 이 함수를 사용하여 액션을 발생시킨다. 그렇기 때문에 reducer라는 function에 암묵적으로 두개의 인자를 주입해주는데, 첫번째 인자는 state, 두번째 인자는 action이다.

import React, { useReducer } from 'react';

// 연산을 위한 함수 선언
function reducer(state, action) {
  // action.type에 따라 다른 작업 수행
  switch (action.type) {
    case 'INCREMENT':
      return { value: state.value + 1 };
    case 'DECREMENT':
      return { value: state.value - 1 };
    default:
      // 아무것도 해당되지 않을 때 기존 상태 반환
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { value: 0 });

  return (
    <>
      <p>
        현재 카운터 값은 <b>{state.value}</b>입니다.
      </p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
    </>
  );
}

useCallback

useCallback은 렌더링 성능을 최적화해야 하는 상황에서 사용한다. 이 훅을 사용하면 이벤트 핸들러 함수를 필요할 때만 생성할 수 있다. 성능 향상을 위한 방법 중 하나로, 함수를 재사용할 수 있게 해준다.

이와 유사한 React.memo라는 기능은 컴포넌트의 props가 바뀌지 않았다면 리렌더링을 방지해준다. 그런데 컴포넌트가 리렌더링 될 때마다 함수를 새로 만들기 때문에 다른 참조값을 가진다 객체를 반환하게 되고 리렌더링이 발생하게 된다. 그렇기 때문에 useCallback을 사용하여 함수를 재사용할 수 있게 해주도록 한다.

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

function App() {
  const [number, setNumber] = useState(1);
  const [dark, setDark] = useState(false);

  const getItems = useCallback(() => {
    // 특정 값(number)이 바뀌었을 때만 함수 생성
    return [number, number + 1, number + 2];
  }, [number]);

  const theme = {
    backgroundColor: dark ? '#333' : '#fff',
    color: dark ? '#fff' : '#333'
  };

  return (
    <div style={theme}>
      <input
        type="number"
        value={number}
        onChange={e => setNumber(parseInt(e.target.value))}
      />
      <button onClick={() => setDark(prevDark => !prevDark)}>
        Toggle theme
      </button>
      <List getItems={getItems} />
    </div>
  );
}

function List({ getItems }) {
  const [items, setItems] = useState([]);

  useEffect(() => {
    setItems(getItems());
    console.log('Updating items');
  }, [getItems]);

  return items.map(item => <div key={item}>{item}</div>);
}

export default App;

useMemo

useMemo는 렌더링 성능을 최적화해야 하는 상황에서 사용한다. 이 훅을 사용하면 연산한 값을 재사용할 수 있다.

특정 값이 바뀌었을 때만 연산을 실행하고, 원하는 값이 바뀌지 않았다면 이전에 연산했던 결과를 다시 사용한다.

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

function complexCompute(num) {
  console.log('complexCompute');
  let i = 0;
  while (i < 1000000000) i++;
  return num * 2;
}

function App() {
  const [number, setNumber] = useState(42);
  const [colored, setColored] = useState(false);

  const styles = useMemo(() => ({
    color: colored ? 'darkred' : 'black'
  }), [colored]);

  const computed = useMemo(() => {
    return complexCompute(number);
  }, [number]);

  return (
    <>
      <h1 style={styles}>{computed}</h1>
      <button className="btn btn-success" onClick={() => setNumber(prev => prev + 1)}>증가</button>
      <button className="btn btn-danger" onClick={() => setNumber(prev => prev - 1)}>감소</button>
      <button className="btn btn-warning" onClick={() => setColored(prev => !prev)}>색변경</button>
    </>
  );
}

export default App;

useRef

useRef는 함수형 컴포넌트에서 ref를 쉽게 사용할 수 있게 해준다. useRef로 만든 객체 안의 current 값이 실제 엘리먼트를 가리킨다.

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

function App() {
  const [todos, setTodos] = useState([
    { id: 1, title: 'todo 1' },
    { id: 2, title: 'todo 2' },
    { id: 3, title: 'todo 3' }
  ]);
  const [todoTitle, setTodoTitle] = useState('');
  const ref = useRef(null);

  const addTodo = event => {
    if (event.key === 'Enter') {
      setTodos([
        ...todos,
        {
          id: Date.now(),
          title: todoTitle
        }
      ]);
      setTodoTitle('');
    }
  };

  return (
    <div className="container">
      <h1>Todo App</h1>

      <div className="input-field mt2">
        <input
          ref={ref}
          type="text"
          id="title"
          placeholder="할일 제목"
          value={todoTitle}
          onChange={event => setTodoTitle(event.target.value)}
          onKeyPress={addTodo}
        />
        <label htmlFor="title" className="active">
          할일 제목
        </label>
      </div>

      <ul>
        {todos.map(todo => {
          return (
            <li key={todo.id}>
              <label>
                <input type="checkbox" className="filled-in" />
                <span>{todo.title}</span>
              </label>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

export default App;
logo

우원 /

안녕하세요👏
우원입니다.
Email
Gihub
안녕하세요. 우원봇입니다.
logo