회사 제품 Playce WASup 을 리액트 16.7로 만들었습니다. Playce RoRo 를 처음 만들 땐 16.4였는데, 어느새 버저닝이 16.11까지 올라왔습니다.

아직 팀에서 리액트 16.8을 써본 사람이 없는데, 이제 쓰려고 합니다. 5월에 릴리즈된 16.8에서 hook이 나왔고, 2019년 11월에는 16.11까지 나와 있습니다. 16.7로 코딩하던 그대로 16.11에서 하면 안될 테니, 뭐가 달라졌는지 알고 가보려고 합니다.

16.8에서 달라진 점

16.8은 hook이 릴리즈된 업데이트입니다. 16.8에서 hook이 업데이트의 전부는 아니겠지만, hook이 리액트에 있어서 큰 변화인만큼, hook에 대해서만 집중하겠습니다.

Hook

먼저, 리액트가 어떻게 훅을 소개하는지 살펴볼까요? 리액트는 훅을 아래와 같이 소개합니다:

클래스를 작성할 필요 없이 상태와 여러 기능을 사용할 수 있습니다. 클래스는 장황하고, 사람을 혼동시키고, 리액트 컴포넌트는 함수 개념에 더 가깝습니다.

이미 작성된 코드를 바꿀 필요는 없습니다. Hook은 선택적으로 사용하면 됩니다. 이전 버전과 호환성을 깨지 않습니다. 리액트에서 클래스를 제거할 계획도 없습니다. Hook을 점진적으로 적용해 나갈 수 있고, 이미 있는 복잡한 클래스 컴포넌트를 리팩토링하는걸 권장하지 않습니다.

상태 관련 로직을 추상화하기 쉽습니다. 이전 코드는 고차 컴포넌트(HOC)과 render props 등을 활용하면 되지만, 컴포넌트를 재구성해야 한다는 점과 래퍼 헬(wrapper hell)이 만들어진단 단점이 있습니다.

특히 고차 컴포넌트는 래퍼 헬을 잘 만들기 때문에 저는 몇 번 사용해본 뒤 쓰지 않고 있으며, render props를 더 선호합니다.

더 이해하기 쉬운 컴포넌트를 만듭니다. 기존 라이프사이클 메소드는 관련없는 로직이 섞여있습니다. 이를 테면 데이터 가져오기와 이벤트 리스너 등록 등입니다. 이것들이 같은 공간에 있기 때문에 테스트하기에 어렵습니다. 훅을 통해 로직에 기반을 둔 작은 함수로 컴포넌트를 나눌 수 있습니다.

위 내용은 Hook 소개 – React 에서 더 자세히 읽을 수 있습니다.


다음으로 리액트 훅에서 사용하는 API들을 알아봤습니다. useState , useEffect , useRef , useContext , useCallback , useReducer , ..., 많지만 이것 말도고 더 있습니다. 다만, 이 API들이 제공하는 기능은, 앞서 말했듯이 이미 리액트에 있는 기능들이며, 완전히 새로운 기능이 생기거나 이미 있는 개념을 대체하는 것이 아닙니다. (전체 API는 다음 링크에서 볼 수 있습니다: Hooks API Reference – React )


다음으로 훅 사용 예시를 봅시다. 상태와 이펙트를 사용하는 간단한 훅의 예제를 알아볼 것입니다. 먼저 state hook입니다. Hook 개요 – React 버튼을 클릭하면 값이 증가하는 간단한 카운터 예시입니다.

import React, { useState } from 'react';

function Example() {
  // "count"라는 새 상태 변수를 선언합니다
  const [count, setCount] = useState(0);

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

여기서 useState 가 바로 훅입니다. 훅을 호출해서 함수 컴포넌트 안에 상태를 추가했습니다. 자세한 설명은 Hook 개요 – React 를 읽어보면 좋습니다.


컴포넌트 하나에서 여러 useState 를 사용해서 여러개의 상태를 사용할 수도 있습니다.

function ExampleWithManyStates() {
  // 상태 변수를 여러 개 선언했습니다!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  // ...
}

리액트는 매 렌더링마다 useState 가 쓰인 순서대로 실행할 겁니다. 아무래도 차후엔 훅들을 실행하는 순서가 중요한 때가 있을 것 같습니다.


이번에는 effect hook입니다. 사이드 이펙트를 일으킬 때 사용하면 됩니다. Hook 개요 – React 에선 사이드 이펙트의 예시로 컴포넌트 안에서 데이터를 가져오거나 구독하고, DOM을 직접 조작하는 작업 등을 들었습니다.

아래 예시는 리액트가 DOM을 업데이트한 뒤에 문서의 타이틀을 바꾸는 컴포넌트입니다.

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

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

  // componentDidMount, componentDidUpdate와 비슷합니다
  useEffect(() => {
    // 브라우저 API를 이용해 문서의 타이틀을 업데이트합니다
    document.title = `You clicked ${count} times`;
  });

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

기본적으로 리액트는 매 렌더링 뒤(DOM을 바꾼 뒤)에, 첫 번째 렌더링도 포함해서, 이펙트를 실행합니다. 이펙트는 컴포넌트 안에 선언 되어있기 때문에, props와 상태에 접근할 수 있습니다.


훅에 대한 설명은 다음 공식 문서에서 더 읽을 수 있습니다: Using the State Hook – React , Hook의 규칙 – React , Using the Effect Hook – React . 제 생각엔 여기까지 읽으면 훅을 쓰기 시작해도 될 것 같습니다. 또 About React Hook - Vingle Tech Blog - Medium 그리고 컴포넌트 제대로 만들기 | DailyEngineering 는 훅을 공부하기에 아주 좋은 내용이 있는 블로그입니다.

16.9에서 달라진 점

16.8은 hooks가 추가됐을 뿐이니 사실 당장 hook을 사용하지 않는다면 버전을 올려도 그리 달라져야 할게 많지 않을 것 같지만, 16.9는 deprecations 때문에 오히려 할일이 많을거란 생각이 듭니다.

React v16.9.0 and the Roadmap Update – React Blog

새로 Deprecation 한것

리네이밍 라이프사이클 메서드

  • componentWillMount UNSAFE_componentWillMount
  • componentWillReceiveProps UNSAFE_componentWillReceiveProps
  • componentWillUpdate UNSAFE_componentWillUpdate

옛 이름을 쓰면 작동은 하지만, 워닝을 보게 됩니다.

만일 컴포넌트를 마이그레이션하거나 테스트할 시간이 없다면, codemod 스크립트로 자동으로 리네임할 수 있습니다.

cd your_project
npx react-codemod rename-unsafe-lifecycles

(Note that it says npx , not npm . npx is a utility that comes with Node 6+ by default.)

codemod를 실행하면 옛날 이름을 바꿉니다. componentWillMount 같은 옛날 이름을 새 이름인 UNSAFE_componentWillMount 로 바꿉니다. CLI에서 확인 가능합니다.

Deprecating javascript: URLs

(링크 참조)

Deprecating “Factory” Components

(링크 참조)

새 기능

테스트를 위한 async act() 추가

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 0};
    this.handleClick = this.handleClick.bind(this);
  }
  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }
  handleClick() {
    this.setState(state => ({
      count: state.count + 1,
    }));
  }
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={this.handleClick}>
          Click me
        </button>
      </div>
    );
  }
}

