TIL 59 | Hooks (useState, useEffect)

참고자료 : React Hooks 공식문서, 함수형 컴포넌트, 클로저, useState

Hooks

함수 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 연동(hook into) 할 수 있게 해주는 함수이다. Hook은 class 안에서는 동작하지 않는다. 대신 class 없이 React를 사용할 수 있게 해준다.

React without Hooks SUCKS

  • Wrapper Hell (Reusing Logic)

  • Giant components

  • Confusing classes

네스팅을 줄이기 위해서 (wrapper hell) 컴포넌트 안에 많은 함수와 jsx 를 넣게 되면 컴포넌트가 비대해지고, 그 반대로 재사용성을 위해서 컴포넌트를 분리하게 되면 네스팅의 지옥에 빠지게 되는 아이러니가 발생한다.

"Not three separate three problems, three symptoms of one problem"

사용하는 이유

  • 컴포넌트 사이에서 상태와 관련된 로직을 재사용하기가 어렵다.
  • 복잡한 컴포넌트들을 이해하기가 어렵다. (관심사의 분리, Lifecycle API)
  • Class는 복잡해서 사람과 기계를 혼동시킨다. (상속을 받을 경우 State, Action을 분리하여 생각할 수 없다.)

Hook 사용 규칙

1. 최상위에서만 Hook을 호출해야 한다.

반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하면 안된다.

import React, { useState } from "react"

function Hooks(props) {
	if (!props.isExist) return;
	const [state, setState] = useState(); // Error!
}

// react가 여러 훅들을 구분할 수 있는 유일한 정보는 훅이 사용된 순서 뿐이기 때문.

2. React 함수 컴포넌트 내에서만 Hook을 호출해야 한다.

일반 JS 함수에서는 Hook을 호출해서는 안된다. (직접 작성한 custom Hook 내에서는 Hook을 호출할 수 있다.)

State Hook (useState)

const [state, setState] = useState(initialState);

상태 유지 값과 그 값을 갱신하는 함수를 반환한다. 최초로 렌더링을 하는 동안, 반환된 state(state)는 첫 번째 전달된 인자(initialState)의 값과 같다.

setState 함수는 state를 갱신할 때 사용한다. 새 state 값을 받아 컴포넌트 리렌더링을 큐에 등록한다.

setState(newState + 1); // setState와 동일하게 비동기 업데이트
setState(prev => prev + 1);
this.setState(prev =>)

다음 리렌더링 시에 useState를 통해 반환받은 첫 번째 값은 항상 갱신된 최신 state가 된다.

FAQ

useState() 하나에서 모든 상태 값을 관리하면 안될까?

함께 변화하는 값들끼리 나누어 여러 state 변수로 나누는 것을 추천한다.

