[React]Hook

OnStar·2021년 9월 2일
0

React

목록 보기
9/11
post-thumbnail

React Hook 공식 문서 바탕의 React Hook 정리

React Hook의 등장배경

클래스형 컴포넌트의 상태 관리의 복잡성과 한계

componentDidMount componentDidUpdate componentWillUnmoun
생명주기에 따라 연관성있는 코드들이 분리되는 동시에 연관성없는 코드들이 한 메서드안에 결합된다.
생명주기 메서드를 기반으로 쪼개는 것보다는, Hook을 통해서 서로 비슷한 것을 작은 함수 묶음으로 컴포넌트를 사용할 수 있다.

클래스형 컴포넌트의 상태 관리의 재사용성

React는 컴포넌트간에 재사용 가능한 로직을 붙이는 방법을 제공하지 않았다. 따라서 독립적인 테스트와 재사용이 어려웠지만 Hook을 통해 상태 관련 로직을 추상화하고 계층의 변화 없이 상태 관련 로직을 재사용할 수 있도록 도와준다.

클래스의 this 키워드

클래스형 컴포넌트를 사용하면서 사용되는 JavaScript의 this 키워드는 대부분의 다른 언어에서와는 다르게 작동함해 코드 작성에서 혼란과 재사용성에 방해가 됐다.
따라서 함수형 컴포넌트에서의 Hook 사용은 클래스없이 React 기능들을 사용하는 방법들을 제시한다.

State Hook 사용하기

// 클래스형 컴포넌트
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

위의 클래스형 컴포넌트를 Hook을 사용하여 함수형 컴포넌트로 만들 수 있다.

// 동일한 기능을 하는 Hook을 사용한 함수형 컴포넌트
import React, { useState } from 'react';

function Example() {
  // 새로운 state 변수를 선언하고, 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는 현재의 state 값과 이 값을 업데이트 하는 함수를 쌍으로 제공한다.

const [현재의_state 값, 이_값을_업데이트하는__함수] = useState(초기_state);

useState 함수에서 선언한 함수는 class의 this.setState와 거의 유사하지만 이전 state와 새로운 state를 합치지 않는 차이점이 있다.

초기_state 는 class의 this.state와 달리 객체일 필요는 없다. 물론 객체일 수도 있다.

여러 state 변수 선언

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

React는 매번 렌더링할때 useState가 사용된 순서대로 실행한다.

state 갱신하기

클래스형 컴포넌트는 count를 갱신하기 위해 this.setState()를 호출한다.
하지만 함수 컴포넌트는 setCount와 count 변수를 가지고 있으므로 this 를 호출하지 않아도 된다.

Effect Hook 사용하기

리액트의 class 생명주기 메서드에 친숙하다면, useEffect Hook을 componentDidMount와 componentDidUpdate, componentWillUnmount가 합쳐진 것으로 생각해도 좋다.

데이터 가져오기, 구독(subscription) 설정하기, 수동으로 리액트 컴포넌트의 DOM을 수정하는 것까지 이 모든 것이 side effects 이며, 함수형 컴포넌트에서 side effect를 수행할 수 있게 해주는 것이 useEffect Hook 이다.

일반적으로 두 종류의 side effects가 있다.
1. 정리(clean-up)가 필요한 것 : 네트워크 리퀘스트, DOM 수동 조작, 로깅 [메모리 누수에 대한 신경 X]
2. 정리(clean-up)이 필요하지 않은 것 : 외부 데이터에 구독(subscription)을 설정해야 하는 경우 [메모리 누수에 대한 신경 O]

1. Clean-up을 이용하지 않는 Effects

리액트가 DOM을 업데이트 한 뒤 추가로 코드를 실행해야 하는 경우가 있다. 실행 이후 신경 쓸 것이 없기 때문이다.

Class를 사용하는 예시

리액트의 클래스 컴포넌트에서 render 메서드 그 자체는 side effect를 발생시키지 않는다. 이떄는 아직 이른 시기로서 이러한 effect를 수행하는 것은 리액트가 DOM을 업데이트하고 난 이후이다.

리액트에서 side effect를 componentDidMount와 componentDidUpdate에 두는 것이 바로 이 때문이다.

// class 컴포넌트 
// 리액트가 DOM을 바꾸고 난 뒤 타이틀 업데이트
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

위의 코드에서 두 개의 생명주기 메서드에 같은 코드가 중복된다.
이는 마운트 단계이든, 업데이트되는 것인지에 상관없이 같은 side effect를 수행해야 하기 때문에 중복을 피할 수 없다.

Hook을 이용하는 예시

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

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

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

useEffect Hook은 우리가 넘긴 함수를 기억했다가(이 함수를 'effect'라고 부른다) DOM 업데이트 수행한 이후에 불러낸다.
데이터를 가져오거나 다른 명령형 API 를 불러내는 일을 할 수도 있다.

useEffect를 컴포넌트 내부에 둠으로써 effect를 통해 count state 변수(또는 그 어떤 props에도)에 접근할 수 있게 된다.
함수 범위 안에 존재해서 값을 얻을 수 있게 된다.

useEffect는 렌더링 이후 매번 수행된다. 기본적으로 첫 번째 렌더링과 이후의 모든 업데이트에서 수행된다. (필요에 맞게 effect를 수정할 수 있다.) 리액트는 effect가 수행되는 시점에 이미 DOM이 업데이트되었음을 보장한다.

2. Clean-up을 이용하는 Effects

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

Class를 사용하는 예시

리액트 class에서는 흔히 componentDidMount에 구독을 설정한 뒤 componentWillUnmount에서 이를 Clean-up한다.

// 클래스형 컴포넌트
// 친구의 온라인 상태를 구독할 수 있는 ChatAPI 모듈의 예를 들어보겠습니다. 
// 다음은 class를 이용하여 상태를 구독(subscribe)하고 보여주는 코드입니다.

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

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

componentDidMountcomponentWillUnmount 에서 개념상 똑같은 effect에 대한 코드가 있음에도 불구하고 생명주기 메서드는 이를 분리하게 만든다.

Hook을 이용하는 예시

Hook을 이용해 위와 같은 기능을 하는 코드를 만들어보자. Clean-up의 실행을 위해 별개의 effect가 필요하다고 생각할 수도 있다. 하지만 구독의 추가와 제거를 위한 코드는 결합도가 높기 때문에 useEffect는 이를 함께 다루도록 고안되었다. 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';
}

