본문 바로가기
Programming/React

[React] Hooks란? 사용방법과 배경 정리 (useEffect, useState, Custom Hook)

by devpine 2023. 4. 21.
반응형

Hook

Hook을 사용하여 기존 Class 바탕의 코드를 작성할 필요 없이, 상태 값 관리와 여러 React 기능을 사용할 수 있다. (v16.8 부터 추가됨)

 

Hook이 만들어진 배경

  1. 기존 컴포넌트 사이에서 상태 로직 재사용이 어려웠다.
    • React는 컴포넌트 간 재사용 가능한 로직을 붙이는 방법을 제공하지 않는다.(ex. 스토어에 연결)
    • 따라서, render props나 HOC(고차 컴포넌트) 패턴으로 해결해 왔지만, 이런 패턴은 컴포넌트 재구성을 강요하며, 코드 추적을 어렵게 만든다. provicders, consumers, HOS, render props 그리고 다른 레이어로 둘러싸인 wrapper hell이 될 수 있다. 그래서 React는 상태 관련 로직 공유를 위한 좀 더 좋은 기초 요소가 필요했다!
    • Hook을 사용하면 컴포넌트로부터 상태 관련 로직을 추상화할 수 있다. 독립적인 테스트와 재사용이 가능하여, 계층의 변화 없이 상태 관련 로직을 재사용할 수 있게 도와준다.
  2. 복잡한 컴포넌트는 이해하기 어렵다.
    • 상태 관련 로직과 사이드 이펙트가 있는 컴포넌트를 유지보수하면, 각 생명주기 메서드에 관련 없는 로직이 포함되곤 한다. (ex. componentDidMount와 componentDidUpdate는 데이터를 가져올 때 수행되어야 하지만, 같은 componentDidMount에서 이벤트 리스너를 설정하거나, componentWillUnmount에서 cleanup 로직을 수행하기도 한다) 이로 인해 버그가 쉽게 발생하고 무결성을 해치게 된다.
    • 그러나 위 예시에서, 상태 관리 로직은 하나로 묶여 있기 때문에 작게 분리하는 것은 불가능하고 테스트도 어렵다. 그래서 많은 개발자는 React를 별도의 상태 관리 라이브러리와 함께 결합하여 사용해왔다. 하지만 이런 상태 관리 라이브러리는 종종 너무 많은 추상화를 하고, 재사용이 어려워지기도 한다.
    • 이를 해결하기 위해, 생명주기 메서드 기반보다는, Hook을 통해 서로 비슷한 것을 하는 작은 함수의 묶음으로 컴포넌트를 나누는 방법을 사용할 수 있다.(구독 설정 및 데이터를 불러오기) 또한 로직 추적을 쉽게 할 수 있도록 리듀서를 사용하여 컴포넌트의 지역 상태 값을 관리할 수 있다.
  3. Class로 인해 생기는 혼동
    • JavaScript의 this 키워드는 대부분의 다른 언어와는 다르게 작동하여 사용자에게 큰 혼란을 주었고, 코드의 재사용서오가 구성을 매우 어렵게 만들었다. 또한 class의 사용을 위해 이벤트 핸들러 등록 방법을 정확히 파악해야 했고, React 내의 함수와 Class 컴포넌트의 구별, 각 요소의 사용 타이밍 등은 많은 개발자 사이에서도 의견이 나뉘었다.
    • Svelte, Angular, Glimmer 등처럼, 컴포넌트를 미리 컴파일해놓는 방식에는 잠재력이 있고, React 개발진은 Prepack을 사용한 컴포넌트 folding에 대해 실험했고 긍정적인 결과를 보았지만, Class 컴포넌트가 이 최적화를 더 느린 경로로 되돌리는 의도하지 않은 패턴을 장려할 수 있다는 것을 발견하였다. 이처럼 Class는 코드의 최소화를 힘들게 만들고, 핫리로딩을 깨지기 쉽고 신뢰할 수 없게 만든다. 즉, 코드 최적화가 가능하고 유지 가능성이 더 높은 환경을 제공하고자 하였다.
    • 해결을 위해, Hook은 Class 없이 React 기능을 사용하는 방법을 제시한다. 개념적으로 React 컴포넌트는 항상 함수에 더 가깝다. 

 

Hook 개요

아래 예시에서, userState가 바로 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

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

Hook을 호출해 함수 컴포넌트 안에 state를 추가하였다. 이 state는 컴포넌트가 다시 렌더링되어도 그대로 유지될 것이다. useState는 현재의 state 값과 이 값을 업데이트 하는 함수를 쌍으로 제공한다. class의 this.setState()와 거의 유사하지만, 이전 state와 새로운 state를 합치지 않는다는 차이점이 있다. 

useState는 인자로 초기 state를 받는다. 예시에서는 초기값으로 0을 넣어주었고, 이 초기값은 첫 번째 렌더링에만 딱 한번 사용된다. this.state와 달리 useState는 객체일 필요가 없다. 

 

