Nakta's Blog

Frontend Dev Story

React의 HOC, Render Prop

11 November 2017

같은 코드가 여러 곳에서 나타난다? 이 때부터 코드에서 냄새가 나기 시작한다고 합니다. 만약 중복 코드가 밟생한 부분의 로직이 수정돼야 한다면 어떻게 해야 할까요? 당연히 해당 로직이 사용된 모든곳을 찾아서 수정해줘야 합니다. 이런 문제를 해결하려면 중복코드를 제거해야되겠죠. React로 개발하면서 중복 코드가 발생했을 때 컴포넌트를 활용해서 중복 로직을 제거하는 방법에 대해서 알아보도록 하겠습니다.

중복 코드를 가진 컴포넌트

React 컴포넌트를 개발하다보면 특정 아이디를 가지고 데이터를 가져와서 사용해야 하는 경우가 있습니다. 여기서는 멤버의 아이디를 이용해서 멤버 정보를 가져온 후, 필요한 데이터를 화면에 렌더링 하는 컴포넌트를 만들어보도록 하겠습니다. 이때 나타날 수 있는 중복코드에 대해서 알아보고 이 중복코드를 제거할 수 있는 방법에 대해서도 살펴보겠습니다.

import React, { PureComponent } from 'react';
class MemberName extends PureComponent {
}
export default MemberName;
import React, { PureComponent } from 'react';
return membersStore.getById(this.props.memberId);
return membersStore.getById(this.props.memberId);
return membersStore.getById(this.props.memberId);

MemberName 컴포넌트

특정 멤버의 이름을 보여주기 위한 컴포넌트를 만들어보겠습니다. memberId라는 값을 받아서 해당 아이디를 갖는 멤버의 이름을 보여주는 컴포넌트 입니다.

memberId를 prop으로 받아야 하기 때문에 propTyeps에 memberId 타입을 지정해줍니다. 여기서는 memberId를 string으로 받습니다. memberId가 꼭 필요하기 때문에 isRequired를 붙였습니다.

props으로 받은 memberId를 가지고 멤버정보를 받아와야합니다. membersStore를 import 해서 getById 메소드를 이용해 멤버 정보를 가져오도록 합니다.

이제 getMember 메소드로 멤버 정보를 가져와 name을 렌더링 하도록 합니다.

import React, { PureComponent } from 'react';
class MemberNickname extends PureComponent {
}
export default MemberNickname;
import React, { PureComponent } from 'react';
return membersStore.getById(this.props.memberId);
return membersStore.getById(this.props.memberId);
return membersStore.getById(this.props.memberId);

MemberNickname 컴포넌트

이번에는 멤버의 별명을 보여주는 MemberNickname 컴포넌트를 만들어 보겠습니다. MemberName 컴포넌트와 마찬가지로 memberId라는 값을 받아서 해당 아이디를 갖는 멤버의 별명을 보여주는 컴포넌트 입니다.

MemberName 컴포넌트와 마찬가지로 memberId를 꼭 필요로 하기 때문에 PropTypes.string.isRequired로 정의합니다.

그리고 memberId 값으로 멤버 정보를 가져옵니다.

이제 getMember 메소드로 멤버 정보를 가져와 nickname을 렌더링 하도록 합니다.

class MemberName extends PureComponent {
static propTypes = {
memberId: PropTypes.string.isRequired,
};
getMember = () => {
return membersStore.getById(this.props.memberId);
};
render() {
return <div>{this.getMember().name}</div>;
}
}
class MemberNickname extends PureComponent {
static propTypes = {
memberId: PropTypes.string.isRequired,
};
getMember = () => {
return membersStore.getById(this.props.memberId);
};
render() {
return <div>{this.getMember().nickname}</div>;
}
}
return membersStore.getById(this.props.memberId);
return membersStore.getById(this.props.memberId);

냄새나는 중복 코드

방금 살펴본 두 컴포넌트에는 중복 코드가 존재합니다. 어느 부분일까요? prop으로 받은 memberId를 가지고 membersStore에서 멤버 정보를 가져오는 부분입니다.

MemberName 컴포넌트의 getMember 메소드와 render 메소드에서 사용하는 getMember() 부분과 같은 로직이 MemberNickname 컴포넌트에도 존재합니다.