위는 Counter 컴포넌트입니다.

import React from 'react';
import ReactDOM from 'react-dom';
import { act } from 'react-dom/test-utils';
import Counter from './Counter';

let container;

beforeEach(() => {
  container = document.createElement('div');
  document.body.appendChild(container);
});

afterEach(() => {
  document.body.removeChild(container);
  container = null;
});

it('can render and update a counter', () => {
  // 첫 render와 componentDidMount를 테스트
  act(() => {
    ReactDOM.render(<Counter />, container);
  });
  const button = container.querySelector('button');
  const label = container.querySelector('p');
  expect(label.textContent).toBe('You clicked 0 times');
  expect(document.title).toBe('You clicked 0 times');

  // 두 번째 render와 componentDidUpdate를 테스트
  act(() => {
    button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
  });
  expect(label.textContent).toBe('You clicked 1 times');
  expect(document.title).toBe('You clicked 1 times');
});

이런 방식 으로 테스트 할 수 있습니다.

act() 동작 방식에 대한 자세한 내용은 Testing Recipes – React 에 예시와 사용법과 함께 포함되어 있습니다.

퍼포먼스 측정을 위해 <React.Profiler> 추가

프로그래머틱하게 측정치를 얻을 수 있습니다.

프로파일러는 리액트 앱이 얼마나 자주 렌더하는지 그리고 렌더링의 코스트를 측정합니다. 앱이 얼마나 느린지, 그리고 메모이즈 등으로 최적화할 때 어떤 이점이 있는지 제공하는게 목적입니다.

프로파일러는 리액트 트리 아무데나 추가할 수 있습니다. 두 가지 props가 필요한데 하나는 /** @type {string} */ id , 다른 하나는 /** @function */ o nRender 콜백 입니다.

render(
  <Profiler id="application" onRender={onRenderCallback}>
    <App>
      <Navigation {...props} />
      <Main {...props} />
    </App>
  </Profiler>
);


나머지 16.9 업데이트는 링크에서 확인합시다: React v16.9.0 and the Roadmap Update – React Blog

16.10

리액트 16.8과 16.9는 리액트 블로그에서 무엇이 달라졌나 설명하는 글을 볼 수 있는데, 16.10과 11은 없습니다. 깃헙 리포지토리의 리액트 릴리즈 의 16.10과 16.11을 보면 큰 변화는 없다는걸 볼 수 있습니다. 아래는 16.10 설명 중 몇 가지입니다.

훅 업데이트가 메모이즈되지 않는 엣지 케이스 수정 .

언제 hydrate하는지 결정하는 휴리스틱을 수정해서, 업데이트 중에 잘못된 hydrate를 하지 않 게 합니다. hydrate() 는 서버 사이드 렌더링(SSR)을 하면 사용하는 함수입니다.

16.11

중첩된 리액트 컨테이너 안에서 mouseenter 핸들러가 두 번 트리거되는걸 수정 .

등이 있습니다.

마무리

리액트 프로젝트를 16.7에서 16.11로 업데이트한다면, hook과 profiler를 중점적으로 보면 될 것 같습니다.

Image Credits — Free Code Camp


그리고 글을 쓰는 중에 리액트 16.12 가 나왔는데, 내용은 다음과 같습니다.

React DOM

  • Fix passive effects ( useEffect ) not being fired in a multi-root app. ( @acdlite in #17347 )

React Is

  • Fix lazy and memo types considered elements instead of components ( @bvaughn in #17278 )


엄휘용 | 솔루션개발팀

eomhy's profile image

eomhy

2019-10-29

Read more posts by this author