useEffect 

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

useEffect는 함수 컴포넌트 내에서 이런 side effects를 수행할 수 있게 한다. class의 componentDidMount, componentDidUpdate, componentWillUnmount와 같은 목적으로 제공되지만, 하나의 API로 통합된 것이다.

아래는 DOM을 업데이트한 위 타이틀을 변경하는 예시이다.

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

function Example() {
  const [count, setCount] = useState(0);
  
  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>
  );
}

React는 DOM을 바꾼 뒤에 effect 함수를 실행한다. Effects는 컴포넌트 안에 선언되어 있기 때문에 props, state에 접근할 수 있다. 기본적으로 React는 매 렌더링 이후 effects를 실행한다. Effects를 해제해야 한다면, 해제하는 함수를 선택적으로 반환해주면 된다. 

예를 들어, 친구의 접속 상태를 구독하는 effect를 사용한 뒤에, 구독을 해지하여 해제해준다.

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

이 예시에서 컴포넌트 unmount될 때 React는 구독을 해지할 것이고, 재렌더링이 일어나 effect가 재실행되기 전에 구독을 해지할 것이다. Hook을 사용하면 구독을 추가하고 제거하는 로직과 같이 서로 관련 있는 코드들을 한군데에 모아서 작성할 수 있다. 반면 class 컴포넌트는 생명주기 메서드 각각에 쪼개서 넣어야 했다.

  • 정리(Clean-up)을 이용하지 않는 Effects: React가 DOM을 업데이트 한 뒤 추가로 코드를 실행해야 하는 경우, 예를 들어 Networt Request, DOM 수동 조작, Logging 등은 Clean-up하지 않아도 된다.
  • 정리(Clean-up)을 이용하는 Effects: 외부 데이터에 구독(subscription)을 설정해야 하는 경우에는 메모리 누수가 발생하지 않도록 Clean-up하는 것이 중요하다.
  • React가 effect를 정리(clean-up)하는 시점은? 컴포넌트가 마운트 해제될 때 실행한다. 

 

Effect 이용 팁

  • 관심사를 구분하고자 한다면 Multiple Effect를 사용한다.
  • Effect를 건너뛰어 성능 최적화한다. 모든 렌더링 이후 effect를 정리하거나 적용하는 것이 때로는 성능 저하를 발생시키는 경우도 있다. class 에서는 componentDidUpdate에서 prevProps, prevState의 비교를 통해 이러한 문제를 해결했다. useEffect에서는 선택적 인수인 두 번째 인수로 의존성 배열을 넘기면 된다. 배열 내에 단 하나만 다를지라도 React는 effect를 재실행한다. 마운트/언마운트 시에 딱 한번씩만 실행하고 싶다면 빈 배열 []을 넘기면 된다. 두 번째 인자는 빌드 시 변환에 의해 자동 추가될 수도 있다. 
  • React 공식 문서에서는 exhaustive-deps 규칙을 eslint-plugin-react-hooks 패키지에 포함하는 것을 추천한다. 패키지는 의존성이 바르지 않게 지정되었을 경고하고 수정하도록 알려준다.
componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

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

 

 

Hook 사용 규칙

  • 최상위(at the top level) 에서만 Hook을 호출해야 한다. 반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하지 않도록 주의한다.
  • React 함수 컴포넌트 내에서만 Hook을 호출해야 한다. 일반 JavaScript 함수에서는 Hook을 호출해서는 안된다.

 

Custom Hook

컴포넌트 간에 상태 관련 로직을 재사용하고 싶을 때, 전통적인 방법으로는 Higher-order components와 render props가 있다. Custom Hook은 이와 달리 컴포넌트 트리에 새 컴포넌트를 추가하지 않고 재사용이 가능하다.

위의 예시였던 친구 접속 상태를 구독하기 위해 useState, useEffect Hook을 사용한 로직을 Custom 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;
}

Custom Hook은 기능보다는 컨벤션에 가깝다. 이름은 use로 시작하고, 안에서 다른 Hook을 호출한다면 그 함수를 Custom Hook이라고 부를 수 있고, linter 플러그인이 Hook을 인식하고 버그를 찾을 수 있게 해준다. 이 외에도 useContext, useReducer 등의 내장 Hook이 존재한다.

사용자 정의 Hook을 만들어 폼 다루기, 애니메이션, 선언형 구독, 타이머, 그 외 다양한 방법을 적용할 수 있다. 복잡한 로직을 단순한 인터페이스 속에 숨길 수 있도록 하거나 복잡하게 뒤엉킨 컴포넌트를 풀어내는 방법을 찾는 것이 좋다. 하지만 너무 이른 단계에서 로직을 뽑아내려고 하지는 않는 것이 좋다. 예를 들어, 내부에 많은 state를 지니고 있지만 적절하게 관리되지 않는 컴포넌트가 있다면, 복잡한 업데이트 로직과 독립적인 테스트에 적합한 useReducer를 사용하는 것도 가능하다. 

반응형

댓글