도움말 - 글감 수집하기 (인용)

도움말 - 부분 리뷰 작성하기

상세한 리액트 Higher Order Components 설명(React Higher Order Components in depth)

개요

이 글은 HOC 패턴을 활용하려는 고급 사용자를 대상으로한다. 만약 당신이 리액트(React)를 처음 접한다면 리액트 공식문서를 읽어보시는 것이 좋다.

Higher Order Components는 여러 리액트 라이브러리에서 매우 유용하다고 입증 된 훌륭한 패턴이다. 이 글에서는 HOC가 무엇인지, 사용자가 할 수있는 작업과 그 한계 및 구현 방법을 자세히 검토 할 것이다.

부록에서 우리는 HOC의 핵심은 아닐 수 있지만 관련된 토픽을 검토 할 것이다.

이 글은 매우 상세한 내용이므로 놓친 내용을 발견 했다면, 필요한 사항을 변경하도록 하겠다.

이 글은 ES6 지식을 전제로한다.

자, 시작해보자!

2016 년 8 월 업데이트

일본어 버전: 

관심 가져 주셔서 감사합니다!

Higher Order Components는 무엇일까?

Higher Order Component는 다른 컴포넌트를 감싸는 리액트 컴포넌트 일뿐이다.

이 패턴은 일반적으로 클래스 팩토리 (예, 클래스 팩토리입니다!) 인 함수로 구현되며, haskell에서 pseudo code로 표현한다면 아래와 같다:

hocFactory:: W: React.Component => E: React.Component

여기서 W (WrappedComponent)는 래핑 된 React.Component이고 E (Enhanced Component)는 반환되는 새로운 HOC, React.Component이다.

"랩핑(wrap)"부분은 다음 두 가지 중 하나의 의미를 갖는다:

  1. Props Proxy : HOC는 WrappedComponent W에 전달되는 props을 조작한다.
  2. Inheritance Inversion : HOC는 WrappedComponent W를 확장한다.

이 두 가지 패턴을 더 자세히 살펴 보도록 하자.

HOC로 무엇을 할 수 있을까?

높은 수준의 HOC를 사용하면 다음과 같은 것을 할 수 있다:

  • 코드 재사용, 로직 및 부트스트랩 추상화
  • Render Highjacking
  • 상태 추상화 및 조작(manipulation)
  • props 조작(manipulation)

이 항목은 곧 더 자세하게 알아 볼 것이다. HOC를 사용하여 할 수 있는 것과 제한되는 기능에 대해서 HOC 구현 방법을 통해 알아 볼 것이다.




HOC 팩토리 구현

이 섹션에서는 리액트에서 HOC를 구현하는 두 가지 주요 방법, 즉 Props Proxy (PP)와 Inheritance Inversion (II)에 대해 알아 볼 것이다. 이것들은 둘 다 WrappedComponent를 조작하는 다양한 방법이다.

Props Proxy

Props Proxy (PP)는 다음과 같은 방식으로 간단하게 구현된다:

function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return <WrappedComponent {...this.props}/>
}
}
}

여기에서 중요한 부분은 HOC의 render 메서드가 리액트 엘리먼트인 WrappedComponent를  리턴한다는 것이다. 우리는 또한 HOC가 받는  props를 전달하므로 Props Proxy라는 이름을 사용한다.

노트:

<WrappedComponent {...this.props}/>
// is equivalent to
React.createElement(WrappedComponent, this.props, null)

이것들은 둘 다 리액트 엘리먼트(reconciliation 프로세스에서 리액트가 렌더링해야 하는 것을 설명)를 생성한다. 리액트 엘리먼트 vs 컴포넌트에 대한 자세한 내용을 알고 싶다면 Dan Abramov의 이 글을 참고하고 reconciliation 프로세스에 대해 더 자세히 알고 싶다면 이 문서를 참고하자.

Props Proxy로 무엇을 할 수 있을까?

  • props 조작
  • Refs를 통해 인스턴스 접근
  • 상태 추상화
  • WrappedComponent를 다른 요소로 래핑

props 조작

WrappedComponent로 전달되는 props를 읽고 추가하고 편집하고 제거 할 수 있다.

중요한 props를 삭제하거나 수정 할 때는 조심하자. Higher Order props가 WrappedComponent의 props를 손상시키지 않도록 네임 스페이스를 이용하는 것이 좋다.

