[React Native 공식 Doc 가이드 #4] State React Native

- 이 글은 React 공식 홈페이지 Docs https://facebook.github.io/react-native/ 에 기반하여 작성한 글입니다
- State
Component를 제어하는 2가지 형태의 data가 있다. 앞에서 배운 props와 이번 절에서 소개할 state이다. props는 부모로부터 정해지고 나면 컴포넌트의 생명주기동안에는 고정되어있는 data이다.
이렇게 고정되어있는 data가 아니라 변할 가능성이 있는 data를 다루고 싶다면 state를 사용해야 한다.
일반적으로 state는 생성자에서 초기화되고(constructor 함수에서 this.state에 Object를 할당한다.), 변경을 원하는 시점에 setState 함수를 호출함으로써 state를 변경한다.
예를 들어보자. 만약 화면에서 깜빡거리는 글자(blink text)를 만들기 원한다고 가정하자. 해당 글자는 component 생성시점에 결정된 후 변하지 않으므로 prop을 이용한다.
해당 글자가 현재 표시되는지 아닌지는 시간이 흐름에 따라서 변하는 사항이므로 state를 이용해야 한다.
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React, { Component } from 'react';
import { AppRegistry, Text, View } from 'react-native';

class Blink extends Component {
constructor(props) {
super(props);
this.state = { isShowingText: true };

// Toggle the state every second
setInterval(() => (
this.setState(previousState => (
{ isShowingText: !previousState.isShowingText }
))
), 1000);
}

render() {
if (!this.state.isShowingText) {
return null;
}

return (
<Text>{this.props.text}</Text>
);
}
}

export default class BlinkApp extends Component {
render() {
return (
<View>
<Blink text='I love to blink' />
<Blink text='Yes blinking is so great' />
<Blink text='Why did they ever take this out of HTML' />
<Blink text='Look at me look at me look at me' />
</View>
);
}
}

// skip this line if using Create React Native App
AppRegistry.registerComponent('AwesomeProject', () => BlinkApp);
위의 코드에서는 setInterval로 타이머를 사용했지만, 실제 앱에서 사용하는 경우는 거의 없다. 일반적으로는 서버로부터 새로운 data를 조회하거나 유저가 입력할 때 state를 세팅하는 경우가 많다.
또 data를 제어하기 위해서 Redux나 Mobx같은 state 컨테이너를 사용할 수도 있다. state 컨테이너를 사용하는 경우라면 setState를 직접 호출하는 경우보다는 state를 변경할 때 Redux나 Mobx를 사용하게 될 것이다.
setState가 호출될 때, BlinkApp 은 Component를 다시 그린다. 타이머에서 setState를 주기적으로 호출하기 때문에, component는 타이머 주기마다 다시 그릴 것이다.
state는 React 에서 동작할때와 똑같이 작동하기 때문에, state를 다루는법을 좀 더 자세히 알고 싶다면 https://reactjs.org/docs/react-component.html#setstate 를 참조하면 된다. 여태까지 작성한 예제가 기본적인 스타일의 텍스트로만 되어있기 때문에 지루할 것이다. 텍스트를 좀 더 꾸며보기 위해서 다음에는 Style을 배워보도록 하자.

[React 공식 Advanced Doc] Forwarding Refs React

- 이 글은 React 공식 홈페이지 Docs https://facebook.github.io/react-native/ 에 기반하여 작성한 글입니다
- Forwarding Refs
Ref forwarding은 component에서 그의 자식들에게 ref를 자동으로 전파하는 기법이다. 일반적인 Component에서 사용할일은 없겠지만 특정 상황에서는 유용하게 사용된다. 특히 재사용가능한 component 라이브러리같은 경우가 그렇다.
ref를 사용하는 일반적인 시나리오를 살펴보자.
- Forwarding refs to DOM components
DOM의 button을 rendering 하는 FancyButton component가 있다고 가정해보자.
function FancyButton(props) {
return (
<button className="FancyButton">
{props.children}
</button>
);
}

ReactDOM.render(
<FancyButton>
fancy button
</FancyButton>,
document.getElementById('root')
);
React component는 상세적으로 어떻게 구현했는지 그리고 Rendering 을 어떻게 했는가를 숨기고 있다. (예제에서 <FancyButton> 이라는 태그만 보고서 어떻게 그려질지 상세적으로 알 수 없다.)
일반적인 경우에는 FancyButton 내부의 button DOM 요소를 참조할 경우는 거의 없다. 이런 특성은 Component가 DOM 구조에 과도하게 의존하는 것을 막아준다는 점에서 장점이 된다.
이런 은닉화가 FeedStory나 Comment같은 어플리케이션 레벨의 Component에서는 바람직하지만, FancyButton이나 MyTextInput과 같이 재사용빈도가 높은 "leaf" component 에서는 불편한 경우가 있을 수도 있다.
이런 Component들은 application에서 정규 DOM(button, input)와 비슷하게 다루어지는 경우가 많은데, 해당 Component에서 DOM 노드의 focus나 selection, animations 같은 이벤트를 제어할수도 있다.
Ref forwarding은 어떤 Component가 ref 변수명으로 참조를 받아서(C로 치면 포인터를 받아서), 그 참조를 자식으로 전파("forward")시킨다.
말이 어려울 수도 있다. 위의 말이 React 에서 어떻게 코드로 표현되는지 예제를 살펴보자.
아래 예제에서 FancyButton은 React.forwardRef를 사용했는데 ref 변수를 전달받아서, rendering 요소중에 DOM button 요소에 포워딩한다.
const FancyButton = React.forwardRef((props, ref) => {
return (
<button ref={ref} className="FancyButton">
{props.children}
</button>
);
});

const ref = React.createRef();

ReactDOM.render(
<FancyButton ref={ref}>
fancy button
</FancyButton>,
document.getElementById('root')
);
이런 방법으로 FancyButton을 사용하는 component들은 FancyButton 안에 있는 button DOM 노드를 참조할 수 있다. (DOM button에 직접 사용하는것처럼 사용하면 된다.)
꽤 많은 과정이 있었다. 무슨일이 있었는지 살펴보자.
1. 처음에 React.createRef를 이용하여 React 참조를 생성하고, ref 변수에 할당하였다.
2. 1.에서 할당한 ref를 <FancyButton ref={ref}> 와 같이 JSX의 attribute로 전달하였다.
3. React.forwardRef 함수의 2번째 인자로 ref를 전달하였다.
4. 이 ref 인자를 <button ref={ref}>로 JSX attribute로 결정하였다.
5. ref.current가 <button> DOM 노드를 가리키도록 하였다.
주의: 두번째 ref 인자는 React.forwardRef를 호출하여 component를 정의했을때만 존재한다. 일반적인 function이나 class component 는 ref를 인자로 받을 수 없고, props에서 ref를 사용하는것도 불가능하다.
또, Ref forwarding을 반드시 DOM component 만 사용해야 한다는 제약사항이 없다. class component의 instance도 참조할 수 있다.
- Note for component library maintainers
component 라이브러리에서 forwardRef를 도입하려고 한다면, breaking change(SW의 한 부분을 바꾸면 다른 component가 동작하지 않는 현상.)를 검토한 후 새로운 major 버전을 배포해야 한다.
라이브러리가 다르게 동작하면(어떤 ref가 할당되었는지, 어떤 type이 export 되는지), app들이 동작하지 않을수도 있고, 다른 라이브러리들이 원래 동작하던 현재 라이브러리의 동작에 의존적일수도 있기 때문이다.
경우에 따라서 React.forwardRef의 사용이 권장되지 않는 경우가 있다. ref forwarding 때문에 라이브러리 동작이 변경될 수도 있고, React가 업그레이드 되면서 app이 동작하지 않을 수도 있다.
- Forwarding refs in higher-order components
이번 테크닉은 higher-order components(HOCs)와 함께 사용할때 유용할 수 있다. 
우선 props를 console 로그고 남기는 HOC component를 에제를 살펴보자.
function logProps(WrappedComponent) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}

