[React] state & effect Hook 사용하기

전체내용 : state Hook사용하기
Hook은 클래스 컴포넌트를 작성하지 않아도 state와 같은 특징들을 사용할 수 있다.

1. State Hook 사용하기

1) Hook 과 함수 컴포넌트

React의 함수 컴포넌트는 다음 두 가지 모양으로 생겼다.

const Example = (props) => {
	// 여기서 Hook 사용가능
	return <div />;
}
function Example(props) {
	//여기서 Hook
	return <div />;
}

Hook은 React state를 함수 안에서 사용할 수 있게 해준다. (클래스 안에서는 동작 X)

기존에는 함수 컴포넌트 사용 중 state를 추가하고 싶을 때 클래스 컴포넌트로 바꿨다면, 이제는 함수 안에서 Hook을 이용하여 state를 사용할 수 있다. (단, 규칙준수)


2) state 변수 선언하기

함수 컴포넌트는 (클래스처럼) this를 가질 수 없기 때문에 this.state를 할당하거나 읽을 수 었다. 대신, useState Hook을 직접 컴포넌트에 호출한다.

import React, { useState } from 'react';

function Example() {
	// count라는 새로운 state 변수 선언함
	const [count, setCount] = useState(0);
  • useState를 호출하는 것의 의미
    "state 변수"를 선언할 수 있다. useState는 클래스 컴포넌트의 this.state가 제공하는 기능과 같다. state변수는 React에 의해 함수가 끝나도 사라지지 않는다.
  • useState의 인자로 넘겨주는 것
    state의 초기 값! 객체일 필요는 없고, 숫자 타입, 문자타입을 가질 수 있다. (2개의 다른 변수를 저장하길 원한다면 useState()를 두 번 호출해야 한다.)
  • useState가 반환하는 것
    [state 변수, 해당 변수를 갱신할 수 있는 함수] ← 이 두 가지 쌍(배열)을 반환한다.
    const [count, setCount] = useState()라고 쓰는 이유. 클래스 컴포넌트의 this.state.countthis.setState와 유사하다.

    /* 참고사항
    이러한 문법은 "배열 구조 분해'라고 한다. 
    이는 다음과 같이 배열에 접근하는 방식으로도 이용할 수 있지만, 
    배열 구조 분해라는 특별한 방법으로 변수를 선언했기 때문에 
    [0]이나 [1]로 배열에 접근하는 것은 좋지 않을 수 있다.
    */
    var fruitStateVariable = useState('banana'); // 두 개의 아이템이 있는 쌍을 반환
    var fruit = fruitStateVariable[0]; // 첫 번째 아이템
    var setFruit = fruitStateVariable[1]; // 두 번째 아이템

3) state 사용하기

state변수의 이름이 count일 때

(1) state 가져오기

  • 클래스 컴포넌트 : count를 보여주기 위해 this.state.count를 사용한다.

    <p>You clicked {this.state.count} times</p>
  • 함수 컴포넌트 : count직접 사용할 수 있다.

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

(2) state 갱신하기

  • 클래스 컴포넌트 : count를 갱신하기 위해 this.setState()를 호출한다.

    <button onClick={() => this.setState({ count: this.state.count + 1 })}>
      Click me
    </button>
  • 함수 컴포넌트 : setCount와 count 변수를 가지고 있으므로 this를 호출하지 않아도 된다.
    클래스 컴포넌트와 달리 state를 갱신하는 것은 병합하는 것이 아니라 대체하는 것이다.

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

4) 전체코드

// useState Hook을 React에서 가져온다.
import React, { useState } from 'react';

function Example() {
	// useState Hook을 이용, 'state 변수 & 해당 state를 갱신할 수 있는 함수'가 만들어진다.
	// useState의 인자로 0을 넘겨줘서 count값을 0으로 초기화했다.
	// state의 변수명, 인자로 넘겨줄 값은 자유롭게 ~
	const [count, setCount] = useState(0);

	return (
		<div>
			<p>You clicked {count} times</p>
			{/* 사용자가 버튼을 클릭하ㅏ면 setcount함수를 호출하여 state변수를 갱신한다. 
			React는 새로운 count변수를 Example 컴포넌트에 넘기며 
            해당 컴포넌트를 리렌더링한다.*/}
			<button onClick={() => setCount(count + 1)}>
				Click me
			</button>
		</div>
	);
}


2. Effect Hook 사용하기

Effect Hook을 사용하면 함수 컴포넌트에서 side effect를 수행할 수 있다.
side effect : 데이터 가져오기, 구독설정하기, 수동으로 리액트 컴포넌트의 DOM을 수정하기, ...

  1. effect에 정리가 필요없는 경우에는 어떤 것도 반환하지 않는다.
  2. effect에 정리가 필요한 경우에는 함수를 반환한다.

→ effect Hook은 두 가지 경우를 한 개의 API로 통합한다.


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

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

(1) Class 를 사용하는 경우

render 메서드 그 자체는 side effect를 발생시키지 않는다. effect를 수행하는 것은 리액트가 DOM을 업데이트 한 이후다. (componentDidMount와 componentDidUpdate에 두는 이유)

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

개념적으로 렌더링 이후에는 항상 같은 코드가 수행되는 코드를 작성했을 때, Class 안의 두 개의 생명주기 메서드에 같은 코드가 중복된다.


