안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

리액트 (React)

[React] 공식 문서 요약 - Hook 살펴 보기

피그브라더 2020. 9. 11. 10:57
 

Hooks at a Glance – React

A JavaScript library for building user interfaces

reactjs.org

* React 공식 문서를 꼼꼼히 읽으면서 개인적으로 요약한 내용입니다. 잘못된 내용이 있다면 지적 부탁드립니다.

 

1. State Hook : useState()

  • State Hook은 함수형 컴포넌트가 클래스형 컴포넌트처럼 지역 상태를 가질 수 있도록 한다.
  • 자세한 설명은 'State Hook 사용하기' 포스팅을 참조하자.

 

다음은 버튼을 클릭할 때마다 화면에 표시되는 숫자가 1씩 증가하는 카운터를 렌더링 하는 예시이다.

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

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

 

여기서 useState() 함수가 바로 Hook이다. useState() 함수를 이용하면 함수형 컴포넌트에 지역 상태를 저장할 수 있다. React는 이 상태 값을 렌더링 간에 유지한다. useState() 함수가 반환하는 배열의 첫 번째 요소는 해당 상태의 값, 두 번째 요소는 해당 상태를 설정하기 위한 함수이다. 그리고 useState() 함수의 인자로 전달하는 값은 해당 상태의 초깃값으로, 첫 번째 렌더링 시에만 사용된다. 참고로, 클래스형 컴포넌트와 달리 여기서의 상태 값은 반드시 객체일 필요가 없으며 어떠한 값이든 가능하다.

 

여러 개의 state 변수를 선언하려면 다음과 같이 useState() 함수를 여러 번 호출하면 된다.

function ExampleWithManyStates() {
  // Declare multiple state variables!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  // ...
}

 

배열 구조 분해(Destructing) 문법은 useState() 함수에 의해 반환된 각각의 상태 값에 서로 다른 변수명을 할당할 수 있도록 해준다. 그런데 이처럼 useState() 함수를 여러 번 호출하는 경우 그 호출 순서는 반드시 매 렌더링마다 같게 해야 한다. 이 규칙을 지키지 않으면 각각의 상태가 렌더링 간에 제대로 유지되지 못한다. 이와 관련해서는 'Hook 규칙' 포스팅을 참조하자.

 

2. Effect Hook : useEffect()

  • Effect Hook은 함수형 컴포넌트가 클래스형 컴포넌트처럼 사이드 이펙트를 수행할 수 있도록 한다.
  • 자세한 설명은 'Effect Hook 사용하기' 포스팅을 참조하자.

 

클래스형 컴포넌트에서는 데이터를 가져오거나, 어떠한 이벤트를 구독하거나, DOM을 직접 조작하는 등의 작업이 가능하다. 이러한 작업들은 자기 자신뿐 아니라 외부에도 영향을 준다는 의미에서 사이드 이펙트(Side Effect) 또는 간단히 이펙트(Effect)라고 부른다. 이때 useEffect() 함수를 사용하면 함수형 컴포넌트에서도 이러한 이펙트를 수행할 수 있도록 해준다. 그리고 일반적으로 클래스형 컴포넌트에서는 이펙트를 componentDidMount() 메소드, componentDidUpdate() 메소드, 혹은 componentWillUnmount() 메소드에서 구현하는데, 같은 맥락의 기능을 여러 개의 메소드에 나눠서 작성하는 것은 유지보수도 힘들고 가독성도 떨어진다. useEffect() 함수를 사용하면 위 세 개의 생명주기 메소드에서 수행할 수 있는 유사한 기능들을 하나의 함수로 묶어서 처리할 수 있게 된다.

 

다음은 React가 DOM을 마운트/업데이트한 후에 문서의 제목을 바꾸도록 하는 컴포넌트이다.

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

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

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

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

 

useEffect() 함수의 인자로 전달되는 함수(이하 effect 함수)는 React가 DOM을 마운트/업데이트할 때마다 실행되며, 컴포넌트 안에 정의되어 있기에 컴포넌트의 props 및 state에 접근할 수 있다. 클래스형 컴포넌트로 치면 componentDidMount() 메소드 및 componentDidUpdate() 메소드에 해당 기능을 구현한 것과 동일하다. 다만 차이점은 effect 함수가 항상 같을 필요가 없기 때문에 매 렌더링마다 다른 이펙트를 수행할 수 있다는 점이다.

 

만약 '해제'할 필요가 있는 이펙트라면 effect 함수가 이펙트의 해제를 위한 함수를 반환하면 된다. 다음은 매 렌더링마다 effect 함수가 구독을 위한 이펙트를 수행하고 언마운트 될 때 구독 해제를 위해 호출해야 할 함수를 설정하도록 한 예시이다.

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

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

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

 

참고로 이펙트를 해제하는 함수는 컴포넌트가 언마운트 될 때만 호출되는 것이 아니다. 매 렌더링마다 새로운 effect 함수를 실행하기 직전에도 호출된다. 물론 성능 상 이유로 매 렌더링마다 기존의 이펙트를 해제하고 새로운 이펙트를 수행하는 것을 방지하는 것도 가능하다. 이와 관련해서는 'Effect Hook 사용하기' 포스팅을 참조하자.

 