render() {
return <WrappedComponent {...this.props} />;
}
}

return LogProps;
}
"logProps" HOC 는 자신을 감싸고 있는 Component에게 모든 props을 넘긴다. 그리고 결과적으로 rendering되는 결과는 변화가 없다.
이 HOC를 이용하여 "fancy button" component가 넘겨받은 모든 props의 로그를 출력할 수 있다.
 1
2
3
4
5
6
7
8
9
10
11
class FancyButton extends React.Component {
focus() {
// ...
}

// ...
}

// Rather than exporting FancyButton, we export LogProps.
// It will render a FancyButton though.
export default logProps(FancyButton);
위 예제에서 한 가지 주의할 점이 있다. refs는 전달되지 않는다.
왜냐하면 ref는 prop이 아니기 때문이다. 일종의 key와 같이 React에 의해 특별하게 다루어지기 때문이다.
만약 ref를 HOC에 추가하고 싶다면 ref는 감싸진 component(Fancy Button)가 아니라 가장 바깥의 component(LogProps)를 참조해야 한다.
위의 말이 헷갈릴수도 있다. ref로 원래 참조하려고 생각했던 FancyButton component가 아니라 실제로는 LogProps를 참조하게 된다.
 1
2
3
4
5
6
7
8
9
10
11
12
13
import FancyButton from './FancyButton';

