Hooks 는 리액트에 조만간 도입될 예정인 기능으로서, 함수형 컴포넌트에서도 상태 관리 및 클래스형 컴포넌트에서만 할 수 있는 다른 작업들을 구현 할 수 있게 해주는 기능입니다. 이 기능은 아직 정식 릴리즈 되어있지 않으며 React v16.7.0-alpha 버전에서만 사용가능합니다.

따라서, 사용 방식이 바뀔 가능성이 있으니, 아직 프로덕션에서는 사용하시면 안됩니다. 하지만 조만간 릴리즈가 되면 사용 할 수 있게 될 테니 미리 한번 알아봅시다!

16.7 버전이 정식 릴리즈되면 이 포스트 또한 다시 수정하도록 하겠습니다.

프로젝트 준비

지금은 이 기능을 사용하시려면 리액트 16.7.0-alpha 를 사용하셔야 합니다. CRA 를 사용해서 프로젝트를 생성하고, react 와 react-dom 을 @next 버전으로 설치하세요.

$ npx create-react-app react-hooks-sample
$ cd react-hooks-sample
$ yarn add react@next react-dom@next

혹은, 다음 샌드박스를 쓰셔도 됩니다:

Edit react-hooks

State Hook: useState

State Hook 은 함수형 컴포넌트에서 변화 할 수 있는 상태를 사용 할 수 있게 해줍니다.

카운터 구현하기

한번 useState 라는 Hook 함수를 사용해서 카운터를 함수형 컴포넌트로 구현해보겠습니다.

Counter.js 라는 파일을 만들어서 다음 코드를 작성해보세요.

src/Counter.js

import React, { useState } from 'react';

const Counter = () => {
  const [value, setValue] = useState(0);

  return (
    <div>
      <p>
        <b>{value}</b>번 누르셨습니다.
      </p>
      <button onClick={() => setValue(value + 1)}>+1</button>
      <button onClick={() => setValue(value - 1)}>-1</button>
    </div>
  );
};

export default Counter;

useState 함수의 파라미터로는 사용하고 싶은 상태의 기본값을 넣어줍니다. 우리는 현재 0을 기본값으로 사용하고 있습니다. useState 를 호출하면 배열을 반환하는데, 이 배열의 첫번째 원소는 현재 상태 값과, 두번째 원소는 이 값을 설정해주는 setter 함수입니다.

이제 App 에서 렌더링해볼까요?

src/App.js

import React, { Component } from 'react';
import Counter from './Counter';

class App extends Component {
  render() {
    return (
      <div>
        <Counter />
      </div>
    );
  }
}

export default App;

image.png

Edit react-hooks

폼 구현하기

이번에는 useState 를 사용해서 여러개의 상태를 관리해야 할 땐 어떻게 해야 하는지 볼까요?

Form.js 컴포넌트를 다음과 같이 만들어보세요:

Form.js

import React, { useState } from 'react';

const Form = () => {
  const [name, setName] = useState('');
  const [description, setDescription] = useState('');

  const onSubmit = e => {
    e.preventDefault();
    alert(`${name} (${description})`);
    setName('');
    setDescription('');
  };

  return (
    <form onSubmit={onSubmit}>
      <input
        placeholder="이름"
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <input
        placeholder="설명"
        value={description}
        onChange={e => setDescription(e.target.value)}
      />
      <button type="submit">확인</button>
    </form>
  );
};

export default Form;

다 만드셨으면 App.js 에서 렌더링 하세요.

import React, { Component } from 'react';
import Form from './Form';

class App extends Component {
  render() {
    return (
      <div>
        <Form />
      </div>
    );
  }
}

export default App;

image.png

Edit react-hooks

여러 유용한 HOC 함수들이 들어있는 recompose 의 withState 를 사용해본 분이시라면, 사용방식이 매우 유사함을 느끼셨을 것 입니다. HOC 를 사용해서 구현하는 것과 비교했을때 주요 차이점은 완전히 함수형 컴포넌트 라는 것 입니다. HOC 를 사용할땐 결국 HOC 에서 클래스형 컴포넌트를 생성했죠. 하지만, Hooks 는 클래스형 컴포넌트로 한번 감싸는게 아니라, 온전한 함수형 컴포넌트에 기능을 부여해줍니다.

Effect Hook: useEffect

이번엔 useEffect 라는 함수에 대하여 알아봅시다. 이 함수는, 컴포넌트가 마운트 되거나 리렌더링이 마치고 나서 실행됩니다. componentDidMountcomponentDidUpdate 와 비슷하다고 생각하시면 됩니다.