MemberNickname 컴포넌트에서도 MemberName과 같은 형태로 렌더링 부분만 다를 뿐 로직이 같습니다.

중복 제거하기

이제 이 냄새나는 중복코드를 제거하는 방법에 대해서 알아보도록 하겠습니다. 컴포넌트의 로직 중복을 제거할 수 있는 방법은 크게 두 가지가 있습니다. HOCrender prop인데요. 하나씩 살펴보도록 하겠습니다.

HOC(Higher Order Component)

먼저 HOC를 이용해서 중복 로직을 제거하는 방법에 대해서 살펴보도록 하겠습니다.

HOC(Higher Order Component)에 대한 자세한 정보는 리액트 공식 홈페이지를 통해서 확인하시면 될것 같습니다.

간단하게 HOC에 대해서 설명 하자면 컴포넌트를 감싸서 추가적인 작업을 해준 후 props로 작업 결과를 넘겨주는 컴포넌트라고 생각하면 됩니다. 이제 이 HOC를 이용해서 어떻게 중복 제거를 할 수 있는지 보겠습니다.

import PropTypes from 'prop-types';
import React, { PureComponent } from 'react';
import membersStore from './membersStore';
export default function withMemberLoader(WrappedCoponent) {
class MemberNickname extends PureComponent {
static propTypes = {
memberId: PropTypes.string.isRequired,
};
getMember = () => {
return membersStore.getById(this.props.memberId);
};
render() {
return <span>{this.getMember().nickname}</span>;
}
}
export default MemberNickname;
export default function withMemberLoader(WrappedCoponent) {
export default function withMemberLoader(WrappedCoponent) {
export default function withMemberLoader(WrappedCoponent) {
export default function withMemberLoader(WrappedCoponent) {

먼저 MemberName 컴포넌트와 MemberNickname 컴포넌트의 중복 코드를 뽑아서 HOC로 만들어줘야 합니다.

withMemberLoader라는 이름으로 컴포넌트를 파라미터로 받는 함수를 만들어 줍니다. 이 함수는 새로운 MemberLoader 라는 컴포넌트를 반환하게 되는데요. 이 안에서 멤버의 아이디를 가지고 멤버 정보를 가져오는 로직을 구현하도록 하겠습니다.

새로 반환하는 MemberLoader 컴포넌트에서 파라미터로 받았던 컴포넌트를 그대로 렌더링하도록 해줍니다. 그래야 원래 렌더링 하고 싶은 컴포넌트의 내용을 보여줄 수 있게 되겠죠? 그리고 MemberLoader 컴포넌트가 대신 받게 된 props를 모두 WrappedComponent로 그대로 넘겨줍니다.

이제 MemberName이나 MemberNickname 컴포넌트 대신 MemberLoader가 받게된 memberId를 가지고 멤버 정보를 가져오도록 메소드를 만들어줍니다.

getMember 메소드로 멤버 정보를 가져올 수 있게 됐습니다. getMember 메소드를 통해서 가져온 멤버 정보를 파라미터로 받은 컴포넌트의 props로 넘겨주면 멤버 정보를 받을 수 있게 됩니다.

withMemberLoader를 이용한 MemberName와 MemberNickname 컴포넌트

자 이제 withMemberLoader HOC가 만들어졌습니다. 이 HOC를 이용해서 중복 로직이 제거된 MemberNameMemberNickname 컴포넌트를 만들어보도록 하겠습니다.

import PropTypes from 'prop-types';
import React, {PureComponent} from 'react';
import membersStore from './membersStore';
class MemberName extends PureComponent {
static propTypes = {
memberId: PropTypes.string.isRequired,
};
getMember = () => {
return membersStore.getById(this.props.memberId);
};
render() {
return <div>{this.getMember().name}</div>;
}
}
export defualt MemberName;
import withMemberLoader from './withMemberLoader';
return membersStore.getById(this.props.memberId);
import withMemberLoader from './withMemberLoader';
return membersStore.getById(this.props.memberId);
return membersStore.getById(this.props.memberId);
return membersStore.getById(this.props.memberId);
return <div>{this.props.member.nickname}</div>;
return <div>{this.props.member.nickname}</div>;

MemberName 컴포넌트

원래 구현한 MemberName 컴포넌트 입니다.

이제 MemberName 컴포넌트를 export 할 때 withMemberLoader로 한 번 감싸서 export 하도록 해주겠습니다. 이렇게 하면 props에 member라는 값이 추가 되고 여기 멤버의 정보가 같이 담겨서 넘어오게 됩니다.

멤버 정보를 withMemberLoader를 통해서 받아오기 때문에 더 이상 getMember 메소드는 의미가 없게 됩니다. getMember 메소드를 제거 하고, 렌더링 부분의 this.getMember()를 props에 있는 member를 참조하도록 수정해줍니다.

MemberNickname 컴포넌트

MemberNickname 컴포넌트도 마찬가지 입니다.

MemberNickname 컴포넌트를 withMemberLoader 로 한번 감싸서 export 해줍니다.

그리고 더이상 의미 없는 getMember 메소드와 이를 참조하는 코드를 props의 member를 참조하도록 수정 합니다.

자 이렇게 중복되는 로직을 HOC 컴포넌트로 추출해줍니다. 그리고 추출한 로직에서 필요한 데이터만 원래 컴포넌트의 props로 넘겨주게 되면 각 컴포넌트마다 중복된 로직을 갖지 않고 손 쉽게 원하는 데이터를 사용할 수 있게 됩니다.

Render Prop

두 번째로 Render Prop을 이용한 방법을 살펴보겠습니다. Render Prop에 대해서 간단하게 설명하자면 어떤 컴포넌트의 props으로 함수를 넘겨줍니다. 이때 넘겨준 함수의 반환값은 렌더링 하고 싶은 JSX입니다. 무슨소리인지 코드를 통해서 살펴보겠습니다.

import React, {PureComponent} from 'react';
class MemberName extends PureComponent {
static propTypes = {
memberId: PropTypes.string.isRequired,
};
getMember = () => {
return membersStore.getById(this.props.memberId);
};
render() {
return <div>{this.getMember().name}</div>;
}
}
import React, {PureComponent} from 'react';
import React, {PureComponent} from 'react';
membersStore.getById(this.props.memberId);
membersStore.getById(this.props.memberId);
return this.props.render(this.getMember());
return this.props.render(this.getMember());

MemberName 컴포넌트

자 다시 한번 중복 로직이 포함된 MemberName 컴포넌트 코드입니다. 여기에서 중복 로직을 MemberLoader라는 컴포넌트를 만들어서 뽑아내도록 하겠습니다.

MemberLoader라는 컴포넌트를 하나 만들어 줍니다. 이 컴포넌트는 props의 값으로 두 가지 값을 받아야 합니다.

  1. memberId
  2. render

memberId는 우리가 MemberNameMemberNickname에서 받았던 값입니다. 그리고 render는 멤버 정보 파라미터로 받아서 원하는 정보를 JSX 형태로 반환해주는 함수입니다.

자 이제 우리의 중복 코드인 getMember를 이 컴포넌트로 뽑아내도록 하겠습니다. 이렇게 정의해준 getMember 메소드와 props의 render 함수를 이용해서 원하는 값을 렌더링 할 수 있도록 해줍니다.

props의 render 함수에 this.getMember()를 이용해서 멤버 정보를 넘겨주도록 합니다. 그러면 넘겨준 render 함수에 따라 원하는 값을 화면에 보여줄 수 있게 됩니다.

RenderProps을 이용한 MemberName과 MemberNickname 컴포넌트

이제 새로 만든 MemberLoader 컴포넌트를 사용해서 중복코드가 제거된 MemberNameMemberNickname를 다시 만들어보겠습니다.

import PropTypes from 'prop-types';
import React, { PureComponent } from 'react';
import membersStore from './membersStore';
class MemberName extends PureComponent {
static propTypes = {
memberId: PropTypes.string.isRequired,
};
getMember = () => {
return membersStore.getById(this.props.memberId);
};
render() {
return <span>{this.getMember().name}</span>;
}
}
export default MemberName;
import React, { PureComponent } from 'react';
return membersStore.getById(this.props.memberId);
import React, { PureComponent } from 'react';
import React, { PureComponent } from 'react';
return membersStore.getById(this.props.memberId);
import React, { PureComponent } from 'react';
return membersStore.getById(this.props.memberId);
import React, { PureComponent } from 'react';
return this.props.children(this.getMember());
<MemberLoader memberId={this.props.memberId}>
return this.props.children(this.getMember());
<MemberLoader memberId={this.props.memberId}>
<MemberLoader memberId={this.props.memberId}>

MemberName 컴포넌트

중복코드가 존재하는 MemberName 컴포넌트 입니다. 여기서 우리가 만든 MemberLoader 컴포넌트를 이용해서 멤버정보를 가져와야 합니다.

render 메소드에서 MemberLoader 컴포넌트를 이용합니다.

MemberLoader 컴포넌트의 prop으로 memberId를 넘겨줘야 멤버정보를 가져올 수 있기 때문에 memberId를 prop으로 넘겨줍니다. 그리고 getMember 메소드는 더이상 필요하지 않기 때문에 삭제해줍니다.

이제 마지막으로 render 함수를 넘겨줘야 합니다. MemberLoader 컴포넌트에서는 이 render 함수에 멤버 정보를 파라미터로 넘겨주게 됩니다. 이 멤버정보를 이용해서 멤버의 이름을 렌더링 하도록 함수를 만들어 넘겨주면 됩니다.

MemberNickname 컴포넌트

MemberNickname 컴포넌트도 크게 다르지 않습니다.

MemberLoader 컴포넌트에 memberId와 render 함수를 넘겨줘서 별명을 렌더링해줍니다. 여기서 render 라는 prop으로 렌더 함수를 넘겨줬는데요. 다른 이름을 쓰면 안될까 하는 의문이 드실 수 있습니다. 물론 다른 이름을 사용하셔도 상관 없습니다. 심지어 children을 함수로 사용하셔도 됩니다.

render 대신 children 사용하기

MemberLoader 컴포넌트에서 render 부분을 children으로 바꿨습니다.

그러면 사용하는 컴포넌트에서는 render를 넘겨주는 대신 children에 함수형태를 넣으면 됩니다.

함수를 직접 넣는 대신 함수 명을 넘겨서 할 수 도 있습니다.

RenderProp이라는 방법으로 중복코드를 제거하는 방법에 대해서 살펴봤습니다.

HOC와 render prop으로 중복코드를 제거했는데요. 두가지 방법에 어떤 차이점이 있고, 또 둘 중 어떤 방법을 쓰면 좋을지에 대새서 알아 보겠습니다.

HOC와 Render Prop의 차이

두 가지 다 중복 코드를 제거한다는 공통점이 있습니다. 다만 약간의 차이점이 있는데요.

함수 vs 컴포넌트

HOC는 컴포넌트를 꾸며주는 함수를 이용하는 반면, Render Prop은 컴포넌트를 이용합니다.

export default function withMemberLoader(WrappedCoponent) {
class MemberLoader extends PureComponent {
static propTypes = {
memberId: PropTypes.string.isRequired,
};
getMember = () => {
return membersStore.getById(this.props.memberId);
};
render() {
return (
<WrappedComponent
{...this.props}
member={this.getMember()}
/>
);
}
}
return MemberLoader;
}
export default function withMemberLoader(WrappedCoponent) {
return this.props.render(this.getMember());
return this.props.render(this.getMember());

HOC

HOC는 파라미터로 컴포넌트를 받는 함수입니다. 이 함수에 파라미터를 넘겨주면 함수 내부에 새롭게 정의한 컴포넌트의 render 메소드에서 넘겨주었던 컴포넌트를 렌더링 해줍니다.

이 때, 넘겨준 파라미터를 렌더링하면서 내부 컴포넌트에서 만들어낸 데이터를 추가적인 prop으로 넘겨줄 수 있게 되는 것이죠. 즉, 함수에 넘겨준 컴포넌트를 꾸며준다고 생각하면 편할것 같습니다.

Render Prop

render prop은 함수가 아닌 또다른 컴포넌트 입니다. 이 컴포넌트는 렌더링 하는 부분이 정해져 있지 않고 prop으로 주입받게 돼있습니다. 그렇다 보니 MemberLoader의 경우 멤버 정보를 가지고 렌더링 해야하는 경우에는 render 함수만 바꿔서 넘겨주면 원하는 데이터를 쉽게 렌더링 할 수 있습니다.

누가 데이터를 받는가?

HOC는 직접적으로 렌더링할 컴포넌트가 필요한 데이터를 받는 반면, Render Prop은 데이터를 제공해주는 컴포넌트에 렌더링 할 함수를 넘겨줍니다.

class MemberName extends PureComponent {
static propTypes = {
memberId: PropTypes.string.isRequired,
member: PropTypes.object.isRequired,
};
render() {
return <div>{this.props.member.name}</div>;
}
}
export defualt withMemberLoader(MemberName);
memberId: PropTypes.string.isRequired,

HOC

HOC는 파라미터로 넘겨준 MemberName 컴포넌트가 member라는 prop을 받게 됩니다.

Render Prop

render prop의 경우 MemberLoader라는 컴포넌트로 실제 렌더링할 JSX를 반환해줄 함수를 넘겨줍니다. 넘겨준 함수의 파라미터로 데이터를 넘겨줄수도 있습니다.

두 가지 방법 중 어떤게 더 좋을까?

HOC와 Render Prop의 공통적인 중복 로직 제거와 차이점에 대해서 살펴봤습니다. 그렇다면 어떤 방법이 더 좋은 방법일까요?

제 생각에는 개인 선호에 따라 더 좋은 방법을 골라 사용하면 될 것 같습니다. 결국은 중복제거에 큰 목적이 있기 때문에 이 목적을 두 가지 방법 모두 충족하기 때문에 무엇을 골라도 무방하다고 생각합니다.

그래도 굳이 하나만 선택해야 한다면… 저는 Render Prop에 손을 들어주고 싶습니다. 이유는 HOC보다 조금 더 가독성이 좋다는 느낌이 들어서 입니다.

return <div>{this.props.member.name}</div>;
<MemberName memberId={memberId}/>
return <div>{this.props.member.name}</div>;
return <div>{this.props.member.name}</div>;
<MemberNickName memberId={memberId}/>
return <div>{this.props.member.name}</div>;
memberId: PropTypes.string.isRequired,
<MemberNickName memberId={memberId}/>
memberId: PropTypes.string.isRequired,

가독성의 차이가 있다?

HOC 방법부터 살펴 볼까요? HOC를 이용한 방법은 컴포넌트를 꾸며주는 방법입니다. 지금까지 살펴본 예제로 보면 MemberName 컴포넌트에 memberId를 넘겨줍니다.

그러면 마법같이 member라는 값을 prop을 통해서 사용할 수 있게 됩니다. 저는 바로 이 부분이 가독성의 문제라고 생각합니다.

member라는 prop이 갑자기 튀어나왔습니다. 어디서 온걸까? MemberName 컴포넌트를 사용하려면 member를 컴포넌트에 넘겨줘야 하는걸까? 라는 생각이 들 수 있습니다.

컴포넌트 마지막 부분에 withMemberLoader로 꾸며준부분을 보기 전까지는 HOC를 이용했는지 알아채지 못할 수 있습니다.

HOC에 비하면 Render Prop는 조금 더 가독성이 좋습니다.

MemberName 컴포넌트 렌더링 부분을 보면 MemberLoaer라는 컴포넌트를 사용했다는걸 바로 알 수 있습니다. MemberLoader라는 이름을 통해서 멤버 정보를 가져온다는 정도는 쉽게 예측할 수 있기 때문에 HOC 방법에 비하면 코드 읽기가 매우 쉽다는걸 느낄 수 있습니다.

결론

컴포넌트에서 발생하는 중복 코드를 제거할 수 있는 방법 두 가지를 살펴봤습니다. HOC, Render Prop 모두 훌륭하게 중복 제거를 해주는 방법입니다. 약간의 차이가 있다면 가독성의 차이라고 생각합니다. HOC과 Rneder Prop 중에 무엇을 써야하는지는 개인 취향에 따라 골라서 사용하면 될것 같습니다.

Git Repository 이동
다른 글 읽기 https://nakta.dev