본문 바로가기

React/공식문서(구버전)

[리액트 공식문서] HOOK 1 ~ 4

1. Hook 소개

- 우리가 왜 Hook을 React에 추가했는지, 그리고 Hook이 애플리케이션을 작성하는 데 어떠한 도움을 주는지 알아보자.

1) 동기

- 컴포넌트 사이에서 상태 로직을 재사용하기 어렵다. > Hook을 사용하면 컴포넌트로부터 상태 관련 로직을 추상화 할 수 있다. 

- 생명주기 메서드를 기반으로 쪼개진 복잡한 컴포넌트들은 이해하기 어렵다. > Hook을 통해 서로 비슷한 것을 하는 작은 함수의 묶음으로 컴포넌트를 나누는 방법을 사용할 수 있다. 

- React 에서의 Class 사용을 위해서는 JavaScript의 this 키워드가 어떻게 작동하는지 알아야만 한다.  Class는 사람과 기계를 혼동시킨다. > Hook은 Class 없이 React 기능들을 사용하는 방법들을 제시한다.

2. Hook 개요

0) Hook 이란?

- Hook은 함수 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 "연동(hook into)"할 수 있게 해주는 함수다.

- Hook은 Class 안에서 동작하지 않는다. 대신 Class 없이 React를 사용할 수 있게 해주는 것이다.

1) State Hook

import React, { useState } from "react";

function Example() {
  // "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이다. 

- Hook을 호출해 함수 컴포넌트 안에 state를 추가했다. 이 state는 컴포넌트가 다시 렌더링 되어도 그대로 유지된다. 

- useState는 현재의 state 값과 이 값을 업데이트 하는 함수를 쌍으로 제공한다. 우리는 이 함수를 이벤트 핸들러나 다른 곳에서 호출할 수 있다. 

- useState는 인자로 초기 state 값을 하나 받는다. useState Hook의 state는 객체일 필요가 없다. 물론 원한다면 그렇게도 가능하다. 이 초기 값은 첫 번째 렌더링에만 딱 한 번 사용된다.

 

function ExampleWithManyStates() {
  // 상태 변수를 여러개 선언했다.
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState("banana");
  const [todos, setTodos] = useState({ text: "Learn Hooks" });
  // ...
}

 

- 하나의 컴포넌트 내에서 State Hook을 여러 개 사용할 수도 있다.

- 배열 구조 분해(destructuring) 문법은 useState로 호출된 state 변수들을 다른 변수명으로 할당할 수 있게 해준다. 

2) Effect Hook

- React 컴포넌트 안에서 데이터를 가져오거나 구독하고, DOM을 직접 조작하는 작업, 이 모든 동작을 "side effects"(또는 짧게 "effects")라고 한다. 왜냐하면 이것은 다른 컴포넌트에 영향을 줄 수도 있고, 렌더링 과정에서는 구현할 수 없는 작업이기 때문이다.

- Effect Hook, 즉 useEffect는 함수 컴포넌트 내에서 이런 side effects를 수행할 수 있게 해준다.

 

import { useState, useEffect } from "react";
function Example() {
  const [count, setCount] = useState(0);
  
  // componentDidMount, componentDidUpdate 와 비슷하다
  useEffect(() => {
    // 브라우저 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를 사용하면, React는 DOM을 바꾼 뒤에 "effect" 함수를 실행할 것이다. 

- Effects는 컴포넌트 안에 선언되었기 때문에 props와 state에 접근할 수 있다. 기본적으로 React는 첫 번째 렌더링도 포함해서 매 렌더링 이후에 effects를 실행한다.

- Effect를 "해제"할 필요가 있다면, 해제하는 함수를 반환해주면 된다. 이는 선택적이다.

- useState와 마찬가지로 컴포넌트 내에서 여러 개의 effect를 사용할 수 있다.

3) Hook 사용 규칙

- Hook은 그냥 JavaScript 함수이지만, 두 가지 규칙을 준수해야 한다.

- 1. 최상위(at the top loevel)에서만 Hook을 호출해야 한다. 반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하지 말자.

- 2. React 함수 컴포넌트 내에서만 Hook을 호출해야 한다. 일반 JavaScript 함수에서는 Hook을 호출해서는 안된다.

4) 나만의 Hook 만들기

- 가깜 상태 관련 로직을 컴포넌트 간에 재사용하고 싶은 경우가 생긴다. 이 문제를 해결하기 위한 전통적인 방법이 두 가지가 있었는데, higher-order components와 render props가 바로 그것이다. Custom Hook은 이들 둘과는 달리 컴포넌트 트리에 새 컴포넌트를 추가하지 않고도 이것을 가능하게 해준다. 

- 친구의 접속 상태를 구독하기 위해서 useState와 useEffect Hook을 사용한 FriendStatus 컴포넌트 예시를 보자. 이 로직을 다른 컴포넌트에서도 재사용하고 싶다고 가정을 해보자. 먼저, 이 로직을 useFriendStatus라는 custom Hook으로 뽑아낸다.

 

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

function useFriendStatus(friendID) {
  const [isOline, setIsOnline] = useState(null);
  
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
  
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    }
  });
  
  return isOnline;
}

 

