[react] v16.3.0, 무엇이 바뀌었나?
2018-04-05

react v16.3.0 이 릴리즈 되었습니다. 개인적인 생각으로는 가장 큰 변화는 두가지 정도입니다. 첫번째는 몇몇 lifecycle method 가 deprecated 되었습니다. 그리고 새로운 Context API 가 추가되었습니다. 그 외에 몇가지 변화를 요약해보면 아래와 같습니다.

  • 새로운 Context API
  • 새로운 Refs API
  • lifecycle 메서드 변경
  • 새로운 Strict mode

자세한 사항은 공식사이트 포스트를 참조하세요.

Context API

간단히 말해서 Context 는 앱 전체에 공통으로 사용할 데이터를 담는 역할을 합니다. 저는 개발하면서 한번도 사용해본적은 없습니다만, react-redux, react-router 등의 react 관련 핵심 라이브러리에서 Context 가 사용되고 있습니다. 리덕스를 사용하고 있다면 아래와 같은 코드 조각을 본적이 있을 것입니다.

<Provider store={store}>
  <App />
</Provider>

react-router 의 경우에는 브라우저 history 관리등을 위해 Context 를 사용하고 있습니다.

this.props.history.push('/list')

props 로 하위 컴포넌트에 데이터를 넘기는 전통적인 방식은 간단한 어플리케이션 개발에는 아무런 문제가 없습니다. 하지만 어플리케이션이 복잡해지고 하위 컴포넌트의 단계가 많아질수록 이런식의 상태 관리는 개발난이도가 높아질뿐 아니라 유지보수 측면에서도 문제를 야기할 수 있습니다. 이럴때 Context 는 하나의 대안이 될 수 있습니다.

이전 버전의 react 에도 Context API 가 있었습니다. 다만, 공식적인 API 라기보다는 실험적인 수준이었기 때문에 사용을 권장하지 않았습니다. 이번 v16.3.0 버전에서 공식적으로 Context API가 발표되었기 때문에 지금까지 Context 가 어떻게 쓰였고 앞으로 어떤 방식으로 사용 가능할지 알아볼 필요가 있겠습니다.

기존 Context API

실험적인 방법이라고하지만, 주요한 라이브러리에는 이미 context 를 사용하고 있습니다.

App.jsx

export default class App extends Component {
  render() {
    return (
      <OldProvider userId="bono" nickName="보노">
        <OldConsumer />
      </OldProvider>
    )
  }
}

OldProvider.jsx

OldProvider에서 context 데이터를 미리 만들어줍니다.

export default class OldProvider extends Component {
  static childContextTypes = {
    userId: PropTypes.string,
    nickName: PropTypes.string,
  }
  getChildContext = () => {
    return {
      userId: this.props.userId,
      nickName: this.props.nickName,
    }
  }
  render() {
    return <div>{this.props.children}</div>
  }
}

OldConsumer.jsx

Provider로 감싸진 컴포넌트 어디에서든 this.context로 context 데이터에 접근이 가능합니다.

export default class OldConsumer extends Component {
  render() {
    const { userId, nickName } = this.context
    return (
      <div>
        <h1>{userId}</h1>
        <h2>{nickName}</h2>
      </div>
    )
  }
}

new Context API

위의 기능을 새로 발표된 Context API 를 사용하여 구현합니다. 파일 개수가 하나더 증가했습니다. 하지만 생성, 공급, 소비와 같이 관심사가 확실하게 분리되기 때문에 훨씬더 구조적인 모습입니다.

App.jsx

export default class App extends Component {
  render() {
    return (
      <Provider userId="bono" nickName="보노">
        <Consumer />
      </Provider>
    )
  }
}

Context.jsx

ProviderConsumer에서 공통으로 Context 객체를 사용하기 위해 Context.tsx 를 별도로 생성합니다.

export default React.createContext()

Provider.jsx

Context 객체에 데이터를 넣어주는 역할을 합니다.

export default class Provider extends Component {
  render() {
    return (
      <Context.Provider value={this.props}>
        {this.props.children}
      </Context.Provider>
    )
  }
}

Consumer.jsx

Context 의 데이터를 사용하는 부분입니다. 좀 특이한 것은 <Context.Consumer> 컴포넌트 바로 아래가 React Element 를 리턴하는 함수 형태로 되어 있다는 것입니다. 이는 Render Props라는 패턴입니다.

export default class Consumer extends Component {
  render() {
    return (
      <Context.Consumer>
        {value => (
          <div>
            <h1>{value.userId}</h1>
            <h2>{value.nickName}</h2>
          </div>
        )}
      </Context.Consumer>
    )
  }
}

Consumer 에서 context 를 사용할때마다 Render Props 패턴으로 context 의 데이터를 가져와야하는 부분이 거슬리는데, 이는 HOC를 이용해 context 의 value 를 prop 으로 가져오도록 구현하면 됩니다(자세한 구현은 생략).

Context API 를 사용한 위 두가지 방법은 각각 아래와 같은 형태로 출력됩니다.

