Front End/React

React Hooks

Hook이란?

React의 공식문서에서는 Hook을 아래와 같이 설명한다.

Hook은 React 16.8에 새로 추가된 기능입니다. Hook은 class를 작성하지 않고도 state와 다른 React의 기능들을 사용할 수 있게 해줍니다.

Hook은 다르게 말하면 함수형 컴포넌트에서 상태 값 및 다른 여러 기능을 사용하기 편리하게 해주는 메소드를 의미한다. Hook은 class가 아닌 function으로만 React를 사용할 수 있게 해주는 것이기 때문에 클래스형 컴포넌트에서는 동작하지 않는다.

Hook 사용 규칙

  1. 리액트 함수의 최상위에서만 호출해야 한다.

    반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하면 예상한 대로 동작하지 않을 수 있다. 컴포넌트 안에는 useState나 useEffect같은 Hook들이 여러번 사용될 수 있는데, React는 이 Hook을 호출되는 순서대로 저장한다. 하지만 조건문, 반복문 안에서 Hook을 호출하게 되면 호출되는 순서대로 저장하기 어려워져 예기치 못한 버그를 초래할 수 있다.

    useState를 조건문 내에서 호출하면 에러가 발생한다.
  1. React 함수 내에서만 사용되어야 한다.

    리액트 함수형 컴포넌트나 Custom Hook이 아닌 다른 일반 JavaScript 함수 내에서 호출하면 안된다. 애초에 Hook은 React의 함수 컴포넌트 내에서 사용되도록 만들어진 메서드이기 때문에 근본적으로 일반 JavaScript 함수 내에서는 정상적으로 동작하지 않는다.

    window.onload라는 함수에서 useEffect를 호출하면 에러가 발생한다.

useMemo

useMemo은 특정 값(value)를 재사용하고자 할 때 사용하는 Hook이며, 잦은 리렌더링으로 인한 성능 저하를 방지하기 위해 사용한다.

function Calculator({value}){
	const result = calculate(value);
	return (
		<>
      <div>
					{result}
      </div>
	  </>;
	)
}

해당 컴포넌트는 props로 넘어온 value 값을 calculate라는 함수에 인자로 넘겨 result 값을 구한 후 div 엘리먼트로 출력하고 있다. 만약 여기서 calculate가 내부적으로 복잡한 연산을 하는 함수라 값을 반환하는 데에 몇 초 이상 걸린다고 가정해보자. 그렇다면 해당 컴포넌트는 렌더링을 할 때마다 이 함수를 계속 호출하게 되고, 그 때마다 시간이 몇 초 이상 소요된다. 이 몇 초의 지연은 렌더링에도 영향을 미칠 것이다.

import { useMemo } from "react";

function Calculator({value}){
	const result = useMemo(() => calculate(value), [value])
	return (
		<>
      <div>
					{result}
      </div>
	  </>;
	)
}

위의 코드에서 만약 value값이 계속 바뀌는게 아니라고 가정한다면, 함수 실행의 결과 값을 어딘가에 저장해뒀다가 다시 꺼내 쓸 수 있다면 굳이 calculate 함수를 호출 할 필요가 없을 것이다. 여기서 useMemo Hook을 사용할 수 있다. useMemo를 사용하여 calculate를 실행하는 콜백함수를 인자로 전달하면, 이전에 구축된 렌더링과 새로 구축되는 렌더링을 비교해 value값이 동일할 경우 이전 렌더링의 value값을 그대로 재활용 할 수 있게 된다. 이는 메모이제이션(Memoization)개념과 긴밀한 관계가 있다.

Memoization

메모이제이션(Memoization)은 알고리즘에서 자주 나오는 개념이며, 기존에 수행한 연산의 결과값을 메모리에 저장해두고, 동일한 입력이 들어오면 재활용하는 프로그래밍 기법을 말한다. 이 메모이제이션을 적절히 사용하면 굳이 중복 연산을 필요로 하지 않기 때문에 앱의 성능을 최적화 할 수 있다.