function Box() {
  const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
  // ...
}
function Box() {
  const [position, setPosition] = useState({ left: 0, top: 0 });
  const [size, setSize] = useState({ width: 100, height: 100 });

  useEffect(() => {
    function handleWindowMouseMove(e) {
      setPosition({ left: e.pageX, top: e.pageY });
    }
    // ...

함수는 상태를 저장하지 않는다고 했는데 어떻게 useState는 상태값을 저장할까?

함수는 실행이 완료되면 함수 내에서 사용했던 모든 메모리들을 정리(가비지 컬렉팅)하는데 실행이 끝나고도 메모리에 스스로를 남겨둘 수 있는 방법(클로저)이 있다.

함수형 컴포넌트는 Hooks를 사용하면서 상태 관리가 가능해졌다. Hooks에서 state를 저장하려면 useState()를 사용한다. useState 역시 함수이며 클로저를 이용해 변수를 저장한다.

const useState = (init = undefined) => {
  let value = init
  const getter = () => value
  const setter = next => (value = next)

  return [getter, setter]
}

const [state, setState] = useState('클로저')

초기값을 받아 내부의 지역변수 value에 할당한다. 내부 함수 getter()는 지역변수 init을 바라보고 있다. 또 다른 내부 함수 setter()은 next 라는 인자를 받아 value의 값ㅇ르 수정한다. 이후 다시 getter()를 호출하게 되면 변경된 value의 값을 호출하게 된다.

두 함수는 배열 형태로 리턴되고 useState를 사용할 때는 배열 구조분해 할당 형태로 많이 사용하게 된다. 위의 경우 내부 함수가 지역 변수를 참조하고 있으므로 사라지지 않는다. 또한 외부로 노출된 getter, setter 함수를 통해 내부 변수에 지속적으로 접근하며 호출/재할당을 할 수 있다. 이는 클래스형 컴포넌트에서 state가 해온 역할과 동일하다.

Effect Hook (useEffect)

effect Hook, 즉 useEffect는 함수 컴포넌트 내에서 이런 side effects를 수행할 수 있게 해준다. React class의 componentDidMountcomponentDidUpdate, componentWillUnmount 와 같은 목적으로 제공되지만 API로 통합된 것이다.

useEffect(didUpdate);

useEffect(() => {}, [count])

명령형 또는 어떤 effect를 발생하는 함수를 인자로 받는다. setState를 통해서만 state를 업데이트 해왔듯, 대신에 useEffect를 사용하면 된다. useEffect에 전달된 함수는 화면에 렌더링이 완료된 후에 수행된다. 기본적으로 동작은 모든 렌더링이 완료된 후에 수행되지만, 어떤 값이 변경되었을 때만 실행되게 할 수도 있다.

조건부 effect 발생

effect의 기본 동작은 모든 렌더링을 완료한 후 effect를 발생하는 것이다. 이와 같은 방법으로 만약 의존성 중 하나가 변경된다면 effect는 항상 재생성된다.

source props가 변경될 때만 effect를 생성할 수 있다. 이것을 수행하기 위해서는 useEffect에 두 번째 인자를 전달하면 된다. 이 인자는 effect가 종속되어 있는 값의 배열이다. 이를 적용한 예는 아래와 같다.

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

자 이제, props.source가 변경될 때에만 구독이 재생성된다.

useEffect 사용 예시

componentDidMount & componentWillUnmount

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

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

  componentDidUpdate(prevProps, prevState) {
		if(prevState.count !== this.state.count){
	    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(() => {
    // 브라우저 API를 이용하여 문서 타이틀을 업데이트합니다.
    document.title = `You clicked ${count} times`; // CDM
  }, []);

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

componentDidUpdate

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

FAQ

setState 비동기 처리(두 번째 인자)는 어떻게 할까?

// class component
handleBtnColor = () => {
	this.setState({
		color: "red"
	}, () => console.log(this.state.color))
}
// function component
const [color, setColor] = useState("blue")

const handleBtnColor = () => {
	setColor("red")
}

useEffect(() => {
	console.log(color)
}, [color])

하나의 useEffect 안에서 로직을 처리하기 힘든 경우

여러 개의 useEffect를 사용하면 된다. 관심사를 구분하려고 한다면 Multiple Effect를 사용한다. Hook이 탄생한 동기가 된 문제 중의 하나가 생명주기 class 메서드가 관련이 없는 로직들은 모아놓고, 관련이 있는 로직들은 여러 개의 메서드에 나누어 놓는 경우가 자주 있다는 것이다. tate Hook을 여러 번 사용할 수 있는 것처럼 effect 또한 여러 번 사용할 수 있다. Effect를 이용하여 서로 관련이 없는 로직들을 갈라놓을 수 있다.

useEffect가 하는 일

useEffect Hook을 이용하여 우리는 리액트에게 컴포넌트가 렌더링 이후에 어떤 일을 수행해야 하는지를 말한다. 리액트는 우리가 넘긴 함수를 기억했다가 DOM 업데이트를 수행한 이후에 불러낼 것이다. 위의 경우 effect를 통해 문서 타이틀을 지정하지만, 이 외에도 데이터를 가져오거나 다른 명령형 API를 불러낼 수도 있다.

useEffect를 컴포넌트 안에서 불러내는 이유

useEffect를 컴포넌트 내부에 둠으로서 effect를 통해 state 변수 (또는 props에도) 접근할 수 있게 된다. 함수 범위 안에 존재하기 때문에 특별한 API 없이도 값을 얻을 수 있다. Hook은 자바스크립트의 클로저를 이용하여 리액트에 한정된 API를 고안하는 것보다 자바스크립트가 이미 가지고 있는 방법을 이용하여 문제를 해결한다.

useEffect는 렌더링 이후에 매번 수행되나?

마운팅과 업데이트라는 방식으로 생각하는 대신에 effect를 렌더링 이후에 발생하는 것으로 생각하는 것이 더 쉽다. 리액트는 effect가 수행되는 시점에 이미 DOM이 업데이트되었음을 보장한다.

Custom Hook

useState, useEffect 등 다양한 내장 Hook 등을 조합하면 반복되는 로직을 분리해 Custom Hook을 작성할 수 있다.

function Box() {
  const position = useWindowPosition();
  const [size, setSize] = useState({ width: 100, height: 100 });
  // ...
}

function useWindowPosition() {
  const [position, setPosition] = useState({ left: 0, top: 0 });
  useEffect(() => {
    // ...
  }, []);
  return position;
}

좋은 웹페이지 즐겨찾기