아까 전에 만들었떤 Counter 컴포넌트에서 useEffect 함수를 사용해보겠습니다:

src/Counter.js

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

const Counter = () => {
  const [value, setValue] = useState(0);

  useEffect(() => {
    // 이 함수는 render 가 마치고 난 다음에 실행됩니다!
    console.log('rendered:', value);
  });

  console.log('rendering: ', value);
  return (
    <div>
      <p>
        <b>{value}</b>번 누르셨습니다.
      </p>
      <button onClick={() => setValue(value + 1)}>+1</button>
      <button onClick={() => setValue(value - 1)}>-1</button>
    </div>
  );
};

export default Counter;

src/App.js

다시, Counter 를 App 에서 렌더링하세요:

import React, { Component } from 'react';
import Counter from './Counter';

class App extends Component {
  render() {
    return (
      <div>
        <Counter />
      </div>
    );
  }
}

export default App;

image.pngEdit react-hooks

잘 알아두세요. useEffect 에 넣은 함수는 컴포넌트가 render 를 마친 다음에 실행됩니다. 그러면, 이걸 사용해서 어떤 작업을 할 수 있을까요?

가끔씩은, 우리가 똑같은 작업을 componentDidMount 와 componentDidUpdate 에서 구현해야 할 때가 있습니다. 예를들어서 velog 에서 포스트 하단에 뜨는 다른 포스트를 누르면 같은 종류의 페이지에서 url 만 바뀌게 됩니다.

image.png

Before: /@velopert/eslint-and-prettier-in-react
After: /@velopert/react-component-styling

때문에 컴포넌트가 언마운트 -> 마운트를 거치는 것이 아니라, 주소를 가르키는 props 만 업데이트가 되는데요, 이에 따라 포스트를 새로 불러와야 하기에, 이런 로직이 구현되어있습니다:

  componentDidMount() {
    this.initialize();
    const { hash } = this.props.location;
    if (hash !== '') {
      PostsActions.activateHeading(decodeURI(hash.split('#')[1]));
    }
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevProps.urlSlug !== this.props.urlSlug) {
      PostsActions.unloadPost();
      this.initialize();
    }
  }

컴포넌트가 새로 마운트 될 때에도 this.initialize 를 호출하고, 업데이트 될 때에도 urlSlug 부분이 바뀌면 this.initialize 를 호출하고있죠. useEffect 를 사용하면 이러한 중복 로직을 해결 해줄 수 있습니다. 물론, 함수형 컴포넌트여야 사용 할 수 있지만요.

useEffect 를 사용 하실 때 주의하실 점은 우리가 설정해준 함수가 render가 될 때마다 실행된다는 점 입니다. 즉, props 나 state 가 바뀌지 않고 부모컴포넌트가 리렌더링 될 때에도 호출이 됩니다. 만약에 특정 상황에만 이 함수가 실행되게끔 하고 싶다면, useEffect 의 두번째 파라미터로 주시하고 싶은 값들을 배열 형태로 전달해주면 됩니다.

  useEffect(() => {
    // 이 함수는 render 가 마치고 난 다음에 실행됩니다!
    console.log('rendered:', value);
  }, [value]);

이렇게 하면, value 값이 바뀔 때만 useEffect 가 호출됩니다.

Hooks 의 사용 규칙

리액트 매뉴얼에서는 Hooks 사용에 있어서 두가지 준수해야 할 규칙을 규정하였습니다.

1. Hooks 를 컴포넌트의 Top-level 에서만 사용 할 것

Hooks 는 반복문이나, 조건문이나, 감싸진 함수에선 사용하면 안됩니다.

2. 리액트 함수에서만 사용 할 것

Hooks 는 리액트 함수형 컴포넌트 내부에서만 사용하셔야 합니다. 일반 JavaScript 함수에서는 사용하면 안됩니다. 하지만, Custom Hook 에서는 괜찮습니다. (이에 대해선 아래 섹션에서 알아보게 됩니다.)

위 규칙을 준수하기 위해서 ESLint 플러그인 도 만들어졌습니다. 아마 이 플러그인은 Hooks 가 공식 릴리즈가 된다면 CRA 로 만든 프로젝트에 자동으로 적용 될 것으로 보입니다.

Custom Hook 만들기