useMemo는 바로 이 개념을 이용해 복잡한 연산의 중복을 피하고 React 앱의 성능을 최적화한다. 직접 메모이제이션 개념을 이용해 로직을 구현할 수도 있지만, useMemo Hook을 호출하면 이런 로직의 구현을 대신해주기 때문에 훨씬 간편하다.

예제를 통해 살펴보기

import React, { useState, useMemo } from "react";
import "./styles.css";
import { add } from "./add";

export default function App() {
  const [name, setName] = useState("");
  const [val1, setVal1] = useState(0);
  const [val2, setVal2] = useState(0);
  // const answer = add(val1, val2);
  const answer = useMemo(() => add(val1, val2), [val1, val2]);

  return (
    <div>
      <input
        className="name-input"
        placeholder="이름을 입력해주세요"
        value={name}
        type="text"
        onChange={(e) => setName(e.target.value)}
      />
      <input
        className="value-input"
        placeholder="숫자를 입력해주세요"
        value={val1}
        type="number"
        onChange={(e) => setVal1(Number(e.target.value))}
      />
      <input
        className="value-input"
        placeholder="숫자를 입력해주세요"
        value={val2}
        type="number"
        onChange={(e) => setVal2(Number(e.target.value))}
      />
      <div>{answer}</div>
    </div>
  );
}

useMemo를 사용하지 않은 상태에서는 name의 상태 변화가 생기면 그 때마다 컴포넌트를 새로 렌더링 하게 되고, 이로 인해 add함수의 연산이 발생하게 된다. 이 때 useMemo를 사용하면 name의 상태가 변해도 val1val2의 값은 이미 저장되어 있기 때문에 add함수는 실행되지 않는다.

useCallback

useCallback 또한 useMemo와 마찬가지로 메모이제이션 기법을 이용한 Hook이다. useMemo는 값의 재사용을 위해 사용한다면, useCallback은 함수의 재사용을 위해 사용하는 Hook이다.

function Calculator({x, y}){
	const add = () => x + y;
	return {
		<>
      <div>
					{add()}
      </div>
	  </>;
	)
}

해당 컴포넌트 내에는 add라는 함수가 선언이 되어 있다. 이 add 함수는 props로 넘어온 xy 값을 더해 <div> 태그에 값을 출력하고 있다. 이 함수는 해당 컴포넌트가 렌더링 될 때마다 새롭게 만들어진다. 해당 컴포넌트가 리렌더링 되더라도 그 함수가 의존하고 있는 xy가 바뀌지 않는다고 가정해본다면, 함수 또한 useMemo처럼 어딘가에 저장해뒀다가 다서 꺼내 쓸 수 있을 것이다. 이 때 useCallback Hooks를 사용하면 해당 함수가 의존하는 값들이 바뀌지 않는 한 기존 함수를 계속 반환한다. 즉, xy값이 같다면 다음 렌더링 때 이 함수를 다시 사용한다.

import React, { useCallback } from "react";
function Calculator({x, y}){
	const add = useCallback(() => x + y, [x, y]);
	return {
		<>
      <div>
					{add()}
      </div>
	  </>;
	)
}

사실 useCallback만 사용해서는 useMemo에 비해 눈에 띌 정도로 최적화를 느낄 수 없다. 왜냐하면 함수를 호출 하지 않는 것이 아니라, 그저 메모리 어딘가에 함수를 꺼내서 호출하기 때문이다. 따라서 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해 useCallback을 사용하는 것은 큰 의미가 없거나 오히려 손해일 때도 있다. 하지만 자식 컴포넌트의 props로 함수를 전달해줄 때에는 사용하기 좋다.

useCallback과 참조 동등성

useCallback은 참조 동등성에 의존한다. React는 JavaScript 언어로 만들어진 라이브러리이기 때문에 기본적으로 JavaScript 문법을 따라간다. JavaScript에서 함수는 객체이므로, 메모리에 값의 주소를 저장하기 때문에 반환하는 값이 같을지라도 다른 변수에 저장했다면 이는 일치연산자로 비교하면 false가 출력된다.