const ref = React.createRef();

// The FancyButton component we imported is the LogProps HOC.
// Even though the rendered output will be the same,
// Our ref will point to LogProps instead of the inner FancyButton component!
// This means we can't call e.g. ref.current.focus()
<FancyButton
label="Click Me"
handleClick={handleClick}
ref={ref}
/>;
다행히 React.forwardRef API를 이용해서 명시적으로 FancyButton 내부를 참조할 수 있다.
React.forwardRef는 props와 ref를 전달받고 React 노드를 반환하는 render 함수를 사용한다.
 1
2
3
4
5
6
7
8
9
10
11
12
13
import FancyButton from './FancyButton';

const ref = React.createRef();

// The FancyButton component we imported is the LogProps HOC.
// Even though the rendered output will be the same,
// Our ref will point to LogProps instead of the inner FancyButton component!
// This means we can't call e.g. ref.current.focus()
<FancyButton
label="Click Me"
handleClick={handleClick}
ref={ref}
/>;
- Displaying a custom name in DevTools
React.forwardRef는 render 함수를 인자로 받는다. React 개발자 도구는 이 함수를 ref를 어떤 fowarding하는 component를 보여줄지를 결정하는데 사용한다.
예를들면 아래 component는 개발자도구에서 "ForwardRef"로 표시될 것이다.
1
2
3
const WrappedComponent = React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
만약 render 함수에 이름을 정의하면 개발자 도구는 해당 이름을 포함해서 표시한다.("ForwardRef(myFunction)")
1
2
3
4
5
const WrappedComponent = React.forwardRef(
function myFunction(props, ref) {
return <LogProps {...props} forwardedRef={ref} />;
}
);
또 function의 displayName 속성을 이용해서도 이름을 정할 수 있다.
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function logProps(Component) {
class LogProps extends React.Component {
// ...
}

function forwardRef(props, ref) {
return <LogProps {...props} forwardedRef={ref} />;
}

// Give this component a more helpful display name in DevTools.
// e.g. "ForwardRef(logProps(MyComponent))"
const name = Component.displayName || Component.name;
forwardRef.displayName = `logProps(${name})`;

return React.forwardRef(forwardRef);
}



[React 공식 Advanced Doc] Fragment React

- 이 글은 React 공식 홈페이지 Docs v16.8.3 (https://reactjs.org/docs) 에 기반하여 작성한 글입니다.
- Fragments
React 에서 1 Component 안에서 복수개의 element들을 반환하는 형태가 일반적이다.
이때 반환하는 element 부분에서 최상위 노드가 1개 여야 하는데, "Fragment"는 여러개의 element 노드를 강제로 <div>같은 DOM 노드로 엮을 필요 없이 1개의 그룹으로 엮어준다.
1
2
3
4
5
6
7
8
9
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}
React.Fragment를 선언할 때 줄임문법(일종의 sugar syntax)가 있긴 하지만, 공식 Doc 에서는 일반적인 tool 모두가 지원하지는 않는다고 하는거 보니 쓰기를 권장하지는 않는 것 같다.
- Motivation
앞에서 일반적으로 Component 에서 복수개의 element들을 반환한다고 했다. 아래 예제를 한번보자.
 1
2
3
4
5
6
7
8
9
10
11
class Table extends React.Component {
render() {
return (
<table>
<tr>
<Columns />
</tr>
</table>
);
}
}
<Columns />는 HTML에서 <table>을 구성하기 위해 여러개의 <td> element 들을 반환할 것이다.
React 에서는 component를 rendering할 때 최상위 노드는 1개 여야 하므로 아래와 같은 형태로 오류가 나지 않게 만들어야 되는데, 하지만 이렇게 되면 HTML의 <table>이 유효하지 않게 된다.
 1