(2) Hook을 이용하는 예시

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

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  • useEffect가 하는 일?
    리액트에게 컴포넌트가 렌더링 이후에 어떤 일을 수행해야 하는지 알려준다. 리액트는 우리가 넘긴 함수(effect라고 부른다)를 기억했다가 DOM 업데이트를 수행한 이후에 불러낸다.
  • useEffect를 컴포넌트 안에서 불러내는 이유?
    컴포넌트 내부에 둠으로써 effect를 통해 count state 변수(또는 그 어떤 prop에도)에 접근할 수 있게 된다.
  • useEffect는 렌더링 이후에 매번 수행되는지?
    그러하다. 기본적으로 첫번째 렌더링과 이후의 모든 업데이트에서 수행된다. 리액트는 effect가 수행되는 시점에 이미 DOM이 업데이트되었음을 보장한다.

useEffect에 전달된 함수가 모든 렌더링에서 다르다. 리렌더링 하는 때마다 이전과 다른 effect로 교체하여 전달해서 렌더링의 결과의 한 부분이 되게 만든다. 각각의 effect는 특정한 렌더링에 속한다.


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

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

(1) Class 사용하는 예시

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

	// 정리 ~~~~ clean up
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
...

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


(2) Hook을 이용하는 예시

구독의 추가와 제거를 위한 코드는 결합도가 높기 때문에 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);
    };
  });
  • effect에서 함수를 반환하는 이유 ? effect를 위한 추가적인 정리 매커니즘이다. 구독의 추가와 제거가 모두 하나의 effect를 구성한다.
  • 리액트가 effect를 정리하는 시점은 정확히 언제죠 ? 리액트는 컴포넌트가 마운트 해제되는 때에 정리를 실행한다. 하지만 위의 예시에서 effect는 한 번이 아니라 렌더링이 실행되는 때마다 실행된다. 이 것이 리액트가 다음 차례의 effect를 실행하기 전에 이전의 렌더링에서 파생된 effect또한 정리하는 이유다.

effect에서 반드시 유명함수(named function)를 반환하지 않아도 된다.


3) effect를 사용하는 팁

(1) 관심사를 구분하려면 Multiple Effect를 사용한다

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

Hook을 이용하면 생명주기 메서드에 따라서가 아니라 코드가 무엇을 하는지에 따라 나눌 수 있다.
리액트는 컴포넌트에 사용된 모든 effect를 지정된 순서에 맞춰 적용한다.


(2) effect가 업데이트 시마다 실행되는 이유

effect 정리가 왜 마운트 해제되는 때에 한 번만이 아니라 모든 리렌더링 시에 실행될까? 이러한 디자인이 버그가 적은 컴포넌트를 만드는 데에 도움이 된다.

class 컴포넌트를 사용할 때 문제가 발생하는 경우

componentDidMount() {
	// 마운트된 이후 친구의 id를 통해 상태를 구독하며 온라인 표시
}
componentWillUnmount() {
	// 마운트 해제할 때 구독 해지

이 경우, 컴포넌트가 화면에 표시되어있는 동안 friend prop이 변하면(ex. 현재 친구 구독해지) 다른 친구(친구의 이전 id를 갖고 있는)의 온라인 상태를 계속 표시하는 버그가 발생한다. 또한 마운트 해제가 일어날 동안에는 구독 해지 호출이 다른 친구 id를 사용하여 메모리 누수나 충돌이 발생할 수도 있다.

이런 경우를 다루기 위해 클래스 컴포넌트에서는 componentDidUpdate를 사용한다.

componentDidUpdate(prevProps) {
	// 이전 friend.id에서 구독 해지
	// 다음 friend.id 구독
}

리액트 앱의 흔한 버그 중의 하나가 componentDidUpdate를 제대로 다루지 않는 것이다.


Hook 을 사용하는 컴포넌트

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

useEffect가 기본적으로 업데이트를 다루기 때문에 업데이트를 위한 특별한 코드가 따로 필요하지 않다.(버그 미리 방지) 다음의 effect를 적용하기 전에 이전의 effect는 정리된다.


(3) Effect를 건너뛰어 성능최적화하기

모든 렌더링 이후에 effect를 정리하거나 적용하는 것이 때때로 성능 저하를 발생시키는 경우가 있다. 클래스 컴포넌트의 경우 componentdidUpdate에서 prevPropsprevState와의 비교를 통해 이러한 문제를 해결한다.

Hook의 경우, useEffect Hook API에 이미 내재되어 있다. useEffect두 번째 인수(선택적)로 배열을 넘기면 특정 값들이 리렌더링 시에 변경될 때만 effect를 재실행한다.

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

두 번째 인자는 필드 시 변환에 의해 자동으로 추가될 수도 있다.

주의
이 최적화 방법을 사용하면, 배열이 컴포넌트 범위 내에서 바뀌는 값들과 effect에 의해 사용되는 값들을 모두 포함한다는 것을 기억해야 한다. 그렇지 않으면 현재 값이 아닌 이전의 렌더링 때의 값을 참고하게 된다.

effect를 실행하고 이를 정리하는 과정을 (마운트와 마운트 해제 시에)딱 한번씩만 실행하고 싶다면, 빈 배열[]을 두 번째 인수로 넘기면 된다. 이렇게 하면 effect가 그 어떤 값에도 의존하지 않으며 재실행될 필요가 없음을 알게 된다. 따라서, 빈 배열[]을 넘기면, effect안의 prop와 state는 초기값을 유지하게 된다.

effect의 잦은 재실행을 피하기 위해서는 exhaustive-deps 규칙을 eslint-plugin-react-hooks 패키지에 포함하는 것을 추천한다. 이 패키지는 의존성이 바르지 않게 지정되었을 때 경고하고 수정하도록 알려준다고 합니다...

© 2020 euzl. from JunhoBaik's, Built with Gatsby