우리가 방금 배운 useState 와 useEffect 를 활용하면, 정말 다양한 작업들을 할 수 있습니다. 그리고 재사용 되는 로직들은 우리가 따로 Custom Hook 으로 만들어서 우리들만의 Hook 을 만들어서 사용 할 수있습니다.

한번 연습삼아 axios 로 웹 요청을 하는 Hook 을 만들어볼까요?

우선 axios 를 설치하세요.

$ yarn add axios

그리고 src 디렉토리에 hooks 디렉토리를 만들어서 우리들의 Custom Hook useRequest 를 작성해보겠습니다. 주석들을 꼼꼼히 읽어보면서 Hook 을 만들어보세요!

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

function useRequest(url) {
  // loading, response, error 값을 다루는 hooks
  const [loading, setLoading] = useState(false);
  const [response, setResponse] = useState(null);
  const [error, setError] = useState(null);

  // 렌더링 될 때, 그리고 url 이 바뀔때만 실행됨
  useEffect(
    async () => {
      setError(null); // 에러 null 처리
      try {
        setLoading(true); // 로딩중
        const res = await axios.get(url); // 실제 요청
        setResponse(res); // response 설정
      } catch (e) {
        setError(e); // error 설정
      }
      setLoading(false); // 로딩 끝
    },
    [url] // url 이 바뀔때만 실행됨
  );
  return [response, loading, error]; // 현재 값들을 배열로 반환
}

export default useRequest;

이제 useRequest 를 사용하는 컴포넌트를 작성해볼까요? 연습용 API 는 JSONPlaceholder 에서 제공되는 API 들을 사용하겠습니다.

src/Post.js

import React from 'react';
import useRequest from './hooks/useRequest';

const Post = () => {
  const [response, loading, error] = useRequest(
    'https://jsonplaceholder.typicode.com/posts/1'
  );

  if (loading) {
    return <div>로딩중..</div>;
  }

  if (error) {
    return <div>에러 발생!</div>;
  }

  /*
    컴포넌트가 가장 처음 마운트 되는 시점은, Request 가 시작되지 않았으므로
    loading 이 false 이면서 response 도 null 이기에
    response null 체킹 필요 
  */
  if (!response) return null;

  const { title, body } = response.data;

  return (
    <div>
      <h1>{title}</h1>
      <p>{body}</p>
    </div>
  );
};

export default Post;

loaindg.gif

Edit react-hooks

와우~ HOC 나 Render Props 로 할 수 있는 것들을, Hook 으로도 할 수 있게 되었습니다. Hook 을 사용하면 정말 다양한 것들을 할 수 있습니다.

그 외의 React 내장 Hooks

리액트 라이브러리에는 방금 봤던 useState, useEffect 말고도 다른 Hooks 들이 내장되어있습니다.

내장 Hooks 에 대한 상세한 정보는 여기 에서 확인 할 수있습니다.

그 중에서 정말 유용할 것 같은 Hook 몇가지만 소개하고 나머지는 추후에 정식 릴리즈가 되면 다시 정리하도록 하겠습니다.

useContext

const context = useContext(Context);

useContext 는 Context API 를 Hook 을 통해 사용 할 수 있게 해줍니다. Render Props 보다, HOC 를 사용하는 것 보다, 혹은 contextType 을 사용하는 것 보다 훨씬 편하게 느껴지는 군요!

Hooks 가 정식 릴리즈가 되면 아마 저는 앞으로 Context 를 사용 할 떄 Hook 으로 사용 할 것 같습니다.

useReducer

useReducer 는 리덕스에서 리듀서를 사용하는 것과 유사한 방식으로 컴포넌트 상태 관리를 할 수 있게 해줍니다. 그렇다고 해서 이 Hook을 사용하기위해 사전에 리덕스를 설치해야 하는건 아닙니다. 그냥 컴포넌트내부에서 사용 할 수 있는 리듀서라고 생각하시면 됩니다.

useRef

useRef 는 함수형 컴포넌트에서도 ref 를 사용 할 수 있게 해주는 Hook 입니다.

정리

React 의 Hooks 가 정식 릴리즈가 되면 다시 한번 리액트 생태계에 큰 바람이 불게 될 것 같습니다. 재사용 되는 로직을 따로 분리시키는 방법이 앞으로 구현 방식에 있어서 선택사항이 정말 많아졌네요. HOC, Render Props, 그리고 대망의 Hooks!! 정말 기대가 됩니다. 빨리 프로덕션에서도 사용 할 수 있으면 좋겠네요 :) 읽어주셔서 감사합니다.