TIL React_06 (Hook)

46291 단어 ReactTILReact

React Hooks란?

리액트 16.8 버전에 추가된 기능으로 class를 작성하지 않고도
state와 다른 리액트 기능을 사용할 수 있게 해준다

리액트 Hooks는 기존에 클래스 컴포넌트 사이에서 state와 관련된
로직을 재사용하기 어려웠던 문제를 해결하기 위해만들어진 방법으로
계층 변화 없이 state관련 로직을 재사용할 수 있도록 도와준다.

클래스 컴포넌트는 생명주기 메소드를 사용하면서 로직이 구성되었을 때
이해하기 어렵다는 단점이 있다.

이때문에 많은 사람들이 상태 관리 라이브러리와
함께 사용하는 이유가 되기도 했다.
다만 이런 상태관리 라이브러리는 너무 많은 추상화를 하고
다른 파일들 사이에 건너뛰기를 요구하며
컴포넌트의 재사용을 더욱 어렵게 만들기도 했다.

Hook은 이런 문제를 해결하기위해 로직에 기반을 둔 작은 함수로
컴포넌트를 나눌 수 있게 만들어졌다.

또한, 클래스를 이해하기 위해서는
JavaScript에서 어떻게 this가 작동하는지 알아야만 했고
이런 이유로 리액트의 진입장벽이 한층 올라가
리액트를 사용하기 어렵게 만드는 요인으로 꼽히기도 했다.

그래서 Hook은 클래스없이 리액트 기능들을 사용하는 방법을 고안해냈고
개념적으로 리액트 컴포넌트는 함수에 가깝게 발전하게 되었다.

Hooks로 생각하기

기존의 리액트는 컴포넌트나 props가 변할 때 생명주기 메소드가 실행되었다면
리액트 Hooks는 데이터가 변화했을 때 동기화되어 웹 페이지도 변경한다.

class Chart extends Component {
  componenetDidMount() {
    // 컴포넌트 마운트 시, 이 코드를 실행
  }
  componenetDidUpdate(prevProps) {
    if(prevProps.data === props.data) {
    return;
    }
    // 컴포넌트 데이터 업데이트 시 이 코드를 실행
  }
  componentWillUnmount() {
    // 컴포넌트 언마운트 시 clean up!
  }
  render() {
    return (
      <svg className="chart" />
    )
  }
}

기존의 리액트를 살펴보면 하나의 데이터를 다룰 때에도 3가지 생명주기 메소드를 사용한다.

props가 변경되었는지에 따라 생명주기 메소드가 실행되어
페이지를 새로 렌더링하게 되는데 만약 복잡한 props가 내려왔을 때에
원하는 부분만 변경해 변경된 부분만 새로 렌더링 하고 싶어도
props와 관련된 컴포넌트 전체를 새로 렌더링하게된다.

이 과정에서 props의 작은 변화에도 생명주기 메소드가 실행이 되어야 하고
이런 과정이 비효율적으로 보이게 된다.

또한, 리액트 Hooks를 사용하는 것보다 가독성이 많이 떨어지게 된다.

const Chart = ({ data }) => {
  useEffect(() => {
    // 컴포넌트 마운트 시, 이 코드 실행
    // 컴포넌트 데이터 업데이트 시 이 코드를 실행
    return () => {
      // 컴포넌트 데이터 업데이트 시 이 코드를 실행
      // 컴포넌트 언마운트 시 clean up!
    }
  }, [data])
  return (
    <svg className="Chart" />
  )
}

리액트 Hooks는 보기에도 코드의 길이가 기존의 리액트보다 간결한 것을 알 수 있다.
좀 더 살펴보면 data가 props로 내려오고 이것을 useEffect의 두번째 인자로 사용되고 있다.

여기서 인자로 사용된 data가 변경될 때마다 useEffect함수가 실행이 된다.
이렇게 data가 변경될 때마다 새로 렌더링이 되기 때문에 새로운 정보를 받아온다.

코드로 봤을때에도 간결해 보이고,
새로 렌더링 되는 과정이 복잡하지 않은 것이
리액트 Hooks의 가장 큰 장점이다.

useState

본격적으로 Hook을 사용하는 방법에 대해 알아보자
Hook을 사용하기 위해서는 우선은 useState를 로드 해와야한다.

import React, { useState } from 'react';

리액트 API를 사용하기 위해 로드했던 부분에 이런식으로 useState를 로드해와야 사용할 수 있다.
이렇게 useState를 로드해왔다면 이제 함수에서 state를 사용할 수 있다.