- 위 Hook은 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>
  )
}

 

- 위 예제에서 각 컴포넌트의 state는 완전히 독립적이다. Hook은 state 그 자체가 아니라, 상태 관련 로직을 재사용하는 방법이다. 실제로 각각의 Hook 호출은 완전히 독립된 state를 가진다. 

- 폼 핸들링, 애니메이션, 선언적 구독, 타이머 등 많은 경우에 custom Hook을 사용할 수 있다. 

3. Using the state Hook

1) state 변수 선언하기

- 함수 컴포넌트는 this를 가질 수 없기 때문에 this.state를 할당하거나 읽을 수 없다. 대신 useState Hook을 직접 컴포넌트에 호출한다.

 

import React, { useState } from "react";

function Example() {
  // 새로운 state 변수를 선어하고, 이것을 count라 부르자.
  const [count, setCount] = useState(0);
}

 

- useState를 호출하는 것을 무엇을 하는 걸까? "state 변수"를 선언할 수 있다. 위에 선언한 변수는 count라고 부르지만 banana처럼 아무 이름으로 지어도 된다. state 변수는 React에 의해 사라지지 않는다.

- useState의 인자로 무엇을 넘겨주어야 할까? useState( )Hook의 인자로 넘겨주는 값은 state의 초기 값이다. 함수 컴포넌트의 state는 클래스와 달리 객체일 필요는 없고, 숫자 타입과 문자 타입을 가질 수 있다. 위의 예시는 사용자가 버튼을 얼마나 많이 클릭했는지 알기를 원하므로 0을 해당 state의 초기 값으로 선언했다. (2개의 다른 변수를 저장하기를 원한다면 useState( )를 두 번 호출해야 한다.)

- useState는 무엇을 반환할까요? state 변수, 해당 변수를 갱신할 수 있는 함수 이 두 가지 쌍을 반환한다. 이것이 바로 const [count, setCount] = useState()라고 쓰는 이유다. 클래스 컴포넌트의 this.state.count와 this.setState와 유사하다.

 

- 위 예제에서 count라고 부르는 state 변수를 선언하고 0으로 초기화한다. React는 해당 변수를 리렌더링할 때 기억하고, 가장 최근에 갱신된 값을 제공한다. count 변수의 값을 갱신하려면 setCount를 호출하면 된다.

2) state 가져오기

- 함수 컴포넌트는 count를 직접 사용할 수 있다.

 

<p>You clicked {count} times</p>

3) state 갱신하기

- 함수 컴포넌트는 setCount와 count 변수를 가지고 있으므로 this를 호출하지 않아도 된다.

 

<button onClick={() => {setCount(count + 1)}>
  Click me
</button>

4. Effect Hook 사용하기

- Effect Hook을 사용하면 함수 컴포넌트에서 side effect를 수행할 수 있다.

- 데이터 가져오기, 구독(subscription) 설정하기, 수동으로 React 컴포넌트의 DOM을 수정하는 것까지 이 모든 것이 side effects이다. 

- React 컴포넌트에는 일반적으로 두 종류의 side effect가 있다. 정리(clean-up)가 필요한 것과 그렇지 않은 것. 

1)  정리(clean-up)를 이용하지 않은 Effects

