컴포넌트 작성에 대한 고민들 (합성 컴포넌트)

합성 컴포넌트로 모달을 만들아보자!

2024-04-24

들어가면서

리액트로 개발을 하면 한번쯤은 , 공통 컴포넌트에 대한 고민이 찾아온다. 당장 데브코스에서의 마지막 프로젝트에서도 겪었던 문제이고, 팀원 모두가 납득할만한 공통 컴포넌트를 만드는 것은 어렵고 힘들다.

하나의 컴포넌트가 특정 도메인에 엮어서 복잡하고 읽기 힘든 코드가 나올 때가 많다.

예를 들면 이런 것이다.

이런 상황에서 조건식으로만 분기를 하다보면 끔찍한 코드가 나온다. 가령 아래와 같은 부분이다. 아래와 같은 컴포넌트는 개발을 하면서 자주 보던 패턴이다. 그리고 이런 패턴은 개발을 하면서 정말 자주 사용했던 것 같다.!

어떤 단위로 컴포넌트를 만들까? 재사용하기 편한 컴포넌트는 무엇일까?에 대한 고민은 개발을 하면서 끊임없이 생기는 것 같다.

추상적으로 공통 컴포넌트가 무엇이냐 하면 아마 다음의 답변이 나올 것이다.

명령형 프로그래밍, 선언형 프로그래밍

명령형 프로그래밍은 무엇을 , 어떻게 해줘를 구체적으로 작성한 프로그래밍 기법이다. 컴포넌트에서의 명령형 프로그래밍은 어떨까? 예를 들어, 하나의 컴포넌트에서 아래의 조건을 만족해야 한다고 생각해보자.

이런 요구사항을 모두 만족하려면, 다음의 컴포넌트가 나올 것이다.

4개의 요구사항을 만족하기 위해 조건식과 분기처리를 쭉 하다보니, 컴포넌트가 읽기 힘들어지고 비대해진다. 이것이 좋은 컴포넌트라고 할 수 있을까?

위 컴포넌트의 문제점은 무엇일까? 여러 가지가 나오겠지만 대표적으로 크게 3개가 있을 것이다.

컴포넌트와 단일 책임의 원칙

단일책임의 원칙은 하나의 모듈은 하나의 액터만 책임진다.이다. 아마 다음의 설명들을 많이 들어봤을 것이다.

이런 원칙에 의해, 컴포넌트도 책임을 분리해줄 필요가 있다.

앞서 비대해진 컴포넌트를 역할과 책임에 따라 분리를 해보자. 가장 먼저 데이터 패칭과 패칭의 결과에 따른 조건부 렌더링을 부모에서 제어하는 것으로 수정해보자.

데이터 패칭에 대한 책임을 커스텀 훅으로 빼고, 패칭의 결과에 따른 조건부 렌더링의 책임은 부모로 위임한다.

이렇게 책임과 역할에 따라 커스텀 훅, 부모 컴포넌트를 적절히 이용하는 것은 중요한 것 같다. 적어도 코드가 비대해지고, 읽기 힘들어지는 문제를 위해서라도...!!

Modal을 만들어보자

이 블로그를 만들때, 글 검색을 위한 모달을 만들 필요가 있었다. 그리고 Modal을 만들때 컴포넌트 내부에서 여러 조건을 걸어준 적이 있다. 다음의 요구사항을 만족해야 했다.

이걸 모달 내에서 모두 처리한다고 생각해보자. 확장적이지 못하고, 재사용성이 떨어질 수 밖에 없다. 아래와 같은 코드가 모달 내에서 많은 조건이 있는 코드이다.

그래서,적절한 커스텀 훅의 활용과 더불어, 합성 컴포넌트로 모달을 사용하기 용이하게 만들어보자! 그전에, 먼저 합성 컴포넌트가 무엇인지부터 살펴보자.

합성 컴포넌트

앞서, 요구사항이 추가됨에 따라 컴포넌트가 비대해지고, 재사용성이 떨어지는 경험을 해결하고자 한다.

마크다운 이미지

먼저 합성이란 컴포넌트 안에 다른 컴포넌트를 담는 방법이다. 즉, 컴포넌트 자체를 props로 넘겨주는 방식을 의미한다.

그럼 합성 컴포넌트는 무엇일까? 합성 컴포넌트는 다른 컴포넌트의 자식으로 포함시켜, 복잡한 UI구조를 간결하게 만든다. 이 합성 컴포넌트를 사용하면, 어느정도 관심사를 컴포넌트별로 쪼갤 수 있고, 각 컴포넌트의 책임을 분산시켜 관리할 수 있다.