import React, { useState } from 'react';

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

useState를 호출하는 것으로 "state 변수"를 선언할 수 있다.
count 라고 선언해두었지만 이름은 상관없다.
useState는 클래스 컴포넌트의 this.state의 역활과 같은 기능을 하게된다.
일반적으로 함수 내에서 선언한 변수는 함수가 끝날때 사라지지만
state 변수는 리액트에서 사라지지 않는다.

useState()에 넘겨준 인자는 state의 초기값이다.
함수 컴포넌트의 state는 클래스와 달리 객체로 만들 필요가 없고
숫자타입과 문자타입을 가질 수 있다.
여기서 두개의 다른 변수를 저장하려면 useState()를 두번 호출하면 된다.

useState()가 반환하는 값은 state변수, 해당 변수를 갱신할 수 있는 함수를 반환한다.
이런 이유 때문에 const [count, setCount] = useState()로 작성 되었다.
위 코드는 클래스 컴포넌트에서 this.state.countthis.setState와 비슷하다.

위의 Hook 예제를 클래스 컴포넌트로 만들면 아래와 같이 만들 수 있다.

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

함수 컴포넌트로 작성한 state를 가져오는 방법은
클래스 컴포넌트보다 간단하다.

<p>You cliked {this.state.count} times </p>

위 예제가 클래스 컴포넌트를 사용했을때
state를 가져오는 방법이었다면...

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

함수 컴포넌트에서는 이렇게 사용할 수 있다.
this.state를 작성할 필요가 없다.

다음은 state를 변경하는 방법이다.

<button onClick={() => this.setState({ count: this.state.count + 1 })}>
  Click me
</button>

위 예제는 클래스 컴포넌트에서 state를 변경하는 방법이었다면...

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

함수 컴포넌트에서는 setCountcount 변수를 가지고 있으므로 this를 호출하지 않아도 된다.
마치 setState를 사용하는것과 같이 setCount를 사용하면 된다.

함수 컴포넌트에서 state를 사용하는 방법을 코드로 정리해보면 아래와 같다.

import React, { useState } from 'react';

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

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

useState를 이용한 함수 컴포넌트를 만들때 이 세가지만 이해하면 된다.

  1. useState Hook을 리액트에서 가져온다.
  2. useState Hook을 이용해 state 변수와 해당 state를 갱신할 수 있는 함수를 만든다.
    이때 useState의 인자값으로 state의 초기값을 지정한다.
    state 변수를 선언할 때에는 구조분해할당을 이용해 만들어준다.
  3. state를 갱신할 수 있는 함수를 이용해 사용자가 상호작용을 했을 때 state 변수를 갱신한다.
    위 예제에선 count라는 변수를 Example 컴포넌트에 넘겨 해당 컴포넌트를 리렌더링 한다.

useEffect

Effect Hook을 사용하면 함수 컴포넌트에서 side effect(사이트 이펙트)를 수행할 수 있다.
여기서 잠깐 사이드 이펙트에 대해 설명하자면

사이드 이펙트(Side Effect)란,
함수(또는 컴포넌트)의 입력 외에도
함수의 결과에 영향을 미치는 요인이라고 할 수 있다.

대표적인 사이드 이펙트는 네트워크 요청(백엔드 API요청)이 있다.

컴포넌트를 개발할 때 fetch와 같은 API 요청이 없어도
컴포넌트는 작동을 해야 한다. 해당 컴포넌트에 가짜 데이터가
들어온다 하더라도 컴포넌트는 표현(presentation) 그 자체에 집중해야 하는 것이다.

하지만 앱을 만들다 보면 API도 호출해야되고 사이드 이펙트는 불가피하게 생기게 될 것이다.
이런 사이드 이펙트도 고려해서 앱을 만든다면
데이터 전송 여부에 따라 "로딩 중" 화면을 나타낼 것인지 아닌지도 고려해야 한다.

다시 리액트 useEffect로 돌아와서
useState에서 사용했던 예제를 가져와서 예를 들자면

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

위의 코드는 문서의 타이틀을 클릭 횟수가 포함된 문장으로
표현할 수 있는 기능을 더했다.

데이터 가져오기, 구독 설정, 수동으로 리액트 컴포넌트의 DOM 수정과 같은 것들이
사이드 이펙트인 것이다.

리액트에는 일반적으로 두종류의 사이드 이펙트가 있다
정리(Clean-up)가 필요한 것과 그렇지 않은 것으로 나눌 수 있다.
이 두가지 기준으로 useEffect의 사용을 정리해보자.