예제: 새 props 추가. 앱의 현재 로그인 한 사용자는 this.props.user를 통해 WrappedComponent에서 사용할 수 있다.

function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
const newProps = {
user: currentLoggedInUser
}
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}

Refs를 통해 인스턴스 접근

이것 (WrappedComponent의 인스턴스)에 ref로 접근 할 수 있지만, ref를 계산하기 위해서는 WrappedComponent의 전체 초기 렌더링 프로세스가 필요하다. 즉, HOC render 메소드에서 WrappedComponent 엘리먼트를 반환해야한다. 리액트가 reconciliation 프로세스를 수행하게하고 그 후 WrappedComponent 인스턴스에 대한 참조를 가져온다.

예제 : 다음 예제에서는 ref를 통해 WrappedComponent의 인스턴스 메소드와 인스턴스 자체에 접근하는 방법을 살펴 본다.

function refsHOC(WrappedComponent) {
return class RefsHOC extends React.Component {
proc(wrappedComponentInstance) {
wrappedComponentInstance.method()
}
render() {
const props = Object.assign({}, this.props, {ref: this.proc.bind(this)})
return <WrappedComponent {...props}/>
}
}
}

WrappedComponent가 렌더링되면 ref 콜백이 실행되고 WrappedComponent의 인스턴스에 대한 참조를 갖게 된다. 이것은 인스턴스 props 읽기/추가 및 인스턴스 메소드 호출에 사용할 수 있다.

상태 추상화

WrappedComponent에 props 및 콜백을 제공하여 똑똑한 컴포넌트가 멍청한 컴포넌트를 처리하는 방식과 매우 유사하게 상태를 추상화 할 수 있다. 자세한 정보는 멍청한, 똑똑한 컴포넌트를 참고하자.

예제: 다음 상태 추상화 예제에서 우리는 이름 입력 필드의 값과 onChange 핸들러를 단순하게 추상화한다. 이것이 일반적이지 않기 때문에 단순하게라고 했지만 여기서 보여주고자하는 포인트를 확인하자.

function ppHOC(WrappedComponent) {
return class PP extends React.Component {
constructor(props) {
super(props)
this.state = {
name: ''
}
this.onNameChange = this.onNameChange.bind(this)
}
onNameChange(event) {
this.setState({
name: event.target.value
})
}
render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onNameChange
}
}
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}

이것을 다음과 같이 사용할 것이다 :

@ppHOC
class Example extends React.Component {
render() {
return <input name="name" {...this.props.name}/>
}
}

이 input은 자동적으로 controlled input이 된다.

좀 더 일반적인 양방향 데이터 바인딩 HOC는 이 링크를 확인하자.

WrappedComponent를 다른 엘리먼트로 래핑하기

스타일, 레이아웃 또는 다른 용도로 WrappedComponent를 다른 컴포넌트 및 엘리먼트로 래핑 할 수 있다. 일부 기본 사용법은 일반 부모 컴포넌트 (부록 B 참조)로 수행 할 수 있지만 이전에 설명한 것처럼 HOC가 더 많은 유연성을 제공한다.

예제: 스타일링 목적

function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return (
<div style={{display: 'block'}}>
<WrappedComponent {...this.props}/>
</div>
)
}
}
}

Inheritance Inversion

상속 Inversion (II)는 다음과 같이 간단하게 구현된다:

function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
return super.render()
}
}
}

보다시피, 리턴된 HOC 클래스 (Enhancer)는 WrappedComponent를 확장(extend)한다. Enhancer 클래스를 확장하는 WrappedComponent 대신 Enhancer에서 수동적으로 확장하므로 Inheritance Inversion이라고한다. 이러한 방식으로 이것들 사이의 관계는 역전이 된다.

Inheritance Inversion을 사용하면 HOC가 이것을 통해 WrappedComponent 인스턴스에 접근 할 수 있다. 즉, 상태(state), props, 컴포넌트 lifecycle hook 및 렌더링 메소드에 접근 할 수 있다.

lifecycle hook은 HOC와 관련 없는 리액트와 관련된 것이기 때문에 자세히 설명하지 않겠다. 그러나 II에서는 WrappedComponent의 새로운 lifecycle hook를 만들 수 있다. super.[lifecycleHook]을 호출하여 WrappedComponent를 손상시키지 않도록 하자.

