리액트(React) 버전 16+ 총정리

리액트(React) 15버전이 나온지(2016년 4월 7일) 꼬박 1년 반 만(2017년 9월 26일)에 16 버전이 릴리즈 되었다. 그리고 글을 쓰고 있는 현재 최근 릴리즈된 버전은 16.3.2이다. 0.14.x 버전에서 15버전까지 외부 api의 변경은 많이 없었지만, 16버전에서는 코어를 재작성(rewrite) 하는 것은 물론, 다양한 기능이 추가 되었고, 앞으로 변경 될 api, component lifecycle등이 많아 졌다. 꾸준히 리액트 업데이트 사항을 봐왔다면 문제가 없겠지만, 그렇지 않았다면 변경된 부분들이 꽤 있어서 사뭇 헷갈릴 수 있는 부분들이 분명히 있을 것이라고 생각한다. 그런 의미에서 리액트 버전 16으로 넘어오면서 변경된 점들 그리고 앞으로 변경될 것들에 대해 알아보자. 또한, 다음 메이저 버전 업데이트(17)를 준비하기위해 필요한 부분에 대해서 한번 정리를 해보도록 하자.
presentation

New API (React)

Fragments (16.0+, 16.2+)

기존 15 버전까지는 컴포넌트의 루트(root)는 한개만 허용이 되었다. 그래서 의미없이 컴포넌트를 감싸주는 컴포넌트를 추가하는 경우가 종종 생겼다(<div></div>). 하지만, fragments api가 추가 되면서 이제 더이상 의미없는 래핑 컴포넌트가 필요 없게 되었다.
15.x 버전:
render() { return ( <div> <ChildA /> <ChildB /> <ChildC /> </div> ); }
16.x 버전:
render() { return [ <ChildA />, <ChildB />, <ChildC />, ]; }
16.2 버전에서는 더욱 개선된 fragment api를 선보였다:
render() { return ( <> <ChildA /> <ChildB /> <ChildC /> </> ); }
또는 Fragment 컴포넌트를 import 해서 사용 할 수도 있다:
import React, { Fragment } from 'react'; ... render() { return ( <Fragment> <ChildA /> <ChildB /> <ChildC /> </Fragment> ); }

Official Context API(16.3+)

리액트의 context api는 기존에도 사용 할 수 있었지만 몇가지 문제점이 있었고, 리액트 공식문서 실험적인 api이기 때문에 왠만하면 사용하지 말라는 안내도 있다. 하지만, 이번 16.3 릴리즈에서 새로 추가된 context api의 경우 그런 문제를 제거했고, api 사용법 자체도 완전하게 변화 된것은 물론 공식적으로 context api를 지원하게 되었다.
먼저, 기존 context api 예제를 보자:
import React, { Children, Component } from 'react';
import PropTypes from 'prop-types';
class ThemeProvider extends Component {
static childContextTypes = {
theme: PropTypes.object,
};
getChildContext() {
return {
theme: {
button: {
borderRadius: 3,
border: 0,
color: 'white',
height: 48,
padding: '0 30px',
boxShadow: '0 3px 5px 2px #ddd',
fontSize: 20,
},
color: {
primary: 'linear-gradient(to right, #0cebeb, #20e3b2, #29ffc6)',
secondary: 'linear-gradient(to right, #f7971e, #ffd200)',
},
},
};
}
render() {
return Children.only(this.props.children);
}
}
export default ThemeProvider;
context 선언부에서는 getChildContext 메소드를 사용하여 context에서 사용될 값을 정의를 해준 후,
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class StatefulButtonWithContext extends Component {
static propTypes = {
color: PropTypes.string,
};
static contextTypes = {
theme: PropTypes.object,
};
render() {
const { color, ...rest } = this.props;
const { theme } = this.context;
return (
<button
{...rest}
style={{
...theme.button,
background: theme.color[color],
}}
/>
);
}
}
export default StatefulButtonWithContext;
사용하는 컴포넌트에서는 contextTypes를 선언해서 context의 어떤 값을 사용 할지를 정의 하는 식으로 api가 제공 되었다.
아래는 새로 변경된 context api의 모습이다. createContext 메소드로 context를 만들면, Provider 컴포넌트와 Consumer 컴포넌트가 생기는데, function as a child 방법을 사용해서 context에 선언된 값을 받아서 사용 할 수 있게 됐다(예제: 공식문서):
const ThemeContext = React.createContext('light');

