컴포넌트에 의존성 주입 활용하기
들어가며
현재 블로그는 Grid의 형태로 포스트 UI를 렌더링하고 있습니다. 저는 서버 컴포넌트를 이용하고 있기 때문에 선언적으로 작성하기 위해 Suspense를 이용했고 초기 구현체는 다음과 같습니다.
MainPosts
초기에는 큰 문제 없이 사용했습니다. 선언적으로 로딩을 관리하고 있었고, 많은 부분에서 사용하는 컴포넌트가 아니었기 때문입니다.
이후에 다른 블로그들과 같이 Post를 그룹별로 나누고 싶은 생각이 들었습니다. 블로그에 대한 포스트라면 블로그 태그를, Next에 대한 내용이라면 Next를 태그로 가진 그룹을 말이죠.
그래서 아래와 같은 새로운 컴포넌트를 만들었습니다.
TagPosts
어떤가요? 새로운 컴포넌트인가요? 저는 두 컴포넌트가 사실상 동일한 컴포넌트라고 생각했습니다. 현재 설계한 두 컴포넌트에서 공통점과 차이점을 나누어 보겠습니다.
공통점
- notion api (이하 fetcher) 를 통해서 Post[] 데이터를 받아온다.
- Grid의 형태로 Post[]를 렌더링 한다.
차이점
- Post를 fetch 하는 방법
어떻게 변경할까?
저는 공통점을 1개의 컴포넌트로 묶어 재사용성이 용이하게 만들고 차이점을 Props를 통해 전달하는 방식으로 설계 변경을 생각하게 되었습니다. 어떤 방식 좋을까요?
Post[]를 받는 순수한 View 컴포넌트를 만든다.
예를 들어 Post[]를 prop으로 받고 Grid로 렌더링 해주는 PostGrid 컴포넌트를 만드는 것이었습니다. 그렇다면 PostGrid를 사용하는 페이지나 컴포넌트에서 fetch를 한 뒤 prop으로 내려주면 될 테니까요. 하지만 이는 다음과 같은 문제가 있습니다.
블로그에서 서버 컴포넌트가 렌더링 되기 전에 사용자에게 로딩 화면을 보여주기 위해서 Suspense를 사용하고 있습니다. 따라서 page에서 직접 fetch를 진행 후 Post[]를 넘겨준다면, Suspense를 사용할 수 없었고, 그렇다면 이전과 같이 페이지 내부에 새로운 컴포넌트들이 필요했습니다. 혹은 Suspense를 사용하지 않는다면, 유저는 페이지 전체를 Post[]가 fetch 되기 전까지 기다릴 수밖에 없습니다. 이는 제가 원하는 방향이 아니었습니다.
PostGrid 컴포넌트에서 type이나 tag와 같은 prop을 받은 뒤 내부에서 처리
그렇다면 prop으로 type을 보내주는 방법이나 TagPosts 컴포넌트를 단순히 확장하는 방법을 생각해 볼 수 있습니다. 하지만 이런 식으로 단순하게 요구사항 처리를 하게 된다면, 추후에도 다른 요구사항에 따라 컴포넌트를 빈번하게 변경해야 할 수도 있습니다.
예를 들어 hidden과 같은 체크박스가 선택된 포스트는 불러오지 않는 새로운 PostGrid가 필요하다면 어떨까요? 아니면 정렬 기준이 다른 PostGrid는 어떻게 해야 할까요? n 개의 요구사항에 따라 생기는 n개의 컴포넌트를 만들거나 prop을 계속 내려 주는건 합리적이지 못하다는 생각이 듭니다.
정말 끔찍하네요….
그래서?
구조에 대해 유심히 고민하던 중 우연히 보게 된 영상이 있습니다.
# 토스ㅣSLASH 23 - Server-driven UI로 토스의 마지막 어드민 만들기
그렇습니다. “의존성 주입” 보통 객체 지향 프로그래밍을 해보신 분이라면, 익숙한 키워드일 것입니다. 그렇다면 제가 이걸 어떤 방식으로 사용해서 현재 코드를 개선할 수 있었을까요?
DIP?
먼저 들어가기 전에 가볍게 의존성 주입이 뭔지, DIP가 뭔지 알아보겠습니다.
DIP는 객체지향 프로그래밍의 5가지 설계 원칙 중 하나입니다.
의존성 역전 원칙(DIP)
상위 계층(정책 결정)이 하위 계층(세부 사항)에 의존하는 전통적인 의존관계를 반전(역전)시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다.
첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다. 둘째, 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.
DIP원칙을 검색하면 아래와 비슷한 이미지를 쉽게 찾을수 있습니다.
즉 사용자는 하위 모듈보다 쉽게 변경되지 않는 상위 모듈이나 추상화된 인터페이스에 의존해야 한다는 것입니다.
그렇다면 이 원칙을 PostGrid 컴포넌트에는 어떤 식으로 적용할 수 있을까요? 저는 다음과 같이 생각했습니다.
PostGrid(사용처)는 구체화된 getPosts나 getPostsByTag를 의존하지 않고 추상화된 PostFetcher에 의존합니다. 따라서 page에서는 요구사항에 맞게 PostGrid에 fetcher를 주입함으로써 새로운 컴포넌트를 만들 필요 없이 재사용할 수 있게 되었습니다. 추가적으로 page내에 Suspense 경계 내부에서 바로 사용할 수 있습니다.
구현
이러한 interface (현재는 call signatures)는 Typescript를 사용하여 구현할 수 있고, 잘못된 타입의 함수를 넘겨준다면, 개발자에게 경고하여, 트랜스 파일 전에 오류를 파악할 수 있습니다.
PostGrid는 더 이상 어떤 방식으로 Post[]를 fetch할지 알 필요가 없습니다. 단순히 외부에서 fetcher를 props로 주입받아 Post[] 데이터를 Grid로 렌더링 하기만 하면 됩니다.
이제 별도의 컴포넌트를 래핑 할 필요 없이 사용할 page에서 fetcher를 주입하는 방법을 이용할 수 있습니다.
어떤가요? 이제 더 이상 MainPosts나 TagPosts와 같이 별도로 컴포넌트를 만들어 줄 필요가 없습니다. 페이지의 요구사항에 알맞은 fetcher를 만들어 PostGrid에 주입 해주기만 하면 됩니다. 더 이상 새로운 컴포넌트를 만들어줄 필요가 없습니다.
만약 이번 주에 발행한 글들을 보여주는 그리드 뷰가 필요해진다면, 어떻게 만들어야 할까요?
이전 방식으로는 ThisWeekPostsGrid와 같은 컴포넌트가 필요했을 수도 있습니다.
어떤 요구사항이 생기더라도 이제는 Call signatures를 충족하는 함수만 만들어 준다면, 문제없습니다.
실제로 아래와 같은 복잡한 요구사항이 생기더라도 추가적인 코드 하나 없이 기존 코드로 해결할 수 있습니다.
최신순으로 정렬되고 Collection 태그를 가진 포스트를 제외하면서 hidden이 아닌 포스트를 Grid로 보여주는 방법. 현재 블로그 메인에 적용된 상태입니다.
이제 좀 더 선언적인 코드를 작성할 수 있게 되었습니다. Suspense를 위한 1 depth의 컴포넌트를 만들어줄 필요도 없습니다. 추후에 노션을 사용하지 않게 되더라도 fetcher 내부를 수정하거나, 새로운 fetcher를 만들어 대체해 준다면, page 컴포넌트는 단 한 줄도 수정할 필요가 없습니다.
마치며
구조를 바꾼 덕분에 다음과 같은 장점이 생겼습니다.
- PostGrid에 fetcher를 주입하기 때문에 더 이상 suspense를 위한 컴포넌트를 만들어주지 않아도 됩니다.
- PostGrid는 데이터를 받아오는 방법에 신경 쓰지 않아도 되며, 주입 받은 fetcher에서 알맞게 처리해서 받은 데이터를 Grid로 렌더링 하는데 집중합니다. 순수한 코드가 되었습니다.
사실 특별한 내용이 아닐 수 있습니다. 의식하지 않았지만, 자기도 모르는 와중에 이러한 코드를 짜고 있을 수도 있습니다.
하지만, 의식적으로 생각하고 설계한다면, 더 이상 우연이 아닌 요구사항에 적절한 코드를 작성할 수 있는 능력이 생기지 않을까 합니다.
참고자료
+
글을 작성하면서 이전에 비슷한 문제에 대해 고민했던 것이 떠올랐습니다. 리액트 과제를 수행하면서, 영화 포스터 웹사이트를 만들어야 했는데, 과정에서 영화를 인기순, 현재 상영작, 상영 예정작 등으로 나누어 그리드로 표시하는 컴포넌트를 만들어야 했습니다
MovieList는 useInfinityMovies에서 받아온 movies를 가지고 그리드 페이지를 만듭니다.
또한 Page에서는 Suspense를 위해 MovieList를 별도로 만들어주었습니다. 뭔가 앞서 본 내용과 비슷하지 않나요? ㅎㅎ
이런 식으로 만들어줄 수 있지 않을까요? 훅에서 사용하는 fetcher를 주입해주거나 훅 자체를 주입 해주는 방법을 사용할 수 있을 것 같습니다.