들어가기 전에

리액트 훅스 공식문서로 배워보자 #2, Hooks 한 눈에 보기!

Hooks는 React 16.8에서 새롭게 추가된 기능입니다. Hooks는 여러분이 state와 다른 리액트 기능들을 클래스를 작성하지 않고도 사용할 수 있게 해줍니다.

Hooks는 이전 버전과 호환이 가능합니다. 이 페이지에서는 숙련된 리액트 사용자들을 위한 Hooks의 개요를 제공합니다. 빠른 페이스의 개요입니다. 만일 헷갈린다면, 아래와 같은 노란색 박스를 찾아보세요. (역자 주: 여기서는 단순히 회색 글씨로 표현될 예정입니다.)

상세한 설명 : 우리가 왜 리액트에 Hooks를 도입했는지 배우고 싶다면, 동기부여 파트를 읽어보세요.

↑↑↑ 각각의 섹션은 위와 같은 방식으로 끝날 것입니다. 링크는 디테일한 설명을 담고 있습니다.

상태 Hook

이 예제는 카운터를 렌더링합니다. 여러분이 버튼을 클릭했을 때, 값이 증가합니다.

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

여기, useStateHook 입니다. (이게 무엇을 의미하는지에 대해 이야기할 것입니다.) 우리는 Hook을 함수 컴포넌트 내부에서 지역 상태를 추가하기 위해 호출합니다. 리액트는 재렌더링 사이에서 이 상태를 유지할 것입니다. useState는 한 쌍을 반환합니다: 현재의 상태 값과 상태를 업데이트할 수 있는 함수. 여러분은 이 함수를 이벤트 핸들러나 다른 곳에서 호출할 수 있습니다. 이 함수는 이전 상태와 새로운 상태를 함께 병합하지 않는다는 것만 빼면 클래스에서 this.setState와 비슷합니다. (나중에 State Hook 사용하기에서 useStatethis.state를 비교할 것입니다.)

useState의 유일한 인자 값은 초기 값입니다. 위의 예제에서는, 0입니다. 왜냐하면 우리 카운터는 0부터 시작하기 때문입니다. 알아두셔야 할 것은 this.state와 다르게, 상태가 반드시 객체일 필요가 없다는 것입니다. 여러분이 원하면 객체를 사용할 수도 있습니다. 초기 상태 인자는 오직 첫 렌더링 시에만 사용됩니다.

여러개의 상태 변수 선언하기

단일 컴포넌트에서 우리는 상태 Hook을 한번 이상 사용할 수 있습니다.

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

배열 구조 분해 할당은 우리가 useState를 호출함으로써 선언한 상태 변수에게 각각 다른 이름을 줄 수 있도록 해줬습니다. 이 이름들은 useState의 일부가 아닙니다. 대신에, 리액트는 여러분이 useState를 많이 호출했다면, 여러분이 매 렌더링동안 같은 순서로 렌더링한다는 것을 가정합니다. 우리는 왜 이렇게 작동하는지, 이런 작동방식이 언제 도움이 되는지 나중에 더 알아보도록 합시다.

하지만 왜 Hook일까?

Hooks는 함수형 컴포넌트로부터 리액트 상태와 라이프사이클 특성들을 끄집어낼 수 있게("hook into") 해주는 함수입니다. Hooks는 클래스 내부에서 동작하지 않습니다. Hooks는 여러분이 리액트를 클래스 없이 사용할 수 있게 해줍니다. (우리는 여러분이 밤을 새워 이미 존재하는 컴포넌트들을 재작성하는 것을 권장하지 않습니다. 하지만 여러분은 Hooks를 여러분이 원하는 새로운 컴포넌트에서 사용할 수 있습니다.)

리액트는 useState와 같은 몇가지 내장된 Hooks를 제공합니다. 여러분은 또한 다른 컴포넌트들 사이에 상태가 있는 행동을 재사용하기 위해 여러분만의 Hooks를 만들 수 있습니다. 우리는 내장 Hooks 먼저 살펴볼 것입니다.

상세한 설명 : 여러분은 다음 페이지에서 State Hook에 대해 더 상세히 배울 수 있습니다. State Hook 사용하기.