2
3
4
5
6
7
8
9
10
class Columns extends React.Component {
render() {
return (
<div>
<td>Hello</td>
<td>World</td>
</div>
);
}
}
최상위 노드는 정말 1개 여야 할까? <div>를 없애고 <td>2개를 반환하려고 시도해보자.
1개의 root 노드만 반환하라고 에러를 뱉는다.
<Table />의 결과는 아래와 같다.
1
2
3
4
5
6
7
8
<table>
<tr>
<div>
<td>Hello</td>
<td>World</td>
</div>
</tr>
</table>
사실 React DOC 에서는 결과값이 위와 같다고 하지만 실제로 해보면 위와같은 결과는 나오지 않는다. 친절하게 에러를 뱉는다.
분명 최상위의 노드는 하나여야 하는데 <td>상위에는 붙일 노드가 마땅치 않다 이를 어떻게 해결해야 할까?
"Fragment"를 쓰면 해결할 수 있다.
-Usage
 1
2
3
4
5
6
7
8
9
10
class Columns extends React.Component {
render() {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
);
}
}
위의 Component가 rendering 되면 아래와 같이 올바른 <Table /> 결과값이 된다.
1
2
3
4
5
6
<table>
<tr>
<td>Hello</td>
<td>World</td>
</tr>
</table>
-Short Syntax
fragments를 선언할 수 있는 간단한 문법이 있다. 마치 비어있는 tag처럼 선언하면 된다.
 1
2
3
4
5
6
7
8
9
10
class Columns extends React.Component {
render() {
return (
<>
<td>Hello</td>
<td>World</td>
</>
);
}
}
<></> 태그는 여태까지 써왔던 element처럼 사용하면 되긴하지만 key나 attribute를 지원하지 않는다.
그리고 주의할 점이 하나 있다. 많은 tool 들은 아직 이 문법을 지원하지 않는다. 많은 tool들이 지원하게 될때까지는 <React.Fragment> 태그를 명시적으로 쓰길 권장한다.
- Keyed Fragments
<></>문법이 아닌 <React.Fragment>와 같이 명시적으로 fragment를 선언했다면 아마 key를 같이 쓰게 될 것이다.
collection을 fragment의 배열로 맵핑시키는 예제를 통해서 무슨말인지 이해해보자.
 1
2
3
4
5
6
7
8
9
10
11
12
13
function Glossary(props) {
return (
<dl>
{props.items.map(item => (
// Without the `key`, React will fire a key warning
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
))}
</dl>
);
}
위의 코드에서 key를 사용하지 않으면 React에서 warning을 뱉을 것이다. 현재는 React.Fragment에 attribute중 key만 사용할 수 있다.
공식 Doc 에서는 event handler와 같은 추가적인 attribute를 추가할 계획이 있다고 한다.

[React 공식 Advanced Doc] Error Boundaries React