context

Refs API

ref 는 reference 의 줄임말로, 특정 컴포넌트를 참조합니다. 컴포넌트를 참조하기 때문에 컴포넌트 내의 변수나 상태, 메서드를 사용할 수 있습니다.

그 동안 Ref 사용시 몇가지 문제점이 있었는데, 이를 해결하기위해 새로운 Ref API 가 나왔습니다. 역시 이전의 ref 사용법과 비교해 보겠습니다.

old Ref

기존의 ref 는 string 형태로 정의합니다(아마 string 형태로 정의하는게 여러 문제를 야기하지 않았을까 추측해봅니다). ref 의 대상이 되는 컴포넌트에 ref 속성을 만들고 텍스트로 이름을 만들어줍니다. 그리고 this.refs.xxx와 같이 사용하면 됩니다.

export default class OldRef extends Component {
  componentDidMount() {
    this.refs.oldRef.focus()
  }
  render() {
    return (
      <div>
        <span>oldRef: </span>
        <input type="text" ref="oldRef" />
      </div>
    )
  }
}

new Ref

공식문서의 예제와 동일한 형태입니다. 위의 OldRef 와 비교해보면, 우선 ref 가 string 에서 객체형태로 바꼈습니다. 그리고 createRef() 함수로 ref 를 만든 다음 실제 타겟 컴포넌트의 ref 속성에 해당 객체를 할당합니다. ref 를 사용할때는 생성자함수에서 만들어둔 this.inputRef를 그대로 이용합니다.

export default class NewRef extends Component {
  constructor(props) {
    super(props)
    this.inputRef = React.createRef()
  }
  componentDidMount() {
    this.inputRef.current.focus()
  }

  render() {
    return (
      <div>
        <span>newRef: </span>
        <input type="text" ref={this.inputRef} />
      </div>
    )
  }
}

새로 만들어진 ref API 에 한가지 한계가 있는데 HOC 로 만들어진 컴포넌트의 속성에 ref가 있으면 리턴되는 컴포넌트에서 이 ref 를 가져오지 못합니다. 이 문제를 해결하기 위해서는 HOC 내부에서 forwardRedRef를 이용하면 리턴되는 컴포넌트에 무사히 전달할 수 있습니다. 자주 사용할 것 같지 않은 API 이므로(나만 그런가..) 코드설명 없이 넘어가겠습니다.

lifecycle 메서드

그 동안 will 관련 메서드가 deprecated 될것이라는 말이 비공식적으로 여러번 나왔습니다. 새로운 버전에서 드디어 이 말이 현실이 됐습니다. componentWillUnMount를 제외한 나머지 will 메서드들이 공식적으로 deprecated 되었으며, 기존 메서드를 사용하기 위해서는 UNSAFE_ prefix 를 붙여줘야합니다. (ex. UNSAFE_componentWillMount, ㅎㄷㄷ) 쓰지 말라는 얘기죠.

그리고 두가지 새로운 메서드가 나왔습니다.

getDerivedStateFromProps는 componentWillReceiveProps 의 대안으로 사용할 수 있는 메서드입니다. 다만 차이가 있다면, 첫째로는 메서드 이름에서 알 수 있듯이 return 값이 존재(해야만)한다는 것이고, 또 static 메서드라는 점입니다. 이 메서드를 사용할때는 반드시(should) update 된 state 를 리턴해주어야합니다. 그리고 static 이기 때문에 this 키워드를 사용할 수 없습니다. 그래서 componentWillReceiveProps 의 완전한 대체제라고 말하기 어렵습니다. 만약 기존에 사용하던 componentWillReceiveProps 메서드에 this가 사용되고 있다면, getDerivedStateFromProps 가 아닌 componentDidUpdate를 대신 사용해야할 것입니다.

getSnapshotBeforeUpdate는 가장 최근에 렌더링된 결과의 상태를 가져옵니다. 공식사이트에서 예를 든것처럼 새로 렌더링 되기전 scroll 상태를 기억하고 있다가 렌더링 된 후에 다시 이전 scroll 위치로 돌아올때 유용하게 사용될 수 있을것 같습니다.

정리

이번 v16.3.0 에서 가장 큰 부분은 Context API 와 lifecycle 메서드의 변화입니다. Context API 는 react 주요 라이브러리에서 많이 사용되고 있었던 만큼 각 라이브러리에 새로운 Context API 가 적용될 것입니다. 그리고 공식적으로 나온 API 이기 때문에 컴포넌트 depth 가 큰 곳에는 적절히 사용할 수도 있을것 같습니다. lifecycle 메서드는 리액트의 앞으로의 방향인 Async Mode에 맞게 재조정된것이라 생각합니다. 여담이지만, react 의 인기가 높은 만큼 변화도 빨라서 지속적인 관심이 없으면 따라가기 벅찰지도 모르겠습니다. 앞으로도 꾸준한 관심이 필요할 것 같습니다.

참고자료