useState() 함수와 마찬가지로, 다음과 같이 useEffect() 함수를 여러 번 호출하는 것도 가능하다.

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
  // ...
}

 

Hook을 이용하면 위와 같이 서로 관련이 있는 로직들을 한 곳에 모아서 작성하는 것이 가능해진다. 클래스형 컴포넌트를 사용한다면 서로 다른 로직이라 하더라도 동일한 생명주기 메소드에 들어가야 할 것이고, 서로 관련이 있는 로직이라 하더라도 몇 개의 생명주기 메소드에 나눠서 들어가야 할 것이다. 참고로 이처럼 useEffect() 함수를 여러 번 호출하는 경우에도 마찬가지로 그 호출 순서는 반드시 매 렌더링마다 같게 해야 한다. 이와 관련해서는 'Hook 규칙' 포스팅을 참조하자.

 

3. Hook 규칙

Hook은 단순한 JavaScript 함수이지만, 다음과 같은 규칙을 반드시 준수해야 한다. 자세한 내용은 'Hook 규칙' 포스팅을 참조하자.

 

  • 최상위(Top Level)에서만 Hook을 호출해야 한다. 즉 반복문, 조건문, 중첩된 함수 내에서 호출하면 안 된다.
  • 함수형 컴포넌트 혹은 커스텀 Hook에서만 Hook을 호출해야 한다. 즉 일반 JavaScript 함수에서는 호출하면 안 된다.

 

※ 이와 같은 규칙들을 강제하고 싶다면 linter 플러그인을 사용하도록 하자. (create-react-app 프로젝트의 경우 이미 설정됨)

 

4. 나만의 Hook 만들기 (커스텀 Hook)

기존의 React에서는 상태 관련 로직을 컴포넌트들이 공유하기 위해서는 고차 컴포넌트 혹은 Render Props 패턴을 사용해야 했다. 그러나 Hook이 도입된 이후에는 커스텀 Hook을 사용함으로써 컴포넌트 트리에 새로운 컴포넌트를 추가하지 않고도 상태 관련 로직을 재사용 및 공유할 수 있게 되었다. 자세한 내용은 '나만의 Hook 만들기' 포스팅을 참조하기 바란다.

 

다음은 props로 전달받은 ID에 해당하는 친구의 접속 상태(isOnline)를 구독하는 로직을 커스텀 Hook으로 추출한 예시이다.

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

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

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

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

  return isOnline;
}

 

이제 우리는 위에서 정의한 커스텀 Hook을 여러 컴포넌트에서 재사용할 수 있다.

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>
  );
}

 

커스텀 Hook은 '로직'을 재활용하는 것일 뿐, 상태 값을 재활용하는 것은 아니다. 즉 같은 커스텀 Hook을 호출한다고 해도 각각 독립적인 상태를 가지게 된다. 이는 한 컴포넌트 안에서 해당 커스텀 Hook을 여러 번 호출할 때도 마찬가지이다.

 

커스텀 Hook은 특별한 기능이라기보다는 기본적으로 Hook의 디자인을 따르는 관습일 뿐이다. 이름이 use로 시작하거나 내부에서 다른 Hook을 호출한다면 그 함수를 커스텀 Hook이라고 부를 수 있다. 이러한 이름 규칙은 linter 플러그인이 Hook을 제대로 인식하고 버그를 찾을 수 있게 해주므로 최대한 준수하는 것을 권장한다.

 

폼 핸들링, 애니메이션, 구독, 타이머 등 많은 경우에 커스텀 Hook이 사용될 수 있다. 앞으로도 React 생태계에 다양한 종류의 커스텀 Hook이 제작되고 배포가 될 것이다. 실제로 지금도 많은 라이브러리에서 커스텀 Hook API를 제공하고 있다(EX. Redux).

 

5. 다른 내장 Hook

useState(), useEffect() 외에도 종종 사용할 수 있는 내장 Hook이 몇 개 더 있다. 예를 들어 다음 예시에서의 useContext() 함수는 컴포넌트를 굳이 중첩하지 않고도 Context 값을 구독할 수 있도록 해준다.

function Example() {
  const locale = useContext(LocaleContext);
  const theme = useContext(ThemeContext);
  // ...
}

 

그리고 useReducer() 함수는 복잡한 컴포넌트들의 state를 리듀서로 관리할 수 있도록 해준다. 리듀서는 현재 상태 값과 액션을 인자로 전달받아서 다음 상태 값을 반환하는 함수를 의미한다. Redux 라이브러리를 사용해본 적 있다면 익숙한 개념일 것이다.

function Todos() {
  const [todos, dispatch] = useReducer(todosReducer);
  // ...
}

 

이밖에 다른 내장 Hook에 대해 알아보고 싶다면 React에서 제공하는 공식 Hook API 문서를 참조하기 바란다.