- 이 글은 React 공식 홈페이지 Docs v16.8.3 (https://reactjs.org/docs) 에 기반하여 작성한 글입니다.
- Error Boundaries
이전 버전까지는 component 안에서 발생한 Javascript error가 React의 내부상태를 망가뜨리거나, emit, cryptic, errors 에러들의 원인이었다.
이런 에러들은 app 코드에서는 미리알 수 있는 에러들이었지만, React에서는 Component안에서 그 에러들을 다룰 수 있는 방법이 없었고, 복구기능도 없었다.
- Introducing Error Boundaries
어떤 특정 UI에서 발생한 JavaScript 에러가 전체 app을 망가뜨려서는 안된다. React를 쓰는 사용자들에게 이런 문제를 해결해주기 위해서, React 16 에서는 "error boundary"라는 새로운 개념을 추가하였다.
Error boundaries는 하나의 React Component인데, 자식 컴포넌트 어느곳에서 JavaScript 에러가 발생하더라도 이런 에러들을 로그로 남기고, 에러 상태의 화면을 표시하는 대신 대체 콘텐츠(React element 형태)를 표시하는 기능을 제공한다.
Error boundaries는 rendering중에 자식 Component들의 lifecycle 함수들이나 생성자들에서 발생한 에러를 탐지한다.
주의: Error boundaries 는 다음과 같은 에러는 처리하지 않는다.
- Event handler
- 비동기 코드
- Server side rendering
- Error boundaries 자기 자신의 에러
error boundary는 class component 여야 하는데 조건이 있다. 아래의 lifecycle method들중 적어도 하나 이상을 구현해야 한다.
- static getDerivedStateFromError()
- componentDidCatch()
static getDerivedStateFromError 함수는 에러가 발생하면 대체 콘텐츠를 rendering 하는데 사용된다.
componentDidCatch 함수는 에러 정보를 로그로 남긴다.
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React from 'react';

class ErrorBoundary extends React.Component{

constructor(props){
super(props);
this.state = {
hasError: false
};
}

static getDerivedStateFromError(error){
return {
hasError: true
};
}

componentDidCatch(error, info){
console.log("log service:");
console.log(error);
}

render() {

if(this.state.hasError){
return <h1>Something went wrong.</h1>;
}

return this.props.children;
}

}

export default ErrorBoundary;
1
2
3
4
5
6
7
8
9
if(this.state.isShowOtherComponent){
return (
<React.Suspense fallback={<div>Loading ..</div>}>
<ErrorBoundary>
<OtherComponent errorTrigger={true}/>
</ErrorBoundary>
</React.Suspense>
);
}
위와 같이 Error Boundary를 정의한 후 ErrorBoundary를 정규 Component 처럼 사용하면 rendering중 발생하는 error를 잡을 수 있다. 
Test를 위해 errorTrigger에 강제로 true 값을 주고, OtherComponent에서는 해당값이 true 일때 강제로 throw new Error("Test Error"); 코드를 주었다.(해당 부분 생략)
버튼을 누르면 에러를 강제로 던지는데 componentDidCatch에서 console.log 함수를 이용해 찍혔는지 개발자도구로 확인해보자.
Error boundaries는 JavaScript의 catch {} 처럼 작동하지만, 오직 class component에 대해서만 작동한다.
실질적으로 error boundary component를 정의하고 application 전반적으로 사용하게 될 것이다.
하지만 error boundaries가 자신의 자식 component들의 에러들에 대해서만 동작한다는 것을 꼭 기억해야 한다.
error boundary는 자기 자신의 에러에 대해서 처리할 수 없다. 만약 error boundary가 에러 메시지를 rendering 하는데 실패하면, 에러는 가까운 상위의 error boundary에 전파된다.
- Where to Place Error Boundaries
error boundaries에 대한 분포도는 프로그래머가 어떻게 정의하냐에 따라 달려있다.
어떤 사람은 server-side framework 처럼 최상위 레벨의 route component에 "Something went wrong" 문구를 단순하게 보여주기로 할 수도 있고,
어떤 사람은 각 widget 별로 error boundaries를 감싸서 rendering에 실패한 component외의 나머지 component들은 정상적으로 rendering 하게 할 수도 있다.
- New Behavior for Uncaught Errors
React 16 에서는 위의 소제목처럼 중요한 변경사항이 있다. error boundary 에서 처리되지 않은 에러들이 있다면 React component tree 전체를 마운팅 하지 않는다.
React측은 위와 같은 결정을 내리기까지 고민한것으로 보인다. 위와같이 결정한 이유는 깨지는 UI를 보여주느니 완전히 그 UI를 보여주지 않는게 낫다고 판단했다고 한다.
예를 들면 Messanger에서 UI를 깨진채로 보여준다면 잘못된 사람에게 메시지를 전송할 수 있거나, 결재앱에서 잘못된 합계를 보여주는니 차라리 보여주지 않는게 낫다는 논지에서 위와같은 결정을 했다고 한다.
위와 같은 이유 때문에 만약 React 16 으로 버전을 마이그레이션 한다면 조심해야 한다.
error boundaries를 구현해서 app이 잘못되었을 때 사용자에게 더 나은 UX를 제공해야 한다.
예를 들면 Facebook Messenger는 sidebar의 내용과 정보 panel, 대화 log, message 입력각각에 대해 error boundaries를 따로 구현하였다고 한다.
만약 위의 UI 파트중에 어떤 component가 잘못된다 하더라도 나머지는 여전히 정상적으로 동작할 것이다.
- Component Stack Traces
React 16은 개발자도구 console에 rendering 하면서 발생한 모든 에러들에 대해서 출력한다.(application에서 무시하게 처리를 하더라도 출력한다.)
또 에러메시지와 JavaScript stack뿐만 아니라 component stack trace도 제공한다. 이 기능덕분에 component tree 어디에서 에러가 발생했는지 알 수 있다.
또 component stack trace의 해당 파일이름과 몇 번째 줄인지도 볼 수 있다. Create React App project에서는 이 설정이 default 이다.
만약 Create React App을 이용하여 프로젝트를 생성하지 않았다면 Babel 환경설정에 직접 plugin을 추가해야 한다. 그리고 주의사항이 있는데 원래 개발용으로 만든기능이라고 production에서는 해당기능을 반드시 제거해야 한다.
주의: stack trace에서 보이는 Component 이름은 Function.name 속성을 참조한다. 만약 구버전의 브라우저나 기기를 지원해야 한다면 이를 지원하지 않으므로 function.name-polyfill과 같은 Function.name polyfill(해당 기능을 지원할 수 있게 해주는 대체 프로그램)을 추가해야 한다.
아니면 component에 displayName을 명시적으로 선언할 수도 있다.
- How About try/catch?
try/catch는 에러처리에 있어서 강력하긴 하지만 코드를 강제로 작성해야 한다.
1
2
3
4
5
try {
showButton();
} catch (error) {
// ...
}

반면 React component는 정의하는 시점에 어떤것이 rendering 되어야 하는지 알 고 있다.
Error boundaries는 React의 기존의 선언문법을 변경하지 않고서 코더가 원하는대로 에러처리에 대해 동작한다.
Component tree 어딘가에서 setState함수에 의해 componentDidUpdate 함수에서 에러가 발생했다 하더라도, 가장 가까운 error boundary로 정확히 에러를 전파한다.
- How About Event Handlers?
Error boundaries는 event handler의 내부에러를 처리하진 않는다.
React는 event handler에서 발생한 error를 복구하는데 error boundaries를 필요로 하지 않는다. render method나 lifecycle method와 달리 event handler는 rendering하는 도중 일어나는 동작이 아니기 때문이다.
만약 에러가 발생해도 React는 화면에 어떤 요소가 rendering rendering되어야 하는지 알고 있기 때문이다.
event handler에서 에러를 처리하려면 우리가 알고 있듯이 javascript 의 try/catch 문을 사용해야 한다.
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
try {
// Do something that could throw
} catch (error) {
this.setState({ error });
}
}

