3 minute read

React Hook(1)

Side Effect & Pure Function

함수 내에서 어떤 구현이 함수 외부에 영향을 끼치는 경우 해당 함수는 Side Effect가 있다고 이야기 한다. Side Effect는 프로그램의 버그가 발생하기 쉽게 한다.

순수 함수(Pure Function)은 이런 부작용(Side Effect)가 없는 함수를 말한다. 즉, 함수의 실행이 외에 영향을 끼치지 않는 함수를 말한다. 따라서 순수 함수는 스레드 안전하고 병력적인 계산이 가능하다.

let foo = 'hello';

function bar() {
  foo = 'world';
}

bar(); // bar는 Side Effect를 발생시킵니다!
-------------------------------------------
  
function upper(str) {
  return str.toUpperCase(); // toUpperCase 메소드는 원본을 수정하지 않습니다 (Immutable)
}

upper('hello') // 'HELLO'

앞서 공부한 React의 함수 컴포넌트는 props가 입력으로, JSX Element가 출력으로 나간다. 여기에는 Side Effect가 없는 순수 함수로 작동한다. 하지만, 보통 React 애플리케이션을 작성할 때는 Ajax요청이 필요하거나, LocalStorage 또는 타이머와 같은 React와 상관없는 API를 사용하는 경우가 발생할 수 있다. 이는 전부 Side Effect이다. React는 Side Effect를 다루기 위한 Hook인 Effect Hook을 제공한다.

Hook?

Hook은 클래스 컴포넌트를 작성하지 않아도 state와 같은 특징들을 사용할 수 있는 함수 컴포넌트이다. Hook을 이용함으로서 props, state, context, refs 그리고 lifecycle와 같은 React개념에 좀 더 직관적인 API를 제공한다.

아래 에제는 class를 사용한 react 예제이다.

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

위 예제에서 state는 { count: 0}이며 사용자가 this.setState()를 호출하는 버튼을 클릭했을 때 state.count를 증가시킨다.

반면 리액트에서 함수 컴포넌트는 아래와 같이 생겼다.

const Example = (props) => {
  // 여기서 Hook을 사용할 수 있습니다!
  return <div />;
}
// 또는
function Example(props) {
  // 여기서 Hook을 사용할 수 있습니다!
  return <div />;
}

// ⚠️ **Hook**은 `class` 안에서는 동작하지 않는다.

Hook 사용 규칙

Hook은 2가지 규칙을 준수해야합니다.

  • 최상위(at the top level)에서만 Hook을 호출해야 한다.

    • 반복문, 조건문, 중첩된 함수내에서는 Hook을 사용하면 안된다.
  • react 함수 컴포넌트 내에서만 Hook을 호출해야 한다.

    • 일반 JavaScript 함수에서 Hook을 호출하면 안된다.
    • 그러나 custom Hook 안에서는 Hook 호출이 가능하다.


⚠️ react는 최상위에 Hook을 호출하지 않는 경우, 잘못된 결과를 초래한다. react가 Hook이 호출되는 순서에 의존하기 때문이다.

react는 Hook의 호출순으로 어떤 state 연동하는지 확인한다.

아래의 컴포넌트는 첫번째 랜더링시 name !== ''true로 작동하여 제대로 작동하지만 그다음 렌더링에서 폼을 초기화하면서 조건 name !== ''false로 작동된다면 오류가 발생한다.

function Form() {
  // name이라는 state 변수를 사용
  const [name, setName] = useState('Mary');

  /* ===== 🔴 Hook을 건너뛰었습니다! ===== */
  if(name !== '') {                                                   
    // Effect를 사용해 폼 데이터를 저장
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }
  
  /* ==== 🔴 surname state 변수를 읽는 데 실패했습니다. ==== */
  // surname이라는 state 변수를 사용
  const [surname, setSurname] = useState('Poppins');

  /* ==== 🔴 제목을 업데이트하기 위한 effect가 대체되는 데 실패했습니다. ==== */
  // Effect를 사용해서 title 를 업데이트
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}

위에서 지역에 최상위에 Hook을 호출하지 않았으며 Hook을 건너뛴 부분이 존재합니다. 이로 인해 Hook의 호출 순서가 달라지게 된다. react는 두번째 useEffect() 호출이 나올 것으로 예상했지만 useState('Poppins') 호출됨으로서 버그가 발생된다.


State Hook 사용하기

state 변수 선언하기

클래스를 사용할 때 constructor 안에서 this.state{ count: 0 }로 설정함으로써 count0으로 초기화했다.

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

함수 컴포넌트는 this를 가질 수 없기 때문에 this.state를 할당하거나 읽을 수 없다. 대신 useState Hook을 직접 컴포넌트에 호출한다.

// react 에서 useState 를 import 해야한다.
import React, { useState } from 'react';

function Example {
  // ES6의 배열 구조 분해(destructuring) 사용
  const [STATE_명, setSTATE_명] = useState(초기_STATE_값)
  // const STATE_명 = useState(초기_STATE_값)[0]
  // conse setSTATE_명 = useState(초기_STATE_값)[1]

  /* 여러개의 State Hook을 사용할 수 있다. */
  const [STATE_1, setSTATE_1] = useState(1);
  const [STATE_2, setSTATE_2] = useState(2);
  // ...

이를 바탕으로 위에 작성된 클래스 문법을 useState로 작성해보자.

import React, { useState } from 'react';

function Example() {
  // const 라는 state 생성
  // setCount() 라는 setState() 생성
  // count는 0으로 초기화
  const [count, setCount] = useState(0);


state 가져오기

클래스 컴포넌트는 count를 보여주기 위해 this.state.count를 사용했지만 함수 컴포넌트는 count를 직접 사용할 수 있다.

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

// 함수 컴포넌트
  <p>You clicked {count} times</p>


state 갱신하기

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

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

// 함수 컴포넌트
  <button onClick={() => setCount(count + 1)}>    Click me
  </button>


📖 useState() vs this.state

state 변화시 병합하여 아래와 같이 동작한다.

  • useState: 변화된 state대체
  • this.state: 변화된 state를 갱신 혹은 병합함