React Hook 2

1. useMemo

useMemo를 사용하면 함수형 컴포넌트 내부에서 발생하는 불필요한 연산을 최적화할 수 있다. 예를 들어 아래 코드에서는 input 값이 변경될 때마다 getAverage 함수를 호출한다.

Average.js

import React, { useState } from 'react';

const getAverage = numbers => {
  console.log('평균값 계산');
  if (numbers.length === 0) return 0;
  const sum = numbers.reduce((a, b) => a + b);
  return sum / numbers.length;
};

const Average = () => {
  const [list, setList] = useState([]);
  const [number, setNumber] = useState('');

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

  const onInsert = e => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
  };

  return (
    <div>
      <input value={number} onChange={onChange} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>Average: </b> {getAverage(list)}
      </div>
    </div>
  );
};

export default Average;

input 값이 변경될 때는 getAverage 함수를 호출할 필요가 없고 list 값이 변경되었을 때만 호출하면 되므로 이 때 useMemo를 사용하여 작업을 최적화할 수 있다. useMemo는 렌더링하는 과정에서 특정 값이 바뀌었을 때만 연산을 하고, 원하는 값이 바뀌지 않았다면 이전에 연산했던 결과를 다시 사용한다. 아래 코드는 useMemo를 사용하여 getAverage 함수 호출을 최적화한 코드이다.

Average.js

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

const getAverage = numbers => {
  console.log('평균값 계산');
  if (numbers.length === 0) return 0;
  const sum = numbers.reduce((a, b) => a + b);
  return sum / numbers.length;
};

const Average = () => {
  const [list, setList] = useState([]);
  const [number, setNumber] = useState('');

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

  const onInsert = e => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
  };

  // useMemo를 사용하여 list 값이 바뀔 때만 getAverage 함수 호출
  const avg = useMemo(() => getAverage(list), [list]);

  return (
    <div>
      <input value={number} onChange={onChange} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>Average: </b> {avg}
      </div>
    </div>
  );
};

export default Average;

2. useCallback

useCallback은 useMemo와 비슷한 함수이다. 주로 렌더링 성능을 최적화해야 하는 상황에서 사용하는데 이 Hook을 사용하면 이벤트 핸들러 함수를 필요할 때만 생성할 수 있다. 위에서 작성한 Average 컴포넌트의 onChage와 onInsert라는 함수는 컴포넌트가 리렌더링될 때마다 새로 생성된다.
컴포넌트의 렌더링이 자주 발생하거나 렌더링해야 할 컴포넌트의 개수가 많아지면 이 부분을 최적화 해주는 것이 좋다. 아래는 useCallback을 사용하여 onChange와 onInsert를 최적화한 코드이다.

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

const getAverage = numbers => {
  console.log('평균값 계산');
  if (numbers.length === 0) return 0;
  const sum = numbers.reduce((a, b) => a + b);
  return sum / numbers.length;
};

const Average = () => {
  const [list, setList] = useState([]);
  const [number, setNumber] = useState('');

  // 컴포넌트가 처음 렌더링될 때만 함수 생성
  // 단순 설정이므로 빈 배열을 넣어도 상관없다.
  const onChange = useCallback(e => {
    setNumber(e.target.value);
  }, []);

  // number 혹은 list가 변경되었을 때만 함수 생성
  // 기존의 number와 list를 조회해서 nextList를 생성하기 때문에
  // 배열 안에 number와 list를 꼭 넣어준다.
  const onInsert = useCallback(
    e => {
      const nextList = list.concat(parseInt(number));
      setList(nextList);
      setNumber('');
    },
    [number, list]
  );

  const avg = useMemo(() => getAverage(list), [list]);

  return (
    <div>
      <input value={number} onChange={onChange} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>Average: </b> {avg}
      </div>
    </div>
  );
};

export default Average;

useCallback의 첫 번째 파라미터에는 생성하고 싶은 함수, 두 번째 파라미터에는 배열을 넣으면 된다. 이 배열에는 어떤 값이 변경되었을 때 함수를 새로 생성해야 하는지를 명시해준다. 빈 배열을 넣었을 경우엔 컴포넌트가 렌더링될 때 단 한번만 함수를 생성한다.

숫자, 문자열, 객체처럼 일반 값을 재사용하려면 useMemo를 사용하고, 함수를 재사용하려면 useCallback을 사용한다.

3. useRef

useRef는 함수형 컴포넌트에서 ref를 쉽게 사용할 수 있도록 해준다. 아래 코드는 등록 버튼을 눌렀을 때 포커스가 input으로 가게 해준다.

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

const getAverage = numbers => {
  console.log('평균값 계산');
  if (numbers.length === 0) return 0;
  const sum = numbers.reduce((a, b) => a + b);
  return sum / numbers.length;
};

const Average = () => {
  const [list, setList] = useState([]);
  const [number, setNumber] = useState('');
  const inputEl = useRef(null);

  // 컴포넌트가 처음 렌더링될 때만 함수 생성
  const onChange = useCallback(e => {
    setNumber(e.target.value);
  }, []);

  // number 혹은 list가 변경되었을 때만 함수 생성
  const onInsert = useCallback(
    e => {
      const nextList = list.concat(parseInt(number));
      setList(nextList);
      setNumber('');
      // useRef를 통해 만든 객체 안의 current 값이 실제 엘리먼트를 가리킨다.
      inputEl.current.focus(); // input box에 focus
    },
    [number, list]
  );

  const avg = useMemo(() => getAverage(list), [list]);

  return (
    <div>
      <input value={number} onChange={onChange} ref={inputEl} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>Average: </b> {avg}
      </div>
    </div>
  );
};

export default Average;

4. custom Hook

여러 컴포넌트에서 비슷한 기능을 공유할 경우, 이를 Hook으로 만들어 로직을 재사용할 수 있다. 아래 코드는 여러 개의 input을 관리하기 위한 useInputs라는 custom Hook이다.

useInputs.js

import { useReducer } from 'react';

function reducer(state, action) {
  return {
    ...state,
    [action.name]: action.value
  };
}

export default function useInputs(initialForm) {
  const [state, dispatch] = useReducer(reducer, initialForm);
  const onChange = e => {
    dispatch(e.target);
  };
  return [state, onChange];
}

Info.js

import React from 'react';
import useInputs from './useInputs';

const Info = () => {
  const [state, onChange] = useInputs({
    name: '',
    email: ''
  });

  const { name, email } = state;

  return (
    <div>
      <div>
        <input name="name" value={name} onChange={onChange} />
        <input name="email" value={email} onChange={onChange} />
      </div>
      <div>
        <p>
          <b>name: </b> {name}
        </p>
        <p>
          <b>email: </b> {email}
        </p>
      </div>
    </div>
  );
};

5. etc

다른 개발자가 만든 Hook도 라이브러리로 설치하여 사용할 수 있다.


reference:

리액트 공식 문서