Hooks는 하위 호환성을 갖고 있습니다. 이번 포스팅에선 우리가 배워볼 hooks의 전반적인 개요를 살펴보겠습니다. 다음 포스팅부터 각각에 대해 자세히 알아볼 것입니다.

State Hook

아래 예시는 counter를 렌더링합니다. 버튼을 클릭하면 값을 올려주죠.

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입니다. 우리는 local state를 더해주기 위해 함수 안에서 이 Hook을 호출해(call) 사용합니다. React는 리렌더링 되어도 이 상태를 유지해줍니다.

useState는 한 쌍의 값을 리턴해줍니다. "현재 상태"와 "상태 업데이트 함수"죠. 후자를 이벤트 헨들러 같은 곳에서 호출하여 사용할 수 있습니다. this.setState와 비슷하지만 기존 상태와 새로운 상태를 합치는 방식으로 동작하진 않습니다.

useState를 사용할 때 넘겨주는 인자는 "초기 상태" 입니다. 위에서 본 예시에선 0이 초기 상태가 됩니다. this.state와 다르게 hooks에서 사용하는 상태는 오브젝트가 아니어도 됩니다(당연히 오브젝트여도 상관 없구요). 초기 상태 인자는 첫 렌더링에서만 사용됩니다.

declaring multiple state variables

하나의 컴포넌트에서 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' }]);
  // ...
}

배열 구조 분해(destructuring)는 우리가 useState를 호출해 정의한 서로 다른 상태에 다른 이름을 붙일 수 있게 도와줍니다. 이름은 useState API와 관련이 없죠. 대신, 만약 useState를 여러 번 사용한다면 리액트는 사용자가 모든 렌더링 마다 같은 순서로 사용할 것이라고 추측합니다. 나중에 왜 이렇게 사용하고 뭐가 좋은지에 대해서 알아보겠습니다.

But what is a Hook?

Hooks는 함수형 컴포넌트에서 리액트 상태와 생명주기 특성을 연동할 수 있게 해줍니다. Hooks는 class안에서 동작하지 않습니다. 대신 class없이 리액트를 사용할 수 있게 해주죠.

리액트는 useState와 같은 몇몇 built-in Hooks를 제공합니다. 또한 리액트 사용자는 자신만의 Hooks를 정의하여 다른 컴포넌트간 상태관련 행동을 재사용할 수도 있습니다. 우리는 built-in Hooks를 먼저 살펴보겠습니다.

Effect Hook

리액트 컴포넌트에서 데이터 가져오기, 구독하기, DOM을 직접 조작하기 등의 작업을 해본 경험이 있을 거예요. 이러한 동작을 "부수 효과(side effect)"라고 부릅니다. 왜냐하면 이런 동작은 다른 컴포넌트에 영향을 미치고 렌더링 동안 완료될 수 없기 때문이죠.

useEffect는 함수형 컴포넌트 안에서 side effect를 사용할 수 있도록 도와줍니다. useEffect는 클래스형 컴포넌트의 componentDidMount, componentDidUpdate, componentWillUnmount등과 같은 목적을 위해 기능합니다. 다만 하나의 API로 통합된 것이죠.

예를 들어, 이 컴포넌트는 리액트가 DOM을 업데이트한 이후 document title을 업데이트 합니다.

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"함수를 사용할 것 입니다. effects는 함수 안에서 정의 되었기 때문에 props, state에 접근할 수 있습니다. 기본적으로 리액트는 첫 렌더링을 포함해 렌더링 할 때 마다 effects를 호출합니다.

Effects는 return하는 함수를 통해 선택적으로 "해제(clean-up)"를 지정해줄 수 있습니다. 예를 들어, 이 컴포넌트는 친구들의 온라인 상태를 구독하는 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를 다시 호출하는 경우나 컴포넌트가 unmount되는 경우 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는 생명주기 메소드가 강제로 각각에 쪼개어서 넣는 것과 다르게 effect를 관련있는 부분끼리(구독과 구독취소 등) 묶어 조직할 수 있도록 도와줍니다.

Rules of Hooks

Hooks는 자바스크립트 함수이지만 두 가지 추가적인 규칙을 내포합니다.

  • 훅은 top level에서 호출해야 합니다. 루프, 조건, 중첩 함수에서 호출해선 안됩니다.
  • 훅은 리액트 함수 컴포넌트에서만 호출해야 합니다. 일반적인 자바스크립트 함수에서 불러선 안됩니다. 자신이 만든 커스텀 훅에서는 호출이 가능합니다.

리액트 팀은 이를 위해 린터 플러그인을 제공합니다. 이런 규칙은 훅이 잘 동작하도록 하는데 필수적입니다.

Building Your Own Hooks

때때로 우리는 상태관련 로직을 컴포넌트 사이에서 재사용 하고 싶을 수 있습니다. 전통적으로 이런 문제를 해결하는 유명한 방법은 higher-order componentsrender props입니다. 그리고 커스텀 훅은 이러한 문제를 트리에 새로운 컴포넌트를 추가하지 않고 해결할 수 있도록 도와줍니다.

위에서 우리는 useState, useEffect를 사용해 친구의 온라인 상태를 확인하는 FriendStatus 컴포넌트를 살펴보았습니다. 이러한 구독 로직을 다른 컴포넌트에서도 사용해야 한다고 해봅시다.

먼저 구독 로직을 useFriendStatus라는 커스텀 훅으로 추출해야 합니다.

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

두 컴포넌트의 상태는 완벽하게 독립적입니다. 훅은 상태 그 자체가 아닌, 상태 관련 로직을 재사용하는 방법입니다. 훅의 호출은 완벽하게 독립된 상태입니다. 따라서 같은 커스텀 훅을 하나의 컴포넌트에서 여러번 호출하는 것 역시 가능합니다.

사실 커스텀 훅은 기능이라기 보단 컨벤션(관례)에 가깝습니다. 이름이 "use"로 시작하면서 다른 커스텀 훅을 호출하는 함수를 커스텀 훅이라고 합니다. useSomething 네이밍 컨벤션은 린트 플러그인에서 버그를 찾을 수 있도록 도와줍니다.

form handling, animation, declarative subscriptions, timers, 그리고 생각하지 못한 많은 경우 등 넓은 범위에 커스텀 훅을 작성할 수 있습니다.

Other Hooks

유용하지만 조금 덜 사용되는 built-in Hooks가 있습니다. 예를들어 useContext는 중첩 없이 React context를 구독할 수 있도록 도와줍니다.

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

useReducer는 복잡한 컴포넌트의 지역 상태를 리듀서를 이용해 관리할 수 있도록 도와줍니다.

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

이것으로 전반적인 Hooks의 개요를 마치게 되었습니다. 다음 포스팅부터 하나하나의 주제를 더 깊게 들어가며 Hooks를 살펴보겠습니다.

profile
웹 개발을 공부하고 있는 윤석주입니다.

0개의 댓글