모달을 예시로 들어보자.

마크다운 이미지

위 모달은 크게 4가지 영역으로 구분할 수 있다. 그리고 이 모달은 서로간의 상태를 서로 공유해야 한다. (서로 같은 모달이기 때문에!)

모달의 열고 닫는 상태와, 여러 모달의 기능적인 부분은 useModal이라는 훅으로 빼보자. 나는 리코일을 사용하고 있기 때문에, 다음의 useModal 훅을 만들었다. 그 후 모달 컴포넌트에서 이 훅을 호출할 것이다.

앞서 우리의 요구사항 중 🚀 모달의 열고, 닫음의 상태가 필요하고 이를 제어해야 한다.를 커스텀 훅으로 분리를 해주었다.

먼저 모달을 닫는 버튼부터 만들어보자. 버튼의 경우 닫는 것뿐만 아니라, 닫았을 때의 액션에 대해 열어두기 위해 onClick을 props로 받는다. 그리고 모달 닫기 버튼에 기대하는 모달 닫기 버튼을 눌렀을 때는, 모달을 닫아라의 역할은 Modal컴포넌트에서 props로 넘겨준다. 즉, 모달을 닫는 책임을 아예 모달 닫기 버튼 컴포넌트에 준 것이다.

다음으로 모달의 배경을 만들어보자. 모달이 열렸을 떄의 전체적인 배경을 컴포넌트로 분리해보자! 이 ModalOverlay는 모달의 배경 + 모달을 닫는 책임을 갖는다.

그럼, 외부 배경에서 Escape 버튼을 누르면 모달이 닫힐 것이다. 이제 🚀 모달 외부를 눌렀을 때, 모달이 닫혀야 한다.의 요구사항을 해결해보자.

이 훅은 모달 뿐만 아니라, 여러 컴포넌트에서 쓰일 수 있다. 드롭다운 외부를 눌렀을 때, 드롭다운이 닫힌다거나 등등 여러 컴포넌트에서 쓰일 수 있는 기능이므로 커스텀 훅으로 빼주었다.

이제 모달에서는 단순히, 이 훅을 사용하면 된다. 그럼 훅으로 🚀 모달 외부를 눌렀을 때, 모달이 닫혀야 한다.의 책임을 위임해줬다. 모달 내에서 전부 제목,내용,푸터를 선언하기 보단 모달의 제목, 모달의 푸터, 모달의 헤더를 각각 만들어보자. 그리고 모달의 최상단에서 이를 적절히 갖고 와 사용하면 된다.

레고조각을 조립해 멋진 레고를 만드는 과정과 비슷하다!

이제 필요에 따라 여러 모달 레고 조각들을 useModal훅과 같이 사용하면 된다. 이렇게 되면 도메인에 얽히지 않고 새로운 모달을 여러 개 만들 수 있다.

예를들어, 블로그 내의 여러 글 목록을 추천해주는 모달을 만든다고 생각해보자. 적지적소에 Modal 관련 컴포넌트를 가져와 쓰고, 일일이 모달의 state를 관리안해도 된다. (useModal훅이 해줄 것이기 떄문에)

마무리

마크다운 이미지

합성 컴포넌트를 + 커스텀 훅을 적절히 사용해 컴포넌트의 책임과 역할을 나누는 것은 개발하면서 꼭 필요한 것 같다.

다른 컴포넌트들도 합성 컴포넌트를 통해 역할과 책임을 분리할 수 있을 것 같다(드롭다운이나, 탭 컴포넌트 등등..!)

합성 컴포넌트를 사용하면, 확실히 레고 조각들처럼 여러 컴포넌트를 조립해 활용할 수 있지만, 도메인과 컴포넌트가 엮이는 부분은 필연적인 것 같다는 생각이 든다.(어쩔 수 없는 것 같다..!)

그럼 이 도메인과 컴포넌트가 엮이는 것은 어떻게 해결할 수 있을까..?! 당장 떠오르는 건 도메인별로 가장 가까운 곳에 컴포넌트를 위치시키는 것이지만 더 좋은 방법이 있는지 모르겠다.!

그럼에도 불구하고 합성 컴포넌트 패턴은 되게 좋은 것 같다! 이렇게 여러 컴포넌트를 합성 컴포넌트로 만들면 쌩으로 컴포넌트 안에 떄려넣는 것보단 재사용성이 확실히 높아지는 것 같다.!