Reconciliation 프로세스

더 진행하기 전에 몇 가지 이론을 요약 해보자.

리액트 엘리먼트는 리액트가 reconciliation 프로세스를 실행할 때 렌더링 될 내용을 설명한다.

리액트 엘리먼트는 문자열과 함수 두 가지 유형이 될 수 있다. String Type React Elements (STRE)는 DOM 노드를 나타내고 Function Type React Elements (FTRE)는 React.Component를 확장하여 생성 된 컴포넌트를 나타낸다. 엘리먼트와 컴포넌트에 대한 자세한 내용은 이 게시물을 참고하자.

FTRE는 리액트의 reconciliation 프로세스에서 전체 STRE 트리로 처리된다 (최종 결과는 항상 DOM 요소이다).

이것은 매우 중요하며 Inheritance Inversion Higher Order Components가 전체 하위 트리를 확인하도록 보장하지 않음을 의미한다.

Inheritance Inversion High Order Components는 전체 하위 트리를 보장하지 않는다.

이것은 Render Hijacking을 다룰 때 중요하다.

Inheritance Inversion으로 무엇을 할 수 있을까?

  • Render Highjacking
  • 상태 조작

Render Highjacking

HOC는 래핑 된 구성 요소의 렌더링 출력을 제어하고 이를 사용하여 모든 종류의 작업을 수행 할 수 있기 때문에 Render Hijacking이라고 한다.

Render Hijacking으로 다음 작업을 할 수 있다:

  • render에서 출력 한 모든 리액트 엘리먼트에서 props를 읽고, 추가하고, 편집하고, 제거한다.
  • render가 출력 한 리액트 엘리먼트 트리를 읽고 수정한다.
  • 조건부로 엘리먼트 트리를 표시한다.
  • 스타일을 목적으로 엘리먼트의 트리를 래핑한다 (Props Proxy와 같이).

*render는 WrappedComponent.render 메서드를 의미한다.

리액트 컴포넌트는 받은 props를 편집 할 수 없으므로 WrappedComponent 인스턴스의 props를 수정하거나 생성 할 수는 없지만 render 메서드에서 출력되는 엘리먼트의 props를 변경할 수는 있다.

이전에 연구 한 것처럼 II HOC는 전체 하위 트리 처리를 보장하지 않는다. 이는 Render Highjacking 기술에 대한 몇 가지 제한을 의미한다. Render Highjacking을 사용하면 WrappedComponent의 렌더링 메서드가 출력하는 엘리먼트의 트리를 조작 할 수 있다는 것이다. 그 요소의 트리가 Function Type React Component를 포함하면 그 컴포넌트의 자식을 조작 할 수 없다. (실제로 리액트의 reconciliation 프로세스에 의해 화면에 렌더링 될 때까지 지연된다.)

예제 1: 조건부 렌더링. 이 HOC는 this.props.loggedIn이 true가 아닌 경우 WrappedComponent가 렌더링 할 내용을 정확하게 렌더링한다. (HOC가 loggedIn prop를받을 것이라고 가정 할 때)

function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
if (this.props.loggedIn) {
return super.render()
} else {
return null
}
}
}
}

예제 2: 렌더링에 의해 출력 된 리액트 엘리먼트 트리 수정.

function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
const elementsTree = super.render()
let newProps = {};
if (elementsTree && elementsTree.type === 'input') {
newProps = {value: 'may the force be with you'}
}
const props = Object.assign({}, elementsTree.props, newProps)
const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children)
return newElementsTree
}
}
}

이 예제에서 WrappedComponent의 최상위 엘리먼트가 인풋을 가지면 값을 "may the force be with you"로 변경된다.

여기에서 할 수 있는 것은 다양하다. 전체 엘리먼트 트리에서 모든 엘리먼트의 props을 바꿀 수 있다. 이것이 바로 Radium에서 사용하는 방식이다 (Radium에 대한 추가 정보).

참고: Props Proxy를 사용하여 Render Highjack을 할 수는 없다.
WrappedComponent.prototype.render를 통해 렌더링 메서드에 액세스 할 수는 있지만 WrappedComponent 인스턴스와 해당 props을 mock하고 리액트에 의존하는 대신 직접 컴포넌트 수명주기를 처리해야한다. 여기서 이것을 다루는 것은 불필요하며 Render Highjacking을 원한다면 Props Proxy 대신 Inheritance Inversion을 사용해야한다. 리액트는 내부적으로 컴포넌트 인스턴스를 처리하며 인스턴스를 처리하는 유일한 방법은 this 또는 refs를 사용하는 것이다.

