안녕하세요, 하이스트레인저 프론트 엔지니어 김유라입니다.
프론트엔드 개발을 하다보면 프로젝트 서비스의 기능이 확장되어 동작하길 바랄 때가 종종있습니다. 이럴 때 우리는 loC(Inversion of Control) 즉, 제어 역전 패턴을 통해 컴포넌트를 사용하는 개발자에게 컴포넌트의 제어권을 넘겨줌으로써, 개발자가 원하는 대로 컴포넌트를 컨트롤 할 수 있습니다. 이번 포스팅에서는 리액트의 다양한 컴포넌트 loC 패턴을 알아보고 재사용성을 극대화 하는 방법에 대해 알아보도록 하겠습니다.
1. 렌더링 loC
컴포넌트는 기본적으로 프로퍼티(props)와 상태(state)를 ‘내부의 로직들로 가공’하여 최종적으로 화면에 렌더링 하는 결과를 만들어냅니다. 즉 컴포넌트 사용자는 단지 렌더링 요소에 포함될 프로퍼티만을 전달할 뿐, 렌더링 결과물은 내부에 의해서만 결정된다고 볼 수 있습니다.
그런데 만약 사용중이던 컴포넌트가 특정 조건에서는 조금 다른 렌더링 결과물을 만들어내야 하는 상황이 발생한다면 어떻게 해야 할까요 ? 간단하게 컴포넌트를 수정하여 새로운 프로퍼티를 받아 해당 조건으로 렌더링을 처리하도록 만들 수 있을 것입니다. 하지만 이런 상황이 계속해서 발생된다면 프로퍼티의 종류가 많아지고 컴포넌트 또한 복잡해질 것입니다. 또한 이미 사용중이던 컴포넌트를 수정함으로써 예상치 못한 사이드 이펙트 문제도 발생하게 될것입니다.
그렇다면 이렇게 변화가 많은 부분에 대해서 컴포넌트 내부에 의해서만 렌더링 결과물을 만들어내지 않고, 컴포넌트를 사용하는 입장에서 렌더링 방식을 컨트롤 할 수 있으면 좀 더 컴포넌트를 유연하게 사용할 수 있을 것입니다.
그래서 지금부터 컴포넌트 사용자가 렌더링을 컨트롤 할 수 있게 하는 loC패턴들에 대해 한번 알아보겠습니다.
1-1. Render Props 패턴
Render Props 패턴은 간단하게 컴포넌트가 렌더링 함수를 프로퍼티로 전달받아 사용하는 방법입니다.
1 | /** StudentList.tsx */ |
1 | /** App.tsx */ |
위 예제처럼 컴포넌트 사용자는 render 프로퍼티만 전달함으로써 컴포넌트의 렌더링 방식을 컨트롤 할 수있고, 좀 더 유연하고 다양한 방법으로 컴포넌트를 활용할 수 있습니다.
이러한 Render Props 패턴은 구현도 간단하고, 동일한 렌더링 방식이 연속적으로 사용되는 컴포넌트 리스트에서는 유용합니다. 하지만 복잡한 조건의 렌더링 방식을 표현하기에는 한계가 있으며, 잘못하면 render함수가 너무 복잡해지거나, render 프로퍼티가 너무 많아지는 문제가 발생할 수 있습니다.
또한 컴포넌트 형태의 호출이 아닌 함수형태의 호출로 렌더링을 하면 리액트에서는 컴포넌트로 인식하지 않기 때문에, 주의가 필요합니다.
그렇다면, 복잡한 조건을 처리할 수 있고, 좀더 컴포넌트스럽게 렌더링을 컨트롤 할 수 있는 방법을 알아보기 위해 이번에는 합성 컴포넌트에 대해 알아보겠습니다.
1-2. 합성 컴포넌트 패턴(Compound Component Pattern)
합성 컴포넌트 패턴은 하나의 컴포넌트를 여러가지 집합체로 만든뒤, 집합체 안에 있는 각각의 컴포넌트를 분리하고 사용하는 쪽에서 조합하여 재사용하는 컴포넌트 패턴을 의미합니다.
예를들어, 기본 카드 컴포넌트를 만들었다고 가정해봅시다. 하지만 개발이 계속됨에 따라 점점 더 다양한 형태의 카드가 필요해지기 시작했습니다. 카드 컴포넌트의 해시태그가 추가되기도 하고, 가로형 컴포넌트이면서 구분선이 추가되는 등 부가적인 요소가 많이 들어오기 시작했습니다.
사실 이 요구사항들은 앞서 살펴본 Render Props를 이용한다면 그렇게 어려운 작업은 아닙니다. 하지만 앞으로 어떻게, 얼마나 많은 요구사항이 추가될지 알 수없기 때문에 이 컴포넌트의 props는 끝을 모르고 늘어날 것입니다.
2. 합성 컴포넌트 패턴 도입
합성컴포넌트의 간단한 예시로 html의 select를 볼 수 있는데, select는 <select>와 <option> 태그의 조합으로 이루어집니다. <select>와 <option>은 각각 독립적으로는 큰 의미가 없지만 사용하는 곳에서 이를 조합해 사용함으로써 화면에 의미 있는 요소가 됩니다.
1 | <select> |
이처럼 사용하는 곳에서 컴포넌트의 조합을 활용할 수 있다면 높은 재사용성을 만족하면서 다양한 상황에 사용할 수 있습니다. 이제 합성 컴포넌트 패턴을 어떤 순서로 작업하는지 간략하게 설명하겠습니다.
2-1. 서브 컴포넌트 구현
html의 <option>에 해당하는 서브컴포넌트를 구현합니다. 먼저, 카드를 구성하는데 필요한 각각의 요소를 나누어서 구현하도록 합니다.
<CardTitle>, <CardDescription>, <CardSubtitle>, <CardUserInfo>, <CardHashTag> 등 카드 컴포넌트의 구성요소가 될 수 있는 것들을 모두 서브컴포넌트로 만들었습니다.
1 | // Card 서브 컴포넌트 |
2-2. 메인 컴포넌트 구현
html의 select에 해당하는 메인 컴포넌트를 구현합니다. 서브컴포넌트를 조합해서 화면에 보이도록 하는 Wrapper 성격의 컴포넌트이면서 Card 컴포넌트를 최종적으로 DOM에 렌더링하는 역할을 합니다.
1 | // Card 메인 컴포넌트 |
2-3. 메인과 서브 컴포넌트를 묶어서 export
이렇게 구현한 컴포넌트들을 묶어서 export해줍니다.
이렇게 해주면 사용하는 곳에서 각각의 서브컴포넌트가 어떤 메인컴포넌트에서 사용되는지 확실하게 알 수 있어 가독성에 도움을 줍니다.
1 | // export |
2-4. 완성된 합성컴포넌트를 사용해 화면에 구현
이제 완성된 합성 컴포넌트를 사용해 어디서든 요구사항을 만족할 수 있게 되었습니다. 아래 사진처럼 조금 더 복잡한 Card를 구현할 일이 생기더라도, 얼마든지 서브컴포넌트의 조합으로 쉽게 만들 수 있습니다.
3. Render Props 패턴, 합성컴포넌트 언제 써야할까?
Render Props 패턴은 render 프로퍼티만을 전달함으로써 컴포넌트의 렌더링 방식을 컨트롤 할 수 있는 특징을 갖고 있습니다. 특히 Render Props 패턴은 동일한 렌더링 방식이 연속적으로 사용되는 컴포넌트(요구사항이 제한적이고 단순한 경우)에 사용하는 것이 적합 할 수 있습니다.
반면, 합성컴포넌트패턴(Compoumd Coposition Pattern)은 부모 컴포넌트가 컨텍스트를 제공하고, 자식 서브 컴포넌트들이 이를 “조합”하여 컴포넌트를 완성할 수 있는 특징을 갖고 있습니다. 이 특징으로 보아, 요구사항이 자주 변하거나 상태/역할별로 UI 구성이 달라지는 경우에 특히 적합할 수 있습니다. 또한 관심사를 서브 컴포넌트 단위로 분리해 가독성과 재사용성을 높이기 때문에 유지보수가 용이합니다.
4. 결론
복잡한 렌더링 컨트롤을 위해서 마냥 합성 컴포넌트만을 사용하는 것이 좋다고만 볼 수도 없습니다. 단순히 프로퍼티를 사용하는 것에 비해서, 추가적으로 Context/Provider 패턴을 구성해야 하며, 컴포넌트들은 해당 Context를 사용하는 로직이 추기돼야 하기 때문에 오히려 컴포넌트의 복잡도가 증가할 수 있습니다. 또한 프로퍼티의 사용을 줄인 대신에 오히려 사용해야하는 컴포넌트의 개수가 더 많아져서 사용성이 나빠질 수 있습니다. 그렇기에 구현하고자 하는 컴포넌트에 어떤 패턴을 적용하는 게 좋을지 충분히 고민하고, 선택적으로 적용하는 것이 중요합니다.
Reference
- https://ui.shadcn.com/
- https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/select
- https://tech.kakaoent.com/front-end/2022/220731-composition-component/