정리(Clean-up)를 이용하지 않는 Effects

리액트가 DOM을 업데이트한 뒤 추가로 코드를 실행해야하는 경우가 있다.
네트워크 리퀘스트, DOM 수동 조작등은 정리가 필요없는 경우이다.
이런 경우 실행된 후 신경쓸 필요가 없기 때문이다

클래스 컴포넌트에서 render() 메소드는 사이드 이펙트를 발생시키지 않는다.
클래스 컴포넌트에서 effect를 수행하는 것은
리액트가 DOM을 업데이트하고난 후 이기 때문이다.

그렇기 때문에 클래스 컴포넌트에서 사이드 이펙트가 발생되는 것은
componentDidMountcomponentDidUpdate에서 발생한다.

위 예제를 다시 클래스 컴포넌트로 만들어보면 아래와 같다.

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

위 코드에서 클래스 안에 두개의 생명주기 메소드에 같은 코드가 중복되는것에 주의하자

이는 컴포넌트가 마운트 된것과 업데이트된것에 상관없이
같은 사이드 이펙트를 수행해야하기 때문이다.
개념적으로는 렌더링 후 항상 같은 코드가 수행되길 바라는 것이다.

하지만 클래스 컴포넌트는 그런 메소드를 가지고 있지 않은 것이다.
그렇다면 함수 컴포넌트에서는 어떻게 구현되는 걸까?

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가 하는 역활은 컴포넌트가 렌더링된 후
어떤 명령을 수행해야되는지 알려준다.

리액트는 우리가 넘겨준 함수를 기억했다가(이런 함수를 effect라고 부른다.)
DOM 업데이트를 수행한 후 불러내게 된다.

위 예제에서는 effect를 통해 문서의 타이틀을 지정하지만
데이터를 가져오거나 명령형 API를 불러내는일도 할 수 있다.
componentDidMount(cDM)와 componenetDidUpdate(cDU)를 합친 기능과 같다.

그렇다면 왜 컴포넌트 안에서 useEffect를 호출하는걸까?
useEffect를 컴포넌트 내부에 놔둠으로써
effect를 통해 state변수(또는 어떤 props)에 접근할 수 있게 된다.
함수 범위 안에 존재하기 때문에 API없이 값을 얻을 수 있다.
이는 JavaScript의 클로저를 이용한 방법이다.

위에서 설명했듯 useEffect는 cDM과 cDU를 합친 기능과 같다.
그렇기 때문에 첫번째 렌더링과 그 이후의 모든 업데이트에서
자기 역활을 수행하게된다.

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

정리를 필요로하는 이펙트는
외부 데이터에 구독을 설정하는경우 가 있을 것이다.
이 경우 메모리 누수가 발생하지 않도록 정리하는 것이 매우 중요하다.

클래스 컴포넌트를 사용했을 때에 정리하는 방법은 다음과 같다.
componentDidMount에서 구독을 설정한 뒤
componentWillUnmount에서 이를 정리한다.

친구의 온라인 상태를 구독할 수 있는 ChatAPI 모듈의 예를 들어
클래스 컴포넌트로 만들면 다음과 같다.

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두개의 메소드 내에서
개념상 똑같은 코드가 있음에도 불구하고 이를 분리하게 만들어야한다.

이를 Hook을 이용해 만든다면 아래와 같다.

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

모든 이펙트는 정리를 위한 함수를 반환할 수 있다.
이렇게 하면 구독의 추가와 제거가 하나의 이펙트를 구성하게 만들 수 있다.

여기서 햇갈릴 수 있는게 정리를 하는 시점인데,
리액트는 컴포넌트의 마운트가 해제될 때 정리를 실행한다
하지만 위 예시에서는 이펙트가 렌더링 될때마다 실행되게 된다.
이렇게 되면 성능이 저하되는 문제가 발생할 수 있는데
이런 문제를 해결하기 위해서는 이펙트 건너뛰기를 이용해야 한다.

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);
    };
}, [isOnline]);

이렇게 useEffect()에 두번째 인자로 state변수를 넘겨주면
state변수가 변할 때에만 리렌더링하게 만들 수 있다.

이 경우 컴포넌트 범위 내에서 바뀌는 값들과
이펙트에 의해 사용되는 값을 모두 포함한다는 점에 주의하자

useEffect를 사용하는 방법을 요약하면 이렇다.
정리가 필요한 경우에는 함수를 반환한다.

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

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
});

정리가 필요하지 않은 경우에는 아무것도 반환하지 않는다.

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

좋은 웹페이지 즐겨찾기