render() {
if (this.state.error) {
return <h1>Caught an error.</h1>
}
return <div onClick={this.handleClick}>Click Me</div>
}
}
위의 예제는 JavaScript의 try/catch 문을 사용했지만 error boundaries는 사용하지 않았다.
- Naming Changes from React 15
React 15 에서는 unstable_handleError 라는 메소드 이름으로 error boundaries 기능을 제한적으로 지원하였다. 이 메소드는 사장되었고 대신 16버전 부터는 componentDidCatch 로 변경해서 사용해야 한다.
공식홈에서는 자동으로 code를 migration 하고 싶다면 codemod를 사용하라고 권장하고 있다.(나는 사용해보지 않았다.)
https://github.com/reactjs/react-codemod#error-boundaries


[React 공식 Advanced Doc] Code-Splitting React

- 이 글은 React 공식 홈페이지 Docs v16.8.3 (https://reactjs.org/docs) 에 기반하여 작성한 글입니다.
- Bundling
대부분의 React app들은 Webpack 이나 Browserify 같은 tool 을 이용한 bundling된 파일들로 구성되어 있다. 여기서 bundling 이란 import 된 파일들을 1개의 파일로 합치는 프로세스를 말한다. 그리고 이때 생성된 1개의 파일을 "bundle" 이라 한다. 이렇게 "bundle" 이 만들어 지게 되면 webpage 에서 전체 app을 한번에 로딩될 수 있다.
1
2
3
import { add } from './math.js';

console.log(add(16, 26));
1
2
3
export function add(a, b) {
return a + b;
}
위와 같이 1개의 export 된 함수와 이를 import 해서 log를 찍는 App이 있다고 할 때 bundle 을 만들면 아래와 같은 형태가 된다.
1
2
3
4
5
function add(a, b) {
return a + b;
}

console.log(add(16, 26));
만약 Create React App이나 Next.js, Gatsby 혹은 이와 비슷한 tool을 사용했다면, 만든 app을 bundle로 만들기 위해서 Webpack 이 설치되어 있을 것이다.
위와 같은 tool 을 이용하지 않았다면 Webpack 의 가이드 및 설치 문서를 읽고 직접 bundling을 구성해야 한다. 
- Code Splitting
Bundling 을 하는건 이점이 많지만 조금만 생각해보면 의문이 생긴다. 대규모 Project 라면 app의 크기가 상당할 것이고, 그렇다면 만들어 지는 bundle의 크기는 1번에 import 하기 너무 크지 않을까라는 의문이 자연스럽게 들 것이다.
특히 만약 다른 라이브러리들까지 이용한다면 app이 생성된 bundle을 load할 때 너무 많은 시간이 걸리지 않는지 주의할 필요가 있다.
bundle이 너무 커지는 현상을 막기 위해서 문제가 발생하기 전에 분할하는 것이 좋다. Code-Splitting은 이미 Webpack 이나 Browserify 같은 bundler 들에서 제공하는 기능이다. Code-Splitting을 이용하면 여러개의 bundle 파일들을 실행시점에 로딩할 수 있다.
우리가 만든 app 이 한꺼번에 loading 되는게 아니라 "lazy-load"(이 챕터에서 뿐만 아니라 프로그래밍에 있어 일반적인 용어이다.)되어 유저가 현재 필요한 부분들만 로딩하므로 상당한 속도개선이 될 수 있다.
여러개로 분할된 파일들의 loading 시간의 총합이 1개의 파일을 로딩한 시간보다 적어지는건 아니다. 하지만 User가 사이트에 접속할 때 초기 로딩시간을 줄여주고, 만약 유저가 필요한 부분의 code가 아니라면 해당 부분의 파일을 로딩하지 않음으로써 속도 향상을 꾀할 수 있다.
- import()
code-splitting을 쓰기 가장 좋은 방법은 dynamic import() 구문을 사용하는 것이다.
Before:
1
2
3
4
5
6
import { add } from './math.js';

clickButton() {
let addResult = add(16, 26);
this.setState({test: addResult});
}
After:
1
2
3
4
5
6
clickButton() {
import('./math.js').then((math) => {
let addResult = math.add(16, 26);
this.setState({test: addResult});
});
}
위의 화면을 보려면 개발자도구를 키고 네트워크 탭을 클릭하면 된다. Before 에서는 2.chunk.js 하나만 다운로드를 받는데, dynamic import() 구문으로 변경하고 나서는 0.chunk.js를 다운받고 버튼을 클릭할 때 1.chunk.js를 한번 더 다운받는다.
이로써 dynamic import() 구문을 사용하면 "bundle"로써 1개의 파일을 한꺼번에 다운받는게 아니라 필요한 시점에 일부 파일을 나눠서 다운받는걸 확인할 수 있다.
주의: dynamic import() 구문은 ECMAScript 이긴 하지만 언어 표준은 아니다.
Webpack 이 해당 구문을 만나면 code-splitting을 자동으로 시작한다. 만약 Create React App을 사용해서 React를 시작했다면 이미 설정이 되어 있을 것이다.(Next.js 에서도 지원한다.)
Webpack 을 수동설정 할 거라면 Webpack에 가이드가 나와있다. 
Babel 은 dynamic import 구문을 파싱할 수는 있지만 변형시키지는 않으므로 플러그인을 설치해 줘야 한다.
- React.lazy
React.lazy 와 Suspense 는 아직 server-side rendering을 지원하지 않는다. 만약 server rendered app 에서 code-splitting을 사용하고 싶다면 Loadable Component를 추천한다. 
React.lazy 함수를 사용하면 Component를 동적으로 import 하여 rendering 할 수 있다.
Before:
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import OtherComponent from './OtherComponent';

class TestRender extends React.Component{

constructor(props){
super(props);

this.state = {
isShowOtherComponent: false
};

this.showOtherComponent = this.showOtherComponent.bind(this);
this.loadOtherComponent = this.loadOtherComponent.bind(this);
}

showOtherComponent() {
this.setState({
isShowOtherComponent: true
});
}

loadOtherComponent(){
if(this.state.isShowOtherComponent){
return <OtherComponent />
}

return null;
}

render() {
return (
<div>
<div>
<button onClick={this.showOtherComponent}>add other component</button>
</div>
{this.loadOtherComponent()}
</div>
);
}
}
After:
 1
2
3
4
5
6
7
8
9
10
11
12
13
const OtherComponent = React.lazy(() => {import('./OtherComponent')});

loadOtherComponent(){
if(this.state.isShowOtherComponent){
return (
<React.Suspense fallback={<div>Loading ..</div>}>
<OtherComponent />
</React.Suspense>
);
}

return null;
}
Suspense는 OtherComponent가 rendering 되는 중간에 대체 콘텐츠를 보여주기 위한 태그이다. fallback attribute에 보여줄 대체 React element를 주면된다. Suspense를 구현하지 않으면 콘솔 에러가 날 것이다. 다음절에서 설명하니 모르겠다면 지금은 그냥 개념만 알아도 괜찮다.
코드를 After: 처럼 바꾸고 개발자도구에서 네트워크 탭을 킨 후 비교를 해보면 dynamic import 예제에서와 같이 Component도 lazy loading 할 수 있다는 사실을 알 수 있다.
React.lazy 는 반드시 동적으로 import() 를 호출해야 한다. 그리고 호출된 import는 default export 되는 React component 모듈의 Promise 를 반환해야 한다. 아래에서도 나오겠지만 현재 React.lazy는 default export만 지원하기 때문에 export {} 형태의 모듈은 import 할 수 없다. 주의하자.
Promise를 반환하는지 눈으로 확인해 보자.
여러 속성중 의미를 알 수 없는 프로퍼티가 있는데 _ctor() 이라는 함수를 호출하면 Promise를 반환하고, __proto__를 펼치면 then 함수가 보일 것이다. 공식문서의 설명이 맞는 것 같다.
- Suspense
만약 OtherComponent를 포함하는 모듈이 아직 MyComponent 가 rendering 될 때까지 로딩되지 않았다면, 로딩중과 같이 로딩되는 동안 보여줄 화면을 마련해야 한다. 이때 Suspense 라는 Component 를 사용함으로써 구현할 수 있다.
구현할 수 있다고 했지만 사실은 구현하지 않으면 console에 에러를 뿜는다. 구현 해야 한다.
 1
2
3
4
5
6
7
8
9
10
11
const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
fallback prop은 Component가 로딩중일 때 rendering할 React element를 인자로 받는다. Suspense component 의 위치는 lazy 를 지정한 Component 상위라면 어디든지 선언가능하다. 그리고 1개의 Suspense component 안에서 여러개의 lazy component를 선언할 수 있다.
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</div>
);
}
- Error boundaries
만약 네트워크 이슈와 같은 이유로 다른 모듈이 로딩되는데 실패하면, error가 발생한다. 이런 상황에서 좋은 UX를 위해 이런 error들을 다룰 수 있고, Error boundaries 를 이용하면 복구하는 기능을 구현할 수 있다.
일단 Error Boundary를 만들어 놓으면 네트워크 에러 같은 에러의 상태를 보여주기 위해 lazy component들 상위 어느곳에든 Component를 위치시킬 수 있다. 자세한 설명은 Error boundaries 섹션이 따로 있으니 개념만 알도록 하자.
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import MyErrorBoundary from './MyErrorBoundary';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

