이 포스트는 '실전 리액트 프로그래밍' 을 참고하여 작성되었습니다.
나는 코드를 작성할 때, 중복인 코드가 너무 싫다. 개발 과정에서 별 생각 없이 코드를 치다보면 어느새 중복된 코드밭이 되어었다.
그래서 항상 코드를 짜고 다시 그 중복을 제거하고자 코드를 리팩토링하는 어떻게보면 바보같은 짓을 반복하며 코딩했다.
사실 코드를 작성하는 개발자의 기본은 중복을 피하는 것이라고 생각한다.
리액트에서는 render 내에서 코드가 많이 작성된다.
중복되는(공통으로 사용되는) 여러 요소들은 컴포넌트화해서 사용하고, 중복되는 로직들은 함수로 빼서 사용한다.
그렇다면 중복으로 사용되는 컴포넌트의 로직은 어떻게 관리할까?
고차 컴포넌트
난 웹 지식이 거의 전무한 상태로 이미 꽤 진행된 프로젝트에 참여하게 되어
그 때 리액트를 처음 접하게 되었다.
코드들을 훑어보는 중에 이런 모습의 코드를 봤다. 뭔가 싶었다.
connect(mapStateToProps, mapDispatchToProps)(withRouter(somePage));
뭐 우선은 usage가 이러한 형식이다라고만 생각하고 넘어가자.
고차 컴포넌트는 컴포넌트의 공통되는 로직을 관리하기 위해 사용한다.
기본적으로 고차 컴포넌트는 컴포넌트를 입력으로 받아, 컴포넌트를 출력해주는 함수이다.
function withComponentMount(InputComponent, componentName) {
return class OutputComponent extends Component {
componentDidMount() {
doSomeThing(componentName);
}
render() {
return <InputComponent {...this.props} />;
}
};
}
export default withComponentMount(JinComponent, 'JinComponent');
withComonentMount 함수가 고차 컴포넌트에 해당된다.
위에서 말했듯이 withComponentMount 함수는 JinComponent를 입력으로 받아 doSomeThing 이라는 공통 기능을 적용한 새로운 OutputComponent를 반환한다.
즉, withComponentMount 함수에 인자로 주는 컴포넌트는 렌더링이 완료되면 doSomeThing() 을 실행하게 되는 것이다.
아주 기본적이고 간단한 예시이다. 고차 컴포넌트 함수를 정의하고, 이를 적용해보았다.
좀 더 재밌는 예제를 살펴보자.
입력된 컴포넌트를 상속하는 고차 컴포넌트
function withInherit(Inputcomponent) {
return class OutputComponent extends InputComponent {
// Inherit .. and do SomeThing
}
}
위의 코드 예제와 무슨 차이인지 보면, 파라미터로 넘긴 InputComponent가 extends 뒤에 위치해있다.
이 고차컴포넌트는 InputComponent를 상속하여 새로운 컴포넌트를 생성해준다.
컴포넌트를 상속받는다면, InputComponent의 인스턴스에 접근할 수 있게 된다.
고차 컴포넌트를 활용한 디버그
function withInherit(InputComponent) {
return class OutputComponent extends InputComponent {
render() {
return (
<>
<p>props: {JSON.stringify(this.props)}</p>
<p>state: {JSON.stringify(this.state)}</p>
{super.render()}
</>
);
}
};
}
withInherit 고차컴포넌트는 InputComponent를 인자로 받아 InputComponent의 props 와 state를 뷰에 보여주고
InputComponent 의 render 함수를 실행 후 종료한다.
특정 이벤트 발생 시 어떤 컴포넌트가 unmount 후 다시 mount될 때 리액트 개발자 도구 툴로 디버깅하기 한계가 있다.
이러한 고차컴포넌트를 사용하면 위해 항상 InputComponent위에 그의 props 값과 state 값이 나타나 있을 것이다.
고차 컴포넌트를 여러개 사용하기
export default connect(... )();
위와 같은 형식으로 사용되는 connect 고차 컴포넌트가 있다.
connect 는 react-redux 에서 사용되는 고차 함수인데, 고차 컴포넌트를 반환한다.
다시 위에서 잠시 언급했던 고차 컴포넌트 사용 예시를 살펴보자.
connect(mapStateToProps, mapDispatchToProps)(withRouter(somePage));
이해를 돕기 위해 나눠서 살펴보겠다.
const enhance = connect(mapStateToProps, mapDispathToProps);
const router = enhance(withRouter(somePage));
export default router;
connect 고차함수가 고차 컴포넌트를 반환하기 때문에, 위와 같이 여러개를 연동하여 사용할 수 있다.
사실 너무 잘게 쪼개진 고차 컴포넌트는 렌더링 성능에 좋지 않다. 필요한만큼만 잘 분배하여 쓰도록 하자.
고차 컴포넌트의 단점
1. 사용자가 직접 넘긴 props 외에 암묵적으로 넘어오는 속성 값이 존재한다.
2. 고차 컴포넌트 간에 같은 이름의 속성 값을 사용하면 충돌이 발생한다.
3. 커스텀 HoC를 만들 때 여러 준비 절차가 필요하다.
1번의 경우에는 위에서 언급한 react-redux의 connect를 사용해본 경험이 있는 사람이라면 쉽게 이해할만한 부분이다.
this.props.dispatch
connect를 사용한 코드를 살펴보면, 이러한 코드를 자주 볼 수 있다.
그런데 분명 이 컴포넌트에게 dispatch를 props로 주는 모습은 찾을 수 없다.
이는 connect 고차함수에서 암묵적으로 dispatch 라는 props를 넘긴 것이다.
처음 이 코드를 봤을 때에는 도대체 이 dispatch는 어디서 나오는가 많은 고민을 했다.
2번의 경우에는 같은 이름의 속성 값을 사용하면 나중에 호출된 컴포넌트의 속성 값으로 overwrite 된다.
1번의 경우와 다르게 내가 자식 컴포넌트에 dispatch 를 명시적으로 전달했을 때, connect 를 사용하면 내가 전달한 dispatch는 사라지고 connect에서 암묵적으로 전달된 dispatch로 overwrite된다.
내가 정의한 컴포넌트의 이름에서 충돌이 나는 것이라면 그저 이름을 변경해주면 된다.
그런데 외부 패키지들간에 충돌이 난다면? 하나는 포기해야한다. ( 같이 사용하기에 힘들다 )
3번
3-1) 고차 컴포넌트를 생성할 때 함수로 감싸줘야한다.
3-2) 디버깅을 위해 컴포넌트의 이름을 설정해주어야한다. (displayName, recompose 패키지 참조)
3-3) 정적 타입 언어 사용 시 함수로 감싸진 부분에 대한 타입 정의가 까다롭다.
마치며
고차컴포넌트의 활용성은 사실 개발자가 어떻게 사용하느냐에 따라 무궁무진할 것 같다.
Lifecycle API를 활용해서 생명주기에 따라 특정 작업을 수행하거나, redux와 연동할 수 도 있고 서버에 요청을 보낼 수도 있고..
반복되는 컴포넌트의 로직을 고차컴포넌트를 사용함으로 해결할 수도 있다.
내가 평소에 별 생각없이 사용하던 패키지와 그 usage가 알고보니 고차 컴포넌트였다는 것을 이제야 알았다.
나도 직접 여러 예제 코드들을 작성하고 동작시켜보면서 감을 익혔다.
단점이 존재하지만, 충분히 매력있는 HoC 였다.
'유연해지기 > React.js' 카테고리의 다른 글
리액트 절대경로 설정 및 모듈/경로 별명 짓기 with CRA (absolute path, path alias) (0) | 2021.10.21 |
---|---|
[React.js] Error Boundary와 Fallback UI 에 대하여 (0) | 2021.10.08 |
[React.js] useEffect, cleanUp, deps, unMount에 대하여 (0) | 2021.09.29 |
[React.js] useRef와 useState의 용도와 차이 (0) | 2021.09.07 |
[React.js] crypto-js를 이용한 패스워드 암호화 (0) | 2021.04.22 |