최근 [우혁님](https://velog.io/@woogur29/posts)이 작성해주신 [Toss Frontend Fundamentals 모의고사 후기 글](https://velog.io/@woogur29/Toss-Frontend-Fundamentals-%EB%AA%A8%EC%9D%98%EA%B3%A0%EC%82%AC-%ED%9B%84%EA%B8%B0)을 봤다. 글에서 소개해주신 [컴포넌트의 "단일 책임" 경계, 어떻게 나누시나요?](https://github.com/toss-fe-interview/frontend-fundamentals-mock-exam-1/discussions/83) 라는 제목의 디스커션을 읽는데 아래와 같은 고민을 하게 되었다. 1. **Render Props** 패턴은 무엇을 해결하는 패턴일까? 2. **커스텀 훅** 기반의 추상화와 **컴포넌트** 기반의 추상화 언제 어떤 추상화를 사용하는게 효과적일까? 이를 알아보기 위해 다양한 리서치를 진행했고 그에 대한 내용을 공유해보려 한다. 아마 이 글을 읽고나면 언제 어떤 형태의 추상화 전략을 사용할지 판단할 수 있는 명확한 근거를 마련할 수 있게 될거라 생각한다. ## Render Props 패턴은 무엇을 해결하는 패턴일까? > https://patterns-dev-kr.github.io/design-patterns/render-props-pattern/ 위 글에서는 **Render Props** 패턴을 사용하면 컴포넌트의 재사용성이 올라간다고 이야기한다. 다만 개인적으로 설명이 조금 부족하다고 느꼈다. 왜 컴포넌트의 재사용성이 올라가는지 명확한 근거를 기반으로 이해하기 위해 이리저리 찾아보다 **정책과 메커니즘의 분리(Separation of mechanism and policy)**에 대해서 알게 되었다. ### 정책과 메커니즘의 분리 > [Wikipedia에 정의된 메커니즘과 정책의 분리](https://en.wikipedia.org/wiki/Separation_of_mechanism_and_policy) Wikipedia에서는 정책과 메커니즘의 분리에 대해서 아래와 같이 이야기한다. > **메커니즘과 정책의 분리(Separation of Mechanism and Policy)**는 컴퓨터 과학의 한 설계 원칙입니다. 이 원칙은 시스템 구현에서 작업의 권한 부여와 자원 할당을 제어하는 부분인 **메커니즘**이, 어떤 작업을 허가하고 어떤 자원을 할당할지 결정하는 기준인 **정책**을 독단적으로 결정하거나 과도하게 제한해서는 안 된다고 명시합니다. 이 원칙의 핵심은 **기능(Mechanism)**과 **정책(Policy)**을 별개로 분리하여 설계해야 한다는 것이다. 이렇게 분리하면 시스템의 하부 구조(메커니즘)를 수정하지 않고도 운영 규칙(정책)만 쉽게 변경할 수 있어 시스템의 유연성과 확장성이 높아진다. ![](https://velog.velcdn.com/images/koreanthuglife/post/6b935159-53d9-4a9a-adcb-3537b53c4e5a/image.png) 예를들어 운영체제에서 **CPU 스케줄링을 할 수 있는 기능**은 메커니즘이고, **어떤 프로세스에 우선순위를 줄 것인가**는 정책이다. CPU 스케줄링을 할 수 있는 기능은 불변적이다. 어떤 OS를 만들든 변하지 않는 기능이다. 어떤 프로세스에 우선순위를 줄 것인가에 대한 부분은 가변적이다. OS에 따라 달라질 수 있다. 이렇게 불변적인 부분(메커니즘)과 가변적인 부분(정책)을 분리해 설계하면 수정이 발생해도 정책(가변적인)에 관한 부분만 수정하면 되기에 시스템의 유연성과 확장성이 높아지게 되는 것이다. ### 리액트에서의 정책과 메커니즘 그렇다면 리액트 기반의 웹 어플리케이션에서 어디까지가 정책이고 어디까지가 메커니즘일까? 나는 보통 리액트 기반의 웹 어플리케이션을 만들때 크게 두 가지 카테고리로 로직을 분리해왔다. - **UI 로직** - **비즈니스 로직** 그리고 최근에는 UI 로직과 비즈니스 로직을 또 다시 두 카테고리인 **정책과 메커니즘**으로 분리하려 노력하고 있다. 간단한 예제로 한 번 알아보자. #### 예제1: List 컴포넌트 프로젝트 초기에 데이터를 받아와 반복문을 돌려 렌더링하는 `List` 컴포넌트를 만들었다. 처음에는 아주 심플하다. 단순히 데이터를 받아 `li` 태그로 뿌려주기만 하면 되었다. ![](https://velog.velcdn.com/images/koreanthuglife/post/9f45a8f1-4c78-4709-abd0-943e5776592a/image.png) 그런데 시간이 지나며 요구사항이 늘어난다. _1. 관리자 페이지 리스트에서는 우측에 삭제 버튼을 넣어주세요._ _2. 메인 페이지 리스트에서는 클릭 시 상세 모달을 띄워주세요._ _3. 검색 결과 리스트에서는 텍스트를 하이라이팅 해주세요._ 이 요구사항들은 리스트가 데이터를 순회한다는 사실과는 무관한, **각 아이템을 어떻게 보여줄 것인가에 대한 정책(Policy)**들이다. 이 두 가지를 분리하지 않고 `props`를 추가하며 컴포넌트 안에서 해결하려 한다면 어떻게 될까? `isAdmin`, `showModal`, `highlightKeyword` 등등의 `props`가 추가되기 시작한다. ![](https://velog.velcdn.com/images/koreanthuglife/post/92fb98e8-8cc0-4dd0-b431-288dd6f75225/image.png) 결국 `List` 컴포넌트는 데이터를 순회하는 본연의 기능(메커니즘) 외에, 수많은 UI 분기 처리(정책)까지 떠안게 된다. 단순히 코드가 길어지는 것이 문제가 아니다. **서로 다른 맥락(Context)의 정책들이 한 컴포넌트에 뒤엉키면서 강한 결합**이 발생한다. 결과적으로 유연한 확장은커녕, 새로운 요구사항이 생길 때마다 기존 로직을 건드려야 하는 **괴물 컴포넌트**가 되어버린다. 여기서 `Render Props` 패턴을 적용해 **순회(Mechanism)**와 **렌더링(Policy)**을 분리한다면 어떻게 될까? **"무엇을 그릴지"**에 대한 결정권을 부모 컴포넌트에 위임하면 된다. ![](https://velog.velcdn.com/images/koreanthuglife/post/2890c9c3-2373-4e31-9a55-828168fe8751/image.png) 이제 `List` 컴포넌트는 내부에 `if (isAdmin)` 같은 조건문이 단 한 줄도 없다. 그저 배열을 돌며 `renderItem` 함수를 호출할 뿐이다. **불변하는 메커니즘(순회)**만 남기고 **가변적인 정책(렌더링)**은 모두 밖으로 밀어낸 것이다. ![](https://velog.velcdn.com/images/koreanthuglife/post/71edf4c3-1e5b-4d46-b347-6f34bc599c33/image.png) 이제 위에서 발생했던 요구사항들을 다시 적용해보자. 정책은 컴포넌트를 사용하는 쪽(부모)에서 주입한다. ![](https://velog.velcdn.com/images/koreanthuglife/post/62e8719f-c680-48ac-aa96-451c17dc9c64/image.png) 이 패턴을 적용함으로써 얻는 이점은 명확하다. 1. **높은 재사용성:** `List` 컴포넌트는 이제 어떤 데이터가 들어오든, 어떤 UI를 그려야 하든 상관없이 '순회'가 필요한 곳이라면 어디든 재사용 가능하다. 2. **유연한 확장성:** 새로운 디자인의 리스트가 필요해도 `List` 컴포넌트를 수정할 필요가 없다. 새로운 `renderItem` 함수만 넘겨주면 된다. 3. **관심사의 분리:** 데이터 흐름 제어(Mechanism)와 UI 표현(Policy)이 완벽하게 분리되었다. #### 예제2: 데이터 요청과 상태 관리 이번에는 `UserProfile` 컴포넌트를 통해 데이터 요청 로직을 살펴보자. 우리가 흔히 작성하는 데이터 요청해 UI를 그리는 컴포넌트는 아래와 같다. ![](https://velog.velcdn.com/images/koreanthuglife/post/34f2e788-9897-486e-846f-a61cc80f07af/image.png) 여기서 "불변하는 부분(Mechanism)"은 무엇일까? 바로 **네트워크 요청을 보내고, 대기 상태를 관리하고, 성공/실패에 따라 상태를 업데이트하는 행위** 그 자체다. 우리가 유저 정보를 가져오든, 상품 목록을 가져오든, 이 비동기 통신의 라이프사이클(요청 -> 대기 -> 응답)은 절대 변하지 않는다. 반면 "가변적인 부분(Policy)"은 **어떤 데이터를 보여줄 것인가**와 **그 데이터를 어떻게 그릴 것인가**이다. 이 반복되는 메커니즘을 추상화해 분리하면 어떨까? ![](https://velog.velcdn.com/images/koreanthuglife/post/e7094086-f585-4065-90d5-5af920562d41/image.png) 혹은 `Suspense`를 활용하면, 비동기 상태를 관리하는 메커니즘뿐만 아니라 대기 및 에러 상태에 대한 **부수적인 정책(Loading/Error UI)**까지 상위로 위임하게 된다. 덕분에 컴포넌트 내부에는 **데이터가 준비되었을 때 무엇을 보여줄 것인가** 라는 핵심 정책만 남길 수 있다. ![](https://velog.velcdn.com/images/koreanthuglife/post/48dd3b8a-b5a3-426a-af4a-826206440832/image.png) 위 예제들에서도 확인할 수 있는것 처럼 개인적으로 정책과 메커니즘을 분리하며 느꼈던 점이 있다. 정책과 메커니즘을 분리하니 코드가 자연스레 선언적이게 된다는 것이다. > **선언적 프로그래밍이란?** > 어떻게(How)가 아닌 무엇을(What) 원하는지를 기술하는 프로그래밍 패러다임. > [Wikipedia](https://en.wikipedia.org/wiki/Declarative_programming) 자연스레 세부 사항(메커니즘)에 대한 부분을 감추며 가변적인(정책) 영역을 외부에서 결정할 수 있도록 열어두게 된다. 즉, 컴포넌트가 **'어떻게(How) 동작하는가'**라는 복잡한 과정은 추상화된 메커니즘 뒤로 숨고, 우리가 실제로 관심을 갖는 **'무엇을(What) 보여줄 것인가'**라는 의도만 명확하게 남게 되는 것이다. 이제 아마 "정책과 메커니즘의 분리"에 대해 어느정도 감이 잡혔을거라 생각한다. 다시 `Render Props` 패턴으로 돌아가보자. ### `Render Props` 패턴을 활용한 정책과 메커니즘의 분리 앞서 살펴본 것처럼 정책과 메커니즘의 분리는 시스템에서 **불변하는 하부 구조(Mechanism)**와 **가변적인 운영 규칙(Policy)**을 분리하는 설계 원칙이다. 이를 통해 우리는 코드의 재사용성과 유연성을 확보할 수 있다. 최근 프론트엔드 생태계에서 각광받는 Headless UI 라이브러리들(Radix UI, TanStack Table 등)이 지향하는 바도 이와 정확히 일치한다. 스타일이나 렌더링 방식(정책)은 전혀 포함하지 않은 채, 기능적인 동작과 상태 관리(메커니즘)만을 제공함으로써 무한한 확장성을 제공한다. 하지만 나는 이 원칙을 단순히 기계적인 '불변'과 '가변'의 이분법으로만 바라봐서는 안 된다고 생각한다. 나는 이 분리의 과정에서 조금 더 확장된 시야를 가질 필요가 있다고 생각한다. > _"The only constant is change." — Heraclitus_ > 유일하게 변하지 않는 것은, 모든 것이 변한다는 사실뿐이다 소프트웨어 엔지니어링에서 좋은 코드, 좋은 아키텍처 같은 것들의 궁극적인 목표는 결국 **변화에 유연하게 대응하기 위함**이다. 따라서 우리는 단순히 코드가 변하냐 안 변하냐를 넘어, **"어디가 더 자주, 더 높은 확률로 변할 것인가?"**를 고민해야 한다. 정책(Policy) 안에서도 **변경의 빈도**에 따라 층위가 나뉠 수 있다고 생각한다. 비즈니스 요건에 따라 매주, 심지어 매일 바뀔 수 있는 정책이 있는가 하면, 한번 정해지면 꽤 오랫동안 불변하는 정책도 있다. 예를들어, **프랜차이즈 카페의 운영 방식**을 생각해보자. * **본사 지침(Slow Policy):** "모든 라떼는 우유 200ml에 샷 2개를 넣는다." 이 레시피는 맛의 일관성을 위해 몇 년이고 변하지 않는 단단한 정책이다. * **시즌 프로모션(Fast Policy):** "이번 크리스마스에는 딸기 토핑을 무료로 얹어준다." 혹은 "비 오는 날에는 10% 할인한다." 같은 마케팅 정책은 상황과 시기에 따라 수시로 변한다. 두 가지 모두 정책이지만, 변화의 속도와 수명은 완전히 다르다. 이처럼 정책과 메커니즘의 경계는 고정되어 있지 않으며, 우리가 처한 **환경**에 따라 상대적으로 결정된다. 예를 들어, 보통 개발에서 **UI**는 기획에 따라 수시로 바뀌는 대표적인 '정책' 영역으로 분류된다. 하지만 우리 팀에 견고한 **디자인 시스템**이 구축되어 있다면 어떨까? 디자인 시스템이 갖춰진 환경에서 버튼의 생김새, 컬러 팔레트, 모달의 인터랙션 방식은 더 이상 매번 고민해야 할 가변적인 정책이 아니다. **시스템 내부에서 규정된, 꽤 오랫동안 변하지 않는 규칙**이 된다. 이 맥락에서 디자인 시스템은 단순한 UI 뭉치가 아니라, 그 자체로 UI를 찍어내는 하나의 **메커니즘**으로 작용한다. 즉, 무엇이 정책이고 무엇이 메커니즘인지는 **"현재 우리 시스템에서 무엇이 더 자주 변하는가?"**라는 질문에 따라 유연하게 정의될 수 있는 것이다. 정책과 메커니즘의 경계는 절대적이지 않다. 우리가 처한 환경과 비즈니스 맥락에 따라 그 범위를 어디까지로 책정할 것인지는 **나름**인 것이다. 이러한 관점에서 `Render Props` 패턴을 다시 정의해보자. `Render Props` 패턴의 진정한 가치는 단순히 렌더링을 위임하는 것이 아니라, 컴포넌트 내부에서 **변경될 가능성이 가장 높은 부분**을 감지하고, 그 책임을 부모에게 전적으로 위임해 버리는 데 있다. 즉, 단순히 [가변 vs 불변]의 분리가 아니라, **[변경 확률이 높은 영역 vs 낮은 영역]**의 분리다. 리스트를 순회하는 로직은 내부에 두고, 아이템을 어떻게 보여줄지는 외부로 위임함으로써, 요구사항 변경에 유연하게 대응할 수 있도록 해준다. 여기까지 생각이 미치자 자연스럽게 다음 고민이 이어졌다. > "기능(메커니즘)과 UI(정책)를 분리하는 것, 커스텀훅으로도 할 수 있지 않나?" `Render Props`와 커스텀훅, 두 가지 도구 모두 **분리**와 **재사용**이라는 목적을 달성할 수 있다. 그렇다면 우리는 언제 컴포넌트 기반 추상화를 사용하고, 언제 훅 기반 추상화를 사용해야 할까? 이제 내가 파악한 그 선택의 기준에 대해 알아보려 한다. ## 커스텀 훅 기반의 추상화와 컴포넌트 기반의 추상화 언제 어떤 추상화를 사용하는게 효과적일까? 커스텀 훅을 기반으로 추상화 하는 것과 컴포넌트 기반으로 추상화 하는것 둘 사이의 차이는 뭘까? 열심히 조사한 내용을 `NotebookLM`에 넣어둔 뒤 **"커스텀 훅 기반의 추상화와 컴포넌트 기반의 추상화 둘 사이의 차이점은 뭐야?"** 라는 질문을 했을때 아래와 같이 대답했다. > 핵심부터 말씀드리면, 훅은 "행동(Behavior)과 상태(State)"를 추상화하는 도구이고, 컴포넌트는 "구조(Structure)와 렌더링(Rendering)"을 추상화하는 도구입니다. - [NotebookLM](https://notebooklm.google.com/notebook/24f4628c-370f-4a60-a596-dead4998800a) 이 답변을 곱씹어보며 내가 느꼈던 두 방식의 결정적인 차이를 정리해보았다. ### 1. 데이터의 위치 Tanstack Query의 `useSuspenseQuery`와 suspensive의 `<SuspenseQuery>`를 통해 알아보자. > Tanstack Query: https://tanstack.com/query/latest/docs/framework/react/reference/useSuspenseQuery > suspensive: https://suspensive.org/ko/docs/react-query/SuspenseQuery 기능적으로는 동일하게 데이터를 가져오지만, 이 둘은 데이터를 다루는 '접근 방식'과 '구조'에서 큰 차이가 있다. 먼저 **커스텀 훅(`useSuspenseQuery`)**이다. ![](https://velog.velcdn.com/images/koreanthuglife/post/d0473548-a792-4788-b0c5-5c979a9c9504/image.png) 훅을 호출하는 순간, 가져온 데이터는 내 컴포넌트의 지역 변수가 된다. 즉, **데이터가 내 컴포넌트 전체 스코프에 병합**된다. 컴포넌트 내부 어디서든 자유롭게 쓸 수 있지만, 반대로 말하면 컴포넌트 전체가 이 데이터에 의존하게 된다. ![](https://velog.velcdn.com/images/koreanthuglife/post/df172cc0-d7ee-4e31-a80c-cdac6d3ce9cb/image.png) 반면 컴포넌트 기반의 추상화에서 데이터는 `<SuspenseQuery>`가 제공하는 렌더 프롭 함수 내부라는 **특정 블록** 안에서만 유효하다. 부모인 `UserProfile`은 데이터의 존재를 알 필요가 없다. ### 2. 렌더링 제어의 범위와 시점 내가 생각하는 컴포넌트 기반 추상화의 또 다른 강력한 이점은 **렌더링의 영향 범위를 제어할 수 있다**는 점이다. **커스텀 훅**은 호출되는 순간, 해당 훅을 사용하는 **부모 컴포넌트 전체**의 생명주기에 관여한다. ![](https://velog.velcdn.com/images/koreanthuglife/post/86b9e985-b17d-49a3-a440-f4c0f17c5ca7/image.png) 위 코드에서 `useSuspenseQuery`가 데이터를 가져오는 동안, `Dashboard` 컴포넌트 전체가 `Suspense` 상태에 빠진다. 즉, 데이터를 보여주는 부분뿐만 아니라, 데이터와 무관한 `<Header />`까지 렌더링이 지연된다. 제어권이 컴포넌트 전체 레벨로 묶이는 것이다. 반면 컴포넌트 기반 추상화를 사용하면 이 제어권을 필요한 부분으로 국소화할 수 있다. ![](https://velog.velcdn.com/images/koreanthuglife/post/71b18cef-16eb-4ef4-998d-0d21a7597d59/image.png) 이제 데이터 요청 로직이 `<SuspenseQuery>`라는 컴포넌트 경계 안에 갇혔다. 덕분에 데이터가 로딩 중이더라도 `<Header />`는 즉시 화면에 뜨고, 데이터가 필요한 부분만 로딩 스피너가 돌게 된다. 즉, 로직을 컴포넌트로 감싸면 **"어느 시점에, 어느 부분만 렌더링을 지연시키거나 에러를 처리할지"**에 대한 시공간적인 제어가 가능해진다. ### 3. 로직과 UI의 결합 개인적으로 가장 크게 체감한 차이점은 바로 **기능과 UI의 결합도(Coupling)**와 **응집도(Cohesion)** 부분이다. > 참고: [직곽적인 클린코드 가이드](https://velog.io/@koreanthuglife/%EA%B0%80%EC%9E%A5-%EC%A7%81%EA%B4%80%EC%A0%81%EC%9D%B8-%ED%81%B4%EB%A6%B0%EC%BD%94%EB%93%9C-%EA%B0%80%EC%9D%B4%EB%93%9C) `Overlay`(모달) 같은 기능을 생각해보자. 모달은 '열림/닫힘 상태'와 '보여지는 껍데기'가 떼려야 뗄 수 없는 관계다. 이 둘은 항상 함께 다녀야 한다. 이걸 **커스텀 훅**으로 구현하면 어떻게 될까? ![](https://velog.velcdn.com/images/koreanthuglife/post/8021a806-89a2-4fa3-98ca-8ae09271f179/image.png) 필연적으로 **로직(상태)은 위쪽**에, **UI(JSX)는 아래쪽**에 위치하게 된다. 하나의 기능을 구현하는데 코드가 위아래로 흩어진다. 만약 이 모달을 지워야 한다면? 위쪽의 훅과 아래쪽의 JSX를 찾아서 각각 지워줘야 한다. 즉, 응집도가 떨어진다. 반면 **컴포넌트 기반 추상화**를 사용하면 이야기가 달라진다. ![](https://velog.velcdn.com/images/koreanthuglife/post/61905483-5544-47d7-bc36-305f9f6b35f1/image.png) 상태를 관리하는 로직과 UI가 `<Overlay>`라는 울타리 안에 **한 덩어리로 뭉쳐있다.** 코드가 훨씬 선언적이고, 읽는 순서대로 동작이 이해된다. 기능을 삭제할 때도 이 덩어리만 들어내면 끝이다. 즉, **UI와 강하게 결합될 수밖에 없는 기능**이라면, 컴포넌트로 묶어서 추상화하는 것이 로직들을 한곳에 모으고(응집도를 높이고) 선언적으로 만드는 데 훨씬 효과적이다. ## 다시 생각해보기 앞서 얘기한 디스커션의 내용에 대한 내 의견을 다시 한 번 정리해보려 한다. ### `SavingList` 컴포넌트 내가 생각한 `SavingList` 컴포넌트가 수행해야할 책임은 아래와 같다. 1. 데이터 요청 2. 데이터 순회 3. 빈 데이터 UI 표현 4. 에러 UI 표현 5. 로딩 UI 표현 6. 자식 UI 렌더링 그렇다면 여기서 어떤 부분이 가변적인가를 고민해봤다. 1. 데이터 요청 -> `SavingList`가 다른 곳에서 사용된다고 해도 적금 리스트 데이터를 요청하는 일은 달라지지 않을 가능성이 크다. 2. 데이터 순회 -> `SavingList`가 다른 곳에서 사용된다고 해도 적금 리스트 데이터를 순회하는 일은 달라지지 않을 가능성이 크다. 3. 빈 데이터 UI 표현 -> `SavingList`가 다른 곳에서 사용된다면 빈 데이터 UI는 달라질 수 있다. 4. 에러 UI 표현 -> `SavingList`가 다른 곳에서 사용된다면 에러 UI는 달라질 수 있다. 5. 로딩 UI 표현 -> `SavingList`가 다른 곳에서 사용된다면 로딩 UI는 달라질 수 있다. 6. 자식 UI 렌더링 -> `SavingList`가 다른 곳에서 사용된다면 자식 UI는 달라질 수 있다. 그러므로 아래 처럼 구현하게 될거라 생각한다. ![](https://velog.velcdn.com/images/koreanthuglife/post/62fae260-5a3e-48e1-874b-ca10a66be7e1/image.png) 가변성이 큰 부분들에 대한 로직을 호출하는 부모에게 위임하고, 변경될 가능성이 적은 1, 2번의 로직을 `SavingList` 컴포넌트 내부로 감추는 것이다. ### `SavingItem` 컴포넌트 내가 생각한 `SavingItem` 컴포넌트의 기능은 아래와 같다. 1. 클릭 이벤트 처리 2. 적금 요소 UI 표현 3. 선택된 적금 UI 표현 여기서 내가 생각한 가변적인 부분은 2번과 3번이다. 다만 여기서 나는 이 부분들의 책임을 부모에게 위임하면 안된다고 판단했다. 그 이유는 컴포넌트의 '책임'과 '정체성' 때문이다. `SavingList`는 데이터를 나열하는 **구조**를 담당하기에, 그 안의 내용물을 외부에 맡기는 것이 유연함을 더해준다. 반면 `SavingItem`은 구체적인 데이터를 시각화하는 **내용**을 담당하는 말단 컴포넌트다. 만약 말단 컴포넌트인 `SavingItem`조차 렌더링을 외부에 위임해버린다면(`Render Props`), 이 컴포넌트는 도메인 맥락을 잃어버린 채 단순히 클릭 이벤트만 전달하는 **빈 껍데기**가 되어버린다. "적금 상품"이라는 도메인 정보를 시각적으로 표현하는 것은 `SavingItem`의 핵심 책임(불변의 영역)에 가깝다. 따라서 미세한 스타일 변경(선택 상태 등)은 `Props`나 `Variant` 패턴을 통해 내부에서 소화하고, 렌더링 자체를 위임하지 않는 것이 응집도를 높이고 실질적인 재사용성을 지키는 길이라 판단했다. 그래서 아래와 같은 형태가 될 것 같다. ![](https://velog.velcdn.com/images/koreanthuglife/post/67758a43-7856-4862-bc9b-d043544eff17/image.png) 이와 관련된 고민을 하며 얻었던 인사이트가 있다. **"어디까지 추상화할 것인가?"**의 답은, 그 컴포넌트가 '흐름을 제어하는 컨테이너'인지, '정보를 표현하는 콘텐츠'인지를 구분하는 데서 시작되는게 아닐까 하는 것이다. 이에 대해 AI에게 질문했는데 아래처럼 잘 정리해줬다. 참고가 되길 바라며 공유한다. > #### A. 흐름을 제어하는 컨테이너 (Container) → "열어야 한다" - **역할:** "무엇"을 보여줄지는 관심 없고, "어떻게" 배치하고 흐르게 할지만 담당합니다. (예: List, Layout, ModalWrapper) - **전략:** 내부를 비워둬야 합니다. 즉, **제어의 역전(Inversion of Control)**이 필요합니다. - **Why?** 컨테이너가 내용물(Content)을 구체적으로 알수록 재사용성은 0에 수렴합니다. SavingList가 SavingItem을 직접 import 해서 렌더링하는 순간, 그 리스트는 ProductItem을 담을 수 없게 되니까요. - **결론:** 그래서 Render Props나 Children 패턴을 써서 **내부를 외부로 위임(Open)**해야 합니다. > > #### B. 정보를 표현하는 콘텐츠 (Content) → "닫아야 한다" - **역할:** 나는 "무엇"인지를 명확히 보여줘야 합니다. (예: SavingItem, UserCard, Button) - **전략:** 내부를 감춰야 합니다. 즉, **캡슐화(Encapsulation)**가 필요합니다. - **Why?** 콘텐츠가 렌더링 방식을 외부에 너무 많이 위임하면, 디자인의 **일관성(Consistency)**이 무너집니다. SavingItem을 쓰는데 페이지마다 폰트가 다르고 레이아웃이 제각각이면 디자인 시스템이 존재할 이유가 없죠. - **결론:** 그래서 Props로 데이터만 받고, 렌더링 로직은 내부에 숨겨야(Close) 합니다. 그래서 종합적으로 아래처럼 사용하게 될거라 생각한다. ![](https://velog.velcdn.com/images/koreanthuglife/post/60eda851-c7e5-408b-afe1-68d066bc3962/image.png) 다만 글을 작성하는 이 시점에 드는 생각으로는 네임스페이스로 연관된 컴포넌트를 묶어주는 방식으로 구현해도 좋을거 같다. 그렇게 하면 로딩, 에러 UI를 재사용 해야하는 경우를 대비할 수 있고 연관된 컴포넌트들을 모아 응집도를 높일 수 있기 때문이다. ![](https://velog.velcdn.com/images/koreanthuglife/post/ce4dbbd2-960c-4c49-84ea-af31b63c884b/image.png) 처음 공부 했을때는 이 내용들이 머리에 둥둥 떠다니는 느낌이었다면 이번에 글을 작성하며 어느정도 정리가 된 느낌이 든다. 회사 코드에 적용하며 조금 더 고민을 해봐야겠다는 생각이 든다.