effect에서 함수를 반환한다. 그 이유는 effect를 위한 추가적인 Clean-up 메커니즘이다. 모든 effect는 정리를 위한 함수를 반환할 수 있따. 이 점이 추가와 제거를 위한 로직을 가까이 묶어 둔다.

리액트는 컴포넌트가 마운트 해제되는 때에 정리를 실행한다. 하지만 effect는 한번이 아니라 렌더링이 실행되는 때마다 실행된다. 리액트가 다음 차례 effect를 실행하기 전에 이전의 렌더링에서 파생된 effect또한 정리하는 이유가 바로 이 때문이다.

팁: Effect를 건너뛰어 성능 최적화하기

모든 렌더링이후에 effect를 적용하는 것이 성능 저하를 발생시키는 경우도 있다.
클래스 컴포넌트의 경우에는 componentDidUpdate 에서 prePropsPrevState와의 비교를 통해서 이러한 문제를 해결할 수 있었다.

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

이러한 요구 조건은 흔하기 때문에 useEffect도 선택적인 두 번째 인수를 통해서 관리할 수 있다.

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

위의 예시에서 [count]를 두 번째 인수로 넘긴다. 만약 count가 5이고 컴포넌트가 리렌더링된 이후에도 여전히 count는 변함없이 5라면 리액트는 이전 렌더링 시의 값 [5]를 그 다음 렌더링 때의 [5]와 비교한다. 배열 내의 모든 값이 같기 때문에 (5 === 5) 리액트는 effect를 건너뛰게 된다. 이런 식의 최적화가 가능하다.

count가 6으로 업데이트된 뒤에 렌더링하면 리액트는 이전에 렌더링된 값 [5]를 그다음 렌더링 시의 [6]와 비교한다. 이때 (5 !== 6) 이기 때문에 리액트는 effect를 재실행한다. 배열 내에 여러 개의 값이 있다면 그중의 단 하나만 다를지라도 리액트는 effect를 재실행한다.

즉, 배열 속의 데이터(state, props)가 변경되었을 때만 effect를 실행한다.

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // props.friend.id가 바뀔 때만 재구독합니다.

만약 두 번째 인자로 빈 배열 []을 넘기게 된다면, effect 안의 props과 state는 초깃값을 유지하게 된다.


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


Hook API 참고서

0개의 댓글