⚡️ 이펙트 Hook

여러분은 리액트 컴포넌트에서 데이터 가져오기(fetching), 구독(subscription), 수동으로 DOM 바꾸기 등을 수행해왔을 것입니다. 우리는 이러한 동작들을 "side effects" (또는 짧게 "effects") 라고 부릅니다. 왜냐하면 그들은 다른 컴포넌트에 영향을 줄 수 있고 렌더링 도중에 완료될 수 없기 때문입니다.

이펙트 Hook, useEffect는 함수형 컴포넌트에서 side effect를 수행하는 능력을 추가합니다. 이 Hook은 리액트 클래스에서 componentDidMount, componentDidUpdate 그리고 componentWillUnmount 와 같은 목적을 지닙니다. 하지만 하나의 API로 통합되었습니다. (우리는 useEffect와 이러한 메소드들을 비교하는 예제를 Effect Hook 사용하기 파트에서 보여줄 것입니다.

예를 들면, 이 컴포넌트는 리액트가 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를 호출할 때, 여러분은 리액트에게 쏟아지는 DOM의 변화 이후에 여러분의 "effect" 함수를 실행하라고 말하는 것입니다. 이펙트들은 컴포넌트 내부에 선언되어 있습니다. 그래서 이펙트들은 props와 state에 접근할 수 있습니다. 기본 값으로, 리액트는 처음 렌더링을 포함하여 매 렌더링마다 이펙트를 실행시킵니다. (클래스의 라이프사이클과 비교하여 어떻게 다른지에 대해서는 나중에 이펙트 훅 사용하기 섹션에서 더 이야기 해보도록 할 것입니다.)

이펙트는 선택적으로 이펙트 이후에 함수를 반환함으로써 "clean up(이펙트를 더이상 수행하지 않는 행위)"을 명시할 수도 있습니다. 예를 들면, 이 컴포넌트는 친구의 온라인 상태를 구독하기 위해서 이펙트를 사용합니다. 그리고 구독을 해지함으로써 clean up 합니다.

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

이 예제에서, 리액트는 컴포넌트가 언마운트될 때, 우리의 ChatAPI로부터 구독해지를 할 것입니다. 이 뿐만 아니라 차후의 렌더링 때문에 다시 이펙트를 실행하려 하기 전에도 구독은 해지됩니다. (만일 여러분이 원한다면, 우리가 ChatAPI로 넘겨준props.friend.id가 변하지 않았다면, 리액트에게 재구독을 스킵하도록 할 수 있는 방법이 있습니다.)

단순히 useState와 같이, 여러분은 한개 이상의 effect를 하나의 컴포넌트에서 사용할 수 있습니다:

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

Hooks는 라이프사이클 메소드를 기반으로 강제로 코드를 나누게 하기 보다는 관련된 코드 조각들(이를테면 구독을 등록, 해지하는 것과 같은)로 여러분이 한 컴포넌트 내부의 side-effect들을 정리하도록 합니다.

상세한 설명 : 여러분은 useEffect에 대해 다음 페이지 Effect Hook 사용하기에서 더 많이 배울 수 있습니다.

✌️ Hooks의 규칙

Hooks는 자바스크립트 함수입니다. 하지만 2가지 추가적인 규칙을 강요합니다.

  • 가장 상위 레벨에서만 Hooks를 호출하세요. 내부 반복문, 조건문, 중첩된 함수 안에서 Hooks를 호출하지 마세요.
  • Hooks를 리액트 함수 컴포넌트에서만 호출하세요. Hooks를 일반적인 자바스크립트 함수에서 호출하지 마세요. (사실 Hooks를 호출하기 위한 유효한 위치가 한 군데 더 있습니다. 그 곳은 바로 여러분의 커스텀 Hooks입니다. 잠시 후에 커스텀 Hooks에 대해 배워볼 것입니다.)

우리는 이러한 규칙들을 자동으로 지키게 만들기 위해 린트 플러그인을 제공합니다. 우리는 이러한 규칙들이 처음에는 혼란 스럽게 만들고 어떠한 구현상의 제한을 주는 것과 같이 보일 수 있다는 것을 이해하지만, Hooks를 잘 작성하기 위해서는 필수적입니다.

상세한 설명 : 여러분은 다음 페이지 Hooks의 규칙에서 더 많은 이러한 규칙들에 대해서 배울 수 있습니다.

💡 나만의 Hooks 만들기

때때로, 우리는 컴포넌트들 사이에서 어떠한 상태가 있는 로직들을 재사용하기를 원합니다. 관습적으로, 이 문제에 대해서 두 개의 인기있는 해결책이 있었습니다: higher-order component (HOC) 그리고 render props가 있었습니다. 커스텀 Hooks는 여러분이 컴포넌트 사이에서 로직 재사용을 할 수 있게 해줍니다. 하지만 여러분의 트리에 더 많은 컴포넌트를 추가하지 않습니다.

이 페이지의 앞에서, 우리는 friend의 온라인 상태를 구독하기 위해서 useStateuseEffect Hooks를 호출하는 FriendStatus 컴포넌트를 소개했습니다. 우리가 이 구독 로직을 또 다른 컴포넌트에서 재사용하길 원한다고 해봅시다.

먼저, 우리는 이 로직을 useFriendStatus라 불리는 커스텀 Hooks로 추출할 것입니다.

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

이 컴포넌트는 friendID를 인자로 받습니다. 그리고 우리의 친구가 온라인 상태인지를 반환합니다.

이제 우리는 이 로직을 두 개의 컴포넌트 모두에서 사용할 수 있습니다.

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

이 컴포넌트들의 상태는 완전히 독립적입니다. Hooks는 상태 그 자체가 아니라 상태가 있는 로직을 재사용하는 방법 중 하나입니다. 사실, 각각의 Hook 호출은 완전히 독립된 상태를 갖고 있습니다. 그래서 여러분은 같은 커스텀 Hook도 한 컴포넌트에서 두 번이상 사용할 수 있습니다.

커스텀 Hook들은 기능이라기보다는 컨벤션입니다. 만일 함수의 이름이 use로 시작하고 이 함수가 다른 Hook들을 호출하면, 우리는 이 Hook을 커스텀 Hook이라고 부릅니다. useSomething 네이밍 컨벤션은 우리의 linter 플러그인이 Hooks를 사용한 코드 내부에서 버그를 찾을 수 있게 해줍니다.

여러분은 폼 다루기, 애니메이션, 선언형 구독, 타이머 그리고 아마 우리가 고려하지 못한 더 많은 use case를 커버하는 커스텀 Hook을 작성할 수 있습니다. 우리는 리액트 커뮤니티에서 어떠한 커스텀 Hook들을 만들어낼지 굉장히 흥분됩니다.

상세한 설명 : Building Your Own Hooks 페이지에서 custom Hook을 만드는 것에 대해 더 배워볼 수 있습니다.

🔌 다른 Hook들

여러분들이 유용하다고 생각할 수도 있지만 일반적으로 덜 쓰이는 빌트인 Hook들이 있습니다. 예를 들면 useContext는 여러분이 리액트 컨텍스트를 nesting도입 없이 구독할 수 있게 해줍니다.

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

그리고 useReducer는 리듀서를 사용하는 복잡한 컴포넌트들의 지역 상태를 관리하게 해줍니다.

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

상세한 설명 : Hooks API Reference에서 빌트인 Hook에 대해 더 많이 배워볼 수 있습니다.

다음 단계

휴, 정말 빠르게 왔습니다! 만일 위에서 배운 어떠한 것들이 말이 되지 않았거나 더 상세히 배우고 싶다면, 다음 페이지들을 읽으시면 됩니다. State Hook 문서와 함께 시작해보세요.

또한 Hooks API Reference 그리고 Hooks FAQ도 확인해보세요.

마지막으로, introduction page를 까먹지마세요. 왜 우리가 Hooks를 추가했는지 그리고 우리가 어떻게 클래스의 재작성 없이 Hook도 함께 사용하기 시작할 것인지에 대해 설명합니다.