상태 조작

HOC는 WrappedComponent 인스턴스의 상태를 읽고, 편집하고, 삭제할 수 있으며 필요한 경우 더 많은 상태를 추가 할 수도 있다. 하지만, WrappedComponent의 상태를 어지럽히고(messing) 있다는 사실에 주의하자. 대체로 HOC는 상태를 읽거나 추가하는 것으로 제한되어야하며, 후자에서는 WrappedComponent의 상태를 어지럽히지 않도록 네임스페이스를 이용하는 것이 좋다.

예제: WrappedComponent의 props 및 상태에 접근하여 디버깅

export function IIHOCDEBUGGER(WrappedComponent) {
return class II extends WrappedComponent {
render() {
return (
<div>
<h2>HOC Debugger Component</h2>
<p>Props</p> <pre>{JSON.stringify(this.props, null, 2)}</pre>
<p>State</p><pre>{JSON.stringify(this.state, null, 2)}</pre>
{super.render()}
</div>
)
}
}
}

이 HOC는 WrappedComponent를 다른 엘리먼트로 랩핑하고 WrappedComponent의 인스턴스 props 및 상태를 표시한다. Ryan FlorenceMichael Jackson이 JSON.stringify 트릭을 가르쳐주었다. 여기서 디버거의 전체 소스를 볼 수 있다.


네이밍(Naming)

HOC를 사용하여 컴포넌트를 래핑하면 개발 및 디버깅 할 때 영향을 줄 수있는 원본 WrappedComponent의 이름이 손실된다.

일반적인 방법은 WrappedComponent의 이름을 취하고 그 앞에 HOC의 이름을 사용자 정의하는 것이다. 다음은 React-Redux에서 가져온 것이다.

HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`
//or
class HOC extends ... {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
...
}

getDisplayName 함수는 다음과 같이 정의된다:

function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName ||
WrappedComponent.name ||
‘Component’
}

recompose lib가 이미 이 기능을 제공하기 때문에 실제로 직접 재 작성할 필요는 없다.


사례 연구

React-Redux

React-Redux는 React의 공식 Redux 바인딩이다. 이것이 제공하는 함수 중 하나는 store를 listen하고 나중에 정리하는 데 필요한 모든 부트스트랩을 처리하는 connect 함수 이다. 이것은 Props Proxy 방식으로 구현되어 있다.

순수 Flux로 작업 한 적이 있다면 하나 이상의 store에 연결된 모든 리액트 컴포넌트에 store 리스너(listener)를 추가 및 제거하고 필요한 부분을 선택하기위한 많은 부트스트랩이 필요하다는 것을 알고 있을 것이다. 따라서 React-Redux 구현은 모든 부트스트랩을 추상화하기 때문에 매우 유용하다. 더 이상 직접 작성할 필요가 없다!

Radium

Radium은 인라인 스타일의 CSS pseudo selectors를 사용하여 인라인 스타일의 기능을 향상시킨 라이브러리다. 인라인 스타일이 좋은 이유는 다른 주제지만, 많은 사람들이 그것을 사용하고 Radium과 같은 라이브러리가 실제로 이러한 방법을 사용한다. 인라인 스타일에 대해 더 알고 싶다면이 Vjeux의 프리젠테이션을 확인하자.

그렇다면 Radium은 hover 같은 CSS pseudo selectors를 어떻게 가능하게 했을까? hover와 같은 CSS pseudo selectors를 시뮬레이트하기 위해 올바른 이벤트 리스너 (새 props)를 주입하기 위해 Render Highjacking을 사용하는 Inheritance Inversion 패턴을 구현한다. 이벤트 리스너는 리액트 엘리먼트 props를 처리하기위해 주입된다. Radium은 WrappedComponent의 render 메소드로 출력 된 모든 엘리먼트 트리를 읽어야하며 스타일 props이 있는 엘리먼트를 찾을 때마다 이벤트 리스너를 추가한다. 간단히 말하면, Radium은 엘리먼트 트리의 props을 수정한다 (Radium이 실제로하는 일은 좀 더 복잡하지만 포인트를 이해하자)

Radium은 정말 간단한 API를 제공한다. 사용자가 눈치 채지 않고 수행하는 모든 작업을 고려하면 꽤 인상적이다. 이것은 HOC의 힘을 엿볼 수 있다.

부록 A: HOC 및 파라미터

다음 내용은 선택 사항이며 건너 뛰어도 된다.

때로 HOC에서 파라미터를 사용하는 것이 유용하다. 이것은 위의 모든 예제에서 중급의 자바스크립트 개발자에게는 익숙하겠지만, 이것을 간단하게 확인하자.

예제: 사소한(trivial) Props Proxy가있는 HOC 파라미터. 여기서 중요한 것은 HOCFactoryFactory 함수다.

function HOCFactoryFactory(...params){
// do something with params
return function HOCFactory(WrappedComponent) {
return class HOC extends React.Component {
render() {
return <WrappedComponent {...this.props}/>
}
}
}
}

이렇게 사용 할 수 있다:

HOCFactoryFactory(params)(WrappedComponent)
//or
@HOCFatoryFactory(params)
class WrappedComponent extends React.Component{}

부록 B: 부모 컴포넌트와의 차이점

다음 내용은 선택 사항이며 건너 뛰어도 된다.

부모 컴포넌트는 일부 children이 있는 리액트 컴포넌트이다. 리액트에는 컴포넌트의 자식을 접근하고 조작하기위한 API가 있다.

예제: children 컴포넌트에 접근하는 부모 컴퍼넌트.

class Parent extends React.Component {
render() {
return (
<div>
{this.props.children}
</div>
)
}
}
}
render((
<Parent>
{children}
</Parent>
), mountNode)

이제 HOC와 몇 가지 중요한 세부 사항을 비교하여 상위 컴포넌트가 수행 할 수있는 작업과 수행 할 수 없는 작업을 검토하자.

  • Render Highjacking (Inheritance Inversion에서 볼 수 있듯이)
  • 내부 props 조작 (Inheritance Inversion에서 볼 수 있듯이)
  • 상태 추상화. 그러나 단점이 있다. 명시적으로 hook을 만들지 않으면 부모 컴포넌트의 상태에 접근 할 수 없다. 이로 인해 유용성이 제한된다.
  • 리액트 엘리먼트 래핑(wrap). 이것은 부모 컴포넌트가 HOC보다 자연스러운 유일한 사용 사례 일 수 있다. HOC도 이 작업을 수행 할 수 있다.
  • children 컴포넌트 조작에는 몇 가지 어려움이 있다. 예를 들어 자식이 단일 루트 엘리먼트를 가지고 있지 않은 경우 모든 요소를 ​​래핑하기 위해 추가 엘리먼트를 추가해야하므로 이는 마크업이 약간 번거로울 수 있다. HOC에서는 한개의 최상위 children 루트는 React/JSX 제약 조건에 의해 보장된다.
  • 부모 컴포넌트는 엘리먼트 트리에서 자유롭게 사용 할 수 있으며, HOC와 마찬가지로 컴포넌트 클래스별로 한 번만 사용해야 한다는 제약이 없다.

일반적으로 부모 컴포넌트를 사용하여 수행 할 수 있는 경우 HOC보다 해킹이 적지만 나열된 내용은 HOC보다 유연성이 떨어진다.

마무리

이 글을 읽은 후 리액트 HOC에 대해 조금 더 알게 됐을 것이다. 이것은 매우 표현력이 뛰어나고 다른 라이브러리에서 꽤 유용 한것으로 입증되었다.

리액트는 많은 혁신을 가져 왔고 Radium, React-Redux, React-Router와 같은 프로젝트를 운영하는 사람들도 그 중 꽤 좋은 증거이다.

나와 연락하고 싶다면 twitter @franleplant에서 나를 팔로우 하자.

이 글에서 설명하는 패턴 중 일부를 확인하고 싶다면 내가 사용한 이 repo로 이동하자.

Credits

크레딧은 주로 React-Redux, Radium, Sebastian Markbåge의 이 gist와 나의 소스를 이용했다.


이 글은 franleplant의 React Higher Order Components in depth을 번역한 글입니다. 전문 번여가가 아니라 오역이 있을 수 있습니다. 알려주시면 수정하도록 하겠습니다. 원본은 아래에서 확인 할 수 있습니다.


리뷰