function doubleFactory(){
    return (a) => 2 * a;
}
  
const double1 = doubleFactory();
const double2 = doubleFactory();
  
double1(8); // 16
double2(8); // 16
  
double1 === double2;  // false
double1 === double1;  // true

이는 React도 동일하다. React는 리렌더링 시 함수를 새로 만들어 호출하고, 이 함수는 기존의 함수와 같은 함수가 아니다. 하지만 useCallback을 이용해 함수 자체를 저장해 다시 사용하면 함수의 메모리 주소 값을 저장했다가 다시 사용하는 것이기 때문에 React 컴포넌트 함수 내에서 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제를 막을 수 있다.

Custom Hooks

Custom Hooks는 말 그대로 개발자가 스스로 커스텀한 훅을 의미하며 이를 이용하면 반복되는 로직을 함수로 뽑아내어 재사용 할 수 있다. 여러 url을 fetch할 때, 여러 input에 의한 상태 변경 등 반복되는 로직을 동일한 함수에서 작동하게 하기 위해 주로 사용한다. 이를 이용하면 상태관리 로직의 재활용이 가능하고 클래스 컴포넌트보다 적은 양의 코드로 동일한 로직을 구현할 수 있으며 함수형으로 작성하기 때문에 보다 명료하다는 장점이 있다.

//FriendStatus : 친구가 online인지 offline인지 return하는 컴포넌트
function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

//FriendListItem : 친구가 online일 때 초록색으로 표시하는 컴포넌트
function FriendListItem(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

위의 두 컴포넌트(FriendStatus, FriendListItem)는 모두 완벽히 동일하게 쓰는 로직이 존재한다. 이 로직을 Custom Hook을 사용해 분리하여 만들 수 있다. 이 때, 일반 함수 내부에서는 Hook을 사용할 수 없지만 Custom Hook에서는 Hook 내부에 useState와 같은 React 내장 Hook을 사용해 작성할 수 있다.

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

이처럼 Custom Hook을 정의할 때는 일종의 규칙이 필요하다.

  • Custom Hook을 정의할 때는 함수 이름 앞에 use를 붙여야 한다.
  • 대개의 경우 프로젝트 내의 /hooks 디렉토리에 Custom Hook을 위치시킨다.
  • Custom Hook으로 만들 때 함수는 조건부 함수가 아니어야 한다. 즉, return하는 값은 조건부여선 안된다. ⇒ 위의 예제에서는 이 규칙을 지키기 위해 온라인 상태의 여부를 boolean 타입으로 반환한다.

위의 컴포넌트(FriendStatus, FriendListItem)에 Custom Hook을 적용한 결과는 아래와 같다. 이 때, 같은 Custom Hook을 사용했다 해서 두 개의 컴포넌트가 같은 state를 공유하는 것은 아니다. 로직만 공유할 뿐, state는 컴포넌트 내에서 독립적으로 정의 되어 있다.

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );

Custom Hook 사용 예제 : useFetch, useInput

const useFetch = ( initialUrl:string ) => {
	const [url, setUrl] = useState(initialUrl);
	const [value, setValue] = useState('');

	const fetchData = () => axios.get(url).then(({data}) => setValue(data));	

	useEffect(() => {
		fetchData();
	},[url]);

	return [value];
};

export default useFetch;
import { useState, useCallback } from 'react';

// initialForm : input에서 받아온 정보를 담을 객체
// input의 속성 중 name을 사용해 구분하고 key와 값의 타입을 미리 지정
function useInputs(initialForm) {
  const [form, setForm] = useState(initialForm);
  // change
  const onChange = useCallback(e => {
    const { name, value } = e.target;
    setForm(form => ({ ...form, [name]: value }));
  }, []);
  const reset = useCallback(() => setForm(initialForm), [initialForm]);
  return [form, onChange, reset];
}

export default useInputs;


Uploaded by N2T