class ThemeProvider extends React.Component {
state = {theme: 'light'};

render() {
return (
<ThemeContext.Provider value={this.state.theme}>
{this.props.children}
</ThemeContext.Provider>
);
}
}

class ThemedButton extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{theme => <Button theme={theme} />}
</ThemeContext.Consumer>
);
}
}

New Lifcecyle Methods (16.0+, 16.3+)

리액트 16 버전에선 componentDidCatch라는 새로운 lifecycle이 생겼고, 16.3에서는 getDerivedStateFromPropsgetSnapshotBeforeUpdate lifecycle이 스태틱(static) 메소드로 추가 되었다. 아래에서도 얘기하겠지만, 이 두 lifecycle(getDerivedStateFromPropsgetSnapshotBeforeUpdate) 추가 되면서 기존의 componentWillMount , componentWillReceiveProps , and componentWillUpdate 향후 메이저 버전 업데이트(17+)에서는 제거될 예정이고, UNSAFE_ prefix가 붙은 lifecycle만 제공될 예정이다(한마디로 이제 저 lifecycle은 사용하지 말라는 얘기다).
Error Boundaries(16.0+): componentDidCatch(error, info)
리액트 팀에서 에러 바운더리(error boundaries)라고 이름지은 이 lifecycle은 children 컴포넌트에서 에러가 생겼을 때, 이를 componentDidCatch에서 잡아내서 적절한 조치를 취할 수 있도록 해주는 유용한 lifecycle이다. 리액트에 이 기능이 추가된 것은 특히 자바스크립트로 만든 spa 앱의 경우 자바스크립트 에러가 하나 터지면 앱 전체가 먹통 되는 경우가 있는데, 이를 방지하기 위해 이 lifecycle을 추가 했다. 사용법은 아래와 같다(예제: 공식문서):
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } componentDidCatch(error, info) { // Display fallback UI this.setState({ hasError: true }); // You can also log the error to an error reporting service logErrorToMyService(error, info); } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong.</h1>; } return this.props.children; } }

<ErrorBoundary> <MyWidget /> </ErrorBoundary>
(16.3+): getDerivedStateFromProps(nextProps, prevState)
새로 추가된 getDerivedStateFromProps 는 static 메소드이다. 이 메소드는 componentWillReceiveProps 를 대체 할 메소드이다.
(16.3+): getSnapshotBeforeUpdate(prevProps, prevState) dom이 업데이트 되기 전에 호출되는 static 메소드이다. componentDidUpdate와 함께 사용하여 기존의 componentWillUpdate lifecycle을 대체 할 수 있다.

기타

(16.3+): React.createRef()
기존(16.3 이전 버전)까지는 ref를 사용 하기 위해 아래와 같이 사용 했다:
render() { return <div ref={div => this.divRef = ref} /> }
추가된 React.createRef() api에서는 이렇게 사용 할 수 있다:
constructor(props) { super(props); this.divRef = React.createRef(); } render() { return <div ref={this.divRef} /> }
(16.3+): React.forwardRef()
16.3+ 이전의 컴포넌트를 예를 들어보자:
function FancyButton(props) {
return (
<button className="FancyButton">
{props.children}
</button>
);
}
이런 FancyButton이라는 컴포넌트가 있고, 부모 컴포넌트에선 아래와 같이 사용 할 것이다:
<FancyButton ref={ref}>Click me!</FancyButton>;
이러한 컴포넌트가 있을 때, 부모 컴포넌트에서 native dom 메소드를 다루기 위해서(button에 focus를 준다던지..)는 FancyButton 컴포넌트에서 api를 만들어 준다던지 ref를 접근 할 수 있도록 해주어야(아니면, ReactDOM.findDOMNode를 사용 한다던지..) 가능 했지만, 새로 추가된 React.forwardRef 메소드를 사용하면 이런 부분을 깔끔하게 해결 할 수 있게 됐다.
아래와 같이 사용하면 부모 컴포넌트에서 ref로 직접 dom 접근이 가능하다 (예제: 공식문서):
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

presentation

New API (ReactDOM)

createPortal

createPortal의 경우 15.x 버전까지는 unstable_ prefix가 붙은채로 공식적으로 지원하지는 않는 api 였다:ReactDOM.unstable_createPortal()
하지만, 16버전 부터는 공식적으로 ReactDOM.createPortal()이 제공된다. 아래의 예제를 보자(참고: 공식문서):
render() {
return ReactDOM.createPortal(
this.props.children,
document.querySelector('.anyValidDom'),
);
}
createPortal의 첫번째 argument는 렌더 가능한 리액트 자식, 엘리먼트, 스트링(string) 또는 fragment가 가능하고, 두번째 argument는 dom 엘리먼트이다. 이제 앞으로 Modal, Popup을 만드는게 훨씬 편해졌다!

Server Side Rendering(SSR)

리액트의 셀링 포인트 중에 하나는 SSR이었다. 하지만, 성능적으로 문제가 많았고 blocking api이기 때문에 서버 렌더링 중엔 서버가 freeze되기 때문에 사용상에 제한이 많았다. 그런대도, 애석하게도 리액트 팀에서도 예전에는 크게 ssr을 개선하려는 노력이 없었다. 하지만, 16 버전이 릴리즈 되면성 SSR 부분을 완전 새로 작성하면서 node의 stream기반 서버 렌더링 api가 추가 되었고(non-blocking), 성능 개선도 많이 되었다. 16버전에서 추가된 서버 렌더링 메소드는 노드 스트림 기반의 ReactDOMServer.renderToNodeStream()ReactDOMServer.renderToStaticNodeStream() api이고, 기존의 renderToString, renderToStaticMarkup은 그대로 지원이 된다. 클라이언트에선 서버렌더링시 render대신 ReactDOM.hydrate 를 통해 렌더링 하도록 api가 추가 되었다.

presentation

New Packages

리액트 팀에서 공식으로 제공하는 새로운 패키지들도 추가 되었다.

React Is

이 패키지는 어떤 값이 리액트(React) 엘리먼트 타입인지 체크해주는 패키지 이다.

React Lifecycles Compat

리애트 16.3 버전이하에서도 새로 추가된 getDerivedStateFromProps, getSnapshotBeforeUpdate를 사용 할 수 있도록 해주는 폴리필 패키지이다.

Create Subscription

비동기 렌더링(async rendering)을 위해 외부 데이터 소스(ex: rxjs, html input ...)를 안전하게 구독(subscribe) 할 수 있도록 도와주는 패키지이다.

presentation

제거 되거나 될 API

Legacy Context API

기존의 getChildContext와 contextTypes를 사용하던 api는 다음 메이저 버전(17)에서는 제거될 예정이다. 16.x 버전까지는 사용 가능하다.

ReactDOM.unstable_createPortal()

위에서도 다뤘지만, 이젠 unstable_ prefix없이 사용 가능하다.

Lifecycles

componentWillMount, componentWillReceiveProps, componentWillUpdate는 16.x 버전에서는 dev 모드에선 warning을 다음 메이저 버전(17)에서는 제거 될 예정이다. 이 lifecycle 대신 getDerivedStateFromPropsgetSnapshotBeforeUpdate 을 사용 하는게 좋고, 굳이 다음 메이저 버전에서도 사용 해야 한다면 UNSAFE_ prefix가 붙은 lifecycle을 사용해야 한다.