- React가 DOM을 업데이트 한 뒤 추가로 코드를 실행해야 하는 경우가 있다. 네트워크 리퀘스트, DOM 수동 조작, 로깅 등은 정리(clean-up)가 필요 없는 경우들이다. 이러한 예들은 실행 이후 신경 쓸 것이 없기 때문이다. 

 

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

function Example() {
  const [count, setCount] = useState(0);
  
  // componentDidMount, componentDidUpdate와 같은 방식으로
  useEffect(() => {
    // 브라우저 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가 하는 일은 무엇일까? useEffect Hook을 이용하여 우리는 React에게 컴포넌트가 렌더링 이후에 어떤 일을 수행해야하는 지를 말한다. React는 우리가 넘긴 함수를 기억했다가(이 함수를 ‘effect’라고 부른다) DOM 업데이트를 수행한 이후에 불러낼 것이다. 위의 경우에는 effect를 통해 문서 타이틀을 지정하지만, 이 외에도 데이터를 가져오거나 다른 명령형(imperative) API를 불러내는 일도 할 수 있다.

- useEffect를 컴포넌트 안에서 불러내는 이유는 무엇일까요? useEffect를 컴포넌트 내부에 둠으로써 effect를 통해 count state 변수(또는 그 어떤 prop에도)에 접근할 수 있게 된다. 함수 범위 안에 존재하기 때문에 특별한 API 없이도 값을 얻을 수 있는 것이다. 

- useEffect는 렌더링 이후에 매번 수행되는 걸까요? 그렇다. 기본적으로 첫번째 렌더링과 이후의 모든 업데이트에서 수행된다.

2) 정리(clean-up)를 이용하는 Effects

- 외부 데이터에 구독(subscription)을 설정해야 하는 경우에는 메모리 누수가 발생하지 않도록 정리(clean-up)하는 것은 매우 중요하다. 

 

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

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

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    
    // effect 이후에 어떻게 정리(clean-up)할 것인지 표시합니다.
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

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

 

- effect에서 함수를 반환하는 이유는 무엇일까? 이는 effect를 위한 추가적인 정리(clean-up) 메커니즘이다. 모든 effect는 정리를 위한 함수를 반환할 수 있다. 이 점이 구독(subscription)의 추가와 제거를 위한 로직을 가까이 묶어둘 수 있게 한다. 구독의 추가와 제거가 모두 하나의 effect를 구성하는 것이다.

- React가 effect를 정리(clean-up)하는 시점은 정확히 언제일까? React는 컴포넌트가 마운트 해제되는 때에 정리(clean-up)를 실행한다. 하지만 위의 예시에서 보았듯이 effect는 한번이 아니라 렌더링이 실행되는 때마다 실행된다. React가 다음 차례의 effect를 실행하기 전에 이전의 렌더링에서 파생된 effect 또한 정리하는 이유가 바로 이 때문이다.

3) effect를 이용하는 팁

- 관심사를 구분하려고 한다면 Multiple Effect를 사용한다. (= 함수 컴포넌트 내에서 Effect Hook을 여러번 사용한다)

- effect가 업데이트 시마다 실행되는 이유: useEffect는 기본적으로 업데이트를 다룬다. 다음 effect를 적용하기 전에 이전의 effect는 정리(clean-up) 한다. 

- Effect를 건너뛰어 성능 취적화하기: 모든 렌더링 이후에 effect를 정리(clean-up)하거나 적용하는 것이 때때로 성능 저하를 발생시키는 경우도 있다. useEffect는 특정 값들이 리렌더링 시에 변경되지 않는다면 React로 하여금 effect를 건너뛰도록 할 수 있다. useEffect의 선택적 인수인 두 번째 인수로 빈 배열을 넘기면 된다.

 

useEffect(() => {
  document.title = `You clickec {count} time`;
}, [count]); // count가 바뀔 때만 effect를 재실행한다.

 

- effect를 실행하고 이를 정리(clean-up)하는 과정을 (마운트와 마운트 해제 시에)딱 한 번씩만 실행하고 싶다면, 빈 배열([])을 두 번째 인수로 넘기면 된다. 이렇게 함으로써 React로 하여금 여러분의 effect가 prop이나 state의 그 어떤 값에도 의존하지 않으며 따라서 재실행되어야 할 필요가 없음을 알게 하는 것이다.