const MyComponent = () => (
<div>
<MyErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</MyErrorBoundary>
</div>
);
- Route-based code splitting
code splitting 코드를 app 어디에 둬야할지 결정하는 건 약간 애매할 수 있다. 당연히 bundle 들을 균등하게 분할 할 수 있는 위치에 두는 것이 좋지만 그렇다고 해서 UX를 방해할 수준이어서는 안된다.
이렇게 애매할 때는 route를 쓰는것이 해결책이 될 수 있다. 일반적으로 web에서 페이지 전환이 일어날 때 많은 시간이 걸린다. 보통 App은 한번에 전체 페이지를 re-rendering 하려는 경향이 있어서 사용자는 페이지안에서 UX적으로 다른 element와 상호작용 한다고 느끼지 못할 것이다.
아래 코드는 React Router와 React.lazy를 이용하여 route 기반으로 코드를 작성한 것이다.
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense, lazy } from 'react';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</Suspense>
</Router>
);
- Named Exports
React.lazy는 현재 default exports 만 지원한다. 만약 named export 형태로 모듈을 사용하고 싶다면, 모듈을 새롭게 하나 만들어서 그 모듈안에서 사용할 component를 default로 다시 export를 하는 형식으로 사용해야 한다.
이런 특징은 사용하지 않는 component들을 가져오지 않는 사상에 기반한다.(이를 treeshaking 이라고 한다. 나무를 흔들어서 필요 없는 것들을 떨어뜨리는, 코딩에서는 필요없는 코드를 없애는 작업)
ManyComponents.js
1
2
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
MyComponent.js
1
export { MyComponent as default } from "./ManyComponents.js";
MyApp.js
1
2
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));
MyComponent.js 를 intermediate module 이라고 표현하고, 이런 트릭으로 export default만 import 가능한 React.lazy 문제를 해결하였다.


1 2 3 4