Search

인터페이스 설계 멘탈 모델 v3

Part 1. 핵심 원리

한 문장 정의

인터페이스 설계란, 분리된 것들을 어떤 형태로 드러내고, 어떻게 연결할지 결정하는 일이다.
인터페이스 설계는 추상화 이후에 오는 작업이다. What과 How가 분리되어 있고, 각 모듈이 "뭘 하는지"는 알고 있는 상태에서 시작한다.

생성적 핵심 문장들

아래 문장들은 나머지를 파생시키는 씨앗이다. 이것들만 기억해도 많은 것을 재구성할 수 있다.

대전제

설계가 정말 필요한가? 프론트엔드는 대부분 복잡하지 않다. 숙련도가 낮으면 뭉쳐두는 게 낫다. 잘못된 추상화 경계가 안 생긴다.

비용과 선택

좋은 인터페이스는 없는 인터페이스다. 싸우지 않고 이기는 게 최선. 차력쇼는 금물.
구조에는 비용이 있다. 분리에도 비용이 있다. 이름 짓기, 인자 설계, 시점 이동. 그래서 인라인이 더 나을 때가 있다.
자기 완결적인 게 충분히 좋다. 분리의 이득과 비용을 저울에 달아본다. 막연한 추측이거나 자기만족일 경우가 많다. 응집을 우선하자.

좋은 인터페이스

좋은 인터페이스는 이미 있다. HTML, HTTP, value/onChange, isOpen/onClose. 면적이 작고, 예측 가능하고, 뻔하고, 조합 가능한 것들.
지저분한 덩어리는 일반과 구체로 인수분해한다. 일반해에 매칭되는 것은 빙고처럼 지워버리고, 남는 자투리만 직접 설계한다.

좋은 컴포넌트

좋은 컴포넌트는 기획/디자인과 1:1 매칭된다. What을 중심으로 압축되어 있고, 열었을 때 요구사항이 그대로 보인다.
좋은 컴포넌트는 일 잘하는 동료다. 맡은 일이 명확하고, 몰라도 되는 건 안 물어보고, 필요한 것만 주고받는다.
좋은 컴포넌트는 뻔하다. 생김새가 뻔해서 기존 지식 활용도가 높고, 들여다보지 않아도 예측 가능하다.

분해와 조립

요구사항의 언어가 경계가 된다. 기획서가 '할인'이라 부르면, 코드에서도 '할인'이 경계가 된다.
안에서 바깥으로, leaf에서 출발한다. 그러면 애초에 큰 추상화가 생기기 힘들다.
좋은 조합은 레이어드 옷차림이다. 큰 것도 여러 작은 추상화가 겹겹이 쌓여서 만들어진다.

진행 방식

점진적으로, 작은 비용으로, 알게 되는 것을 조금씩. 작은 피드백 사이클을 여러 번 돌면 매 사이클마다 학습이 일어나 더 똑똑한 선택을 할 수 있게 된다. 이 격차가 복리 효과를 낸다. 작은 도미노가 여러 단계를 거쳐 큰 도미노를 쓰러뜨리는 것과 같다.
확정한 것은 기반으로, 불확실한 것은 옵션으로. 모르는 걸 미리 설계하지 마라.

전체 흐름 한눈에

┌─────────────────────────────────────────────────┐ │ 0. 설계가 필요한가? │ │ └─ 필요 없으면 → 뭉쳐둠, 끝 │ └─────────────────┬───────────────────────────────┘ │ 필요하면 ▼ ┌─────────────────────────────────────────────────┐ │ 1. 복잡성 회피 │ │ ├─ 일반해에 매칭 → 지움 │ │ ├─ 플랫폼/라이브러리 → 쓰고 끝 │ │ └─ 남는 자투리만 → 다음 단계로 │ └─────────────────┬───────────────────────────────┘ ▼ ┌─────────────────────────────────────────────────┐ │ 2. What 인수분해 │ │ ├─ 일반 / 구체 나눔 │ │ └─ 요구사항의 언어 = 경계 │ └─────────────────┬───────────────────────────────┘ ▼ ┌─────────────────────────────────────────────────┐ │ 3. 안에서 바깥으로 조립 │ │ ├─ leaf에서 출발 │ │ ├─ 작은 것들의 조합 (레이어드) │ │ └─ 구체는 바깥으로, IoC로 받음 │ └─────────────────┬───────────────────────────────┘ ▼ ┌─────────────────────────────────────────────────┐ │ 4. 형태 결정 │ 5. 연결 결정 │ │ ├─ 일반해 차용 │ ├─ 관절 선택 │ │ └─ 관용어 사용 │ └─ IoC 레벨 결정 │ └─────────────────┬───────────────────────────────┘ ▼ ┌─────────────────────────────────────────────────┐ │ 6. 점검 │ │ ├─ 호출자 관점에서 예측 가능한가? │ │ └─ 주니어 인수인계 / 콜센터 테스트 │ └─────────────────────────────────────────────────┘ ↺ 전 과정을 점진적으로 (작게, 반복)
Plain Text
복사

흐름 설명

0단계 (메타 조건): 먼저 "설계가 정말 필요한가?"를 묻는다. 프론트엔드는 대부분 복잡하지 않다. 숙련도가 낮으면 뭉쳐두는 게 낫다.
1단계 (복잡성 회피): 설계가 필요하다면, 먼저 싸우지 않고 이길 방법을 찾는다. 일반해에 매칭되면 지워버리고, 플랫폼이나 라이브러리가 풀어놓은 게 있으면 그걸 쓴다.
2단계 (분해): 남는 것들을 What 기준으로 인수분해한다. 일반과 구체로 나누고, 요구사항의 언어가 경계가 된다.
3단계 (조립): 가장 안쪽(leaf)에서 출발해서 바깥으로 조립한다. 작은 추상화들이 레이어드 옷차림처럼 겹쳐서 큰 것이 된다.
4~5단계 (형태+연결): 각 모듈을 어떤 형태로 드러내고, 어떤 관절로 연결할지 결정한다.
6단계 (점검): 호출자 관점에서 예측 가능한지 점검한다. 구현 모드와 점검 모드를 분리해서. 두 가지 상상을 해본다.
주니어 인수인계 테스트: 신규 입사한 주니어에게 이 인터페이스를 인수인계해야 한다고 상상해본다. 설명해야 할 사정이나 암묵적 맥락이 많아서 생각만 해도 지치거나, 말이 길어지고 숨이 찰 것 같다면 잘못된 것이다.
콜센터 테스트: 고객에게 전화로 이 개념을 설명한다고 상상해본다. 자연스럽게 말이 되면 고객 언어다.
진행 방식: 전 과정은 점진적으로 진행한다. 한번에 크게 하지 않고, 작은 단위로, 알게 되는 것을 조금씩 적용한다. 작은 피드백 사이클이 복리 효과를 낸다.

Part 2. 설계 전

0. 설계가 필요한가?

인터페이스 설계의 첫 번째 질문은 "설계가 정말 필요한가?"다.
구조를 만드는 데는 반드시 비용이 발생한다. 분리하면 이름을 지어야 하고, 인자를 설계해야 하고, 파일을 오가며 시점 이동을 해야 한다. 숙련도가 낮으면 이 비용을 감당하면서 ROI를 뽑기 어렵다. 분리가 잦아지거나 큰 설계를 자주 시도하면 비용이 기하급수적으로 증가한다.
뭉쳐두면 잘못된 추상화 경계가 안 생긴다. 나중에 기획/디자인이랑 같이 펼쳐놓고 보면 추상화의 단위가 보인다. 그때 분리해도 늦지 않다.

판단 기준

IF 숙련도 낮음 → 뭉쳐라 IF 분리가 잦음 → 멈춰라 IF 일회성 컴포넌트 → 관대하게 적용 IF 재사용 범위 넓음 (디자인 시스템, 공유 모듈) → 설계 투자 가치 있음
Plain Text
복사

예시: props가 데이터 통로로 전락할 때

// ❌ props가 통로로 전락 function ProductListPage() { const { data, isLoading, error } = useQuery(['products'], fetchProducts); const [filter, setFilter] = useState('all'); return ( <ProductList products={data} isLoading={isLoading} error={error} filter={filter} onFilterChange={setFilter} /> ); }
TypeScript
복사
이 값들이 어디서 쓰이는지 보면 결국 ProductList 내부다. 이렇게 하면:
데이터 페칭이 페이지 컴포넌트의 책임인지 모호해진다 (오히려 리스트 컴포넌트 책임에 가까워 보임)
ProductList를 동작시키려면 외부 컴포넌트가 필요하다는 결합이 생긴다
내 안에서 쓰이는 값들을 굳이 외부에서 받아서 응집도가 낮아진다
React 컴포넌트는 그 자체로 독립적이고 자기 완결적인 게 대체로 좋다. 굳이 데이터 페칭 등의 책임을 외부로 분리하느라 결합도를 높일 필요가 없다. 분리를 통해 얻는 이득과 지불하는 비용을 저울에 달아보면, 생각보다 막연한 추측이거나 자기만족일 경우가 많다.
// ✓ 자기 완결적 function ProductList() { const { data, isLoading, error } = useQuery(['products'], fetchProducts); const [filter, setFilter] = useState('all'); if (isLoading) return <Skeleton />; if (error) return <ErrorSection />; return ( <List items={data} filter={filter} onFilterChange={setFilter} /> ); }
TypeScript
복사
ProductList가 자기 책임을 자기 안에서 처리한다. 외부 의존 없이 독립적으로 동작한다.

예시: 데이터 페처 컴포넌트 패턴

데이터 페칭을 하나의 목적(What)으로 보고 별도로 정리할 수도 있다.
const GetProduct = ({ children }: { children: (product: Product) => React.ReactNode }) => { const { id } = useParams<{ id: string }>(); return ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary fallback={<ErrorSection onRetry={reset} />}> <Suspense fallback={<Skeleton />}> <SuspenseQuery {...productQueries.detail(Number(id))}> {({ data }) => <>{children(data)}</>} </SuspenseQuery> </Suspense> </ErrorBoundary> )} </QueryErrorResetBoundary> ); };
TypeScript
복사
function ProductDetailPage() { return ( <> <GetProduct> {product => ( <section> <ProductImage images={product.images} /> <ProductInfoSection product={product} /> <Stock label="재고" value={product.stock} /> <QuantityCounter product={product} /> <AddToCartButton product={product} /> </section> )} </GetProduct> <GetProduct> {product => ( <section> <Text variant="H2_Bold">상세 정보</Text> <Text>{product.detailDescription}</Text> </section> )} </GetProduct> <GetRecommendedProducts> {recommendedProducts => ( <section> <Text variant="H2_Bold">추천 제품</Text> <HStack> {recommendedProducts.map(product => ( <Link to={`/product/${product.id}`} key={product.id}> <RecommendedProduct product={product} /> </Link> ))} </HStack> </section> )} </GetRecommendedProducts> </> ); }
TypeScript
복사
GetProduct, GetRecommendedProducts는 "데이터를 가져온다"는 What을 담당하는 컴포넌트다. 로딩, 에러, Suspense 처리를 내부에서 해결하고, 성공했을 때의 데이터만 children에게 넘긴다. 페이지 컴포넌트는 "데이터가 있을 때 어떻게 보여줄지"만 신경 쓰면 된다.

1. 복잡성 회피

설계가 필요하다고 판단했다면, 먼저 싸우지 않고 이길 방법을 찾는다.
최고의 승리는 싸우지 않고 이기는 것이다. 차력쇼는 금물이다. 복잡한 로직도 직접 설계하기 전에, 이미 검증된 구조나 플랫폼이 풀어놓은 게 있는지 먼저 확인한다.

세 가지 회피 전략

1) 데이터/계산/액션 분리
코드를 익숙한 상황으로 몰고 간다.
데이터, 계산 → 순수 함수로 액션 → 면적 최소화
Plain Text
복사
2) 일반해 매칭
Form, List, Dialog, Input, Modal, Toggle, Selector... 이런 일반해에 패턴 매칭되면 빙고처럼 지워버린다.
"이거 뭐랑 닮았지?" → "List네" → 지움, 끝
Plain Text
복사
3) 플랫폼/라이브러리 활용
브라우저 API, es-toolkit, react-query, overlay-kit, react-router 등. 이미 풀어놓은 게 있으면 그걸 쓴다.
IF 복잡한 로직 → 먼저 플랫폼/라이브러리가 풀어놓은 게 있나 체크 IF 있으면 → 쓰고 끝 IF 없으면 → 그때 직접 설계 IF 직접 로직 짜고 있음 → "차력쇼 아닌가?" 자문
Plain Text
복사

인라인의 가치

handle- 뭐시기 하는 작은 핸들러 함수들도 웬만하면 인라인한다.
// ❌ 굳이 분리 const handleClick = () => { setCount(count + 1); }; <button onClick={handleClick}>+</button> // ✓ 인라인 <button onClick={() => setCount(count + 1)}>+</button>
TypeScript
복사
인라인하면:
이름을 안 지어도 된다
인자를 설계할 일이 없다 (암묵적 인자가 생기는 일도 없다)
사용처에 가까워져서 응집도가 높아지고, 시점 이동 비용도 없어진다
IF 작은 핸들러 함수 → 웬만하면 인라인 시도 IF 핸들러가 커지거나 재사용 필요 → 그때 분리 IF 분리했는데 암묵적 인자 생김 → 설계 재검토
Plain Text
복사

남는 자투리만 다음 단계로

일반해로 지우고, 플랫폼으로 해결하고, 인라인으로 단순화하고 나서도 남는 것들. 그것만 다음 단계(분해와 조립)로 가져간다.

Part 3. 분해와 조립

2. What 인수분해

복잡성 회피 전략을 적용하고 남은 자투리들. 이제 이것들을 What 기준으로 인수분해한다.

일반과 구체로 나눈다

지저분한 덩어리가 있다면 무조건 일반과 구체로 인수분해한다.
일반: 여러 곳에서 반복되는 패턴, 도메인에 무관한 것 구체: 이 맥락에서만 필요한 것, 도메인 특화된 것
Plain Text
복사
일반해에 매칭되는 것은 빙고처럼 패턴 매칭해서 지워버리고, 남는 자투리만 직접 설계한다.

요구사항의 언어가 경계가 된다

추상화의 단위는 어디서 오는가? 기획/디자인 문서에서 온다.
기획서가 '할인'이라 부르면 → 코드에서도 '할인'이 경계 기획서가 '장바구니'라 부르면 → 코드에서도 '장바구니'가 경계
Plain Text
복사
기획/디자인이랑 같이 펼쳐놓고 보면 추상화의 단위가 보인다. 도메인 언어가 곧 경계다.

프랙탈하게 같은 규칙이 반복된다

큰 덩어리도 작은 덩어리도 같은 규칙으로 분해한다. 종료 조건은 AST leaf node, 즉 더 분해할 What이 없을 때까지.
페이지 └─ 섹션 └─ 컴포넌트 └─ 요소 └─ (더 이상 분해할 What 없음)
Plain Text
복사

판단 기준

IF 일반 → 일반해의 idiomatic 인터페이스에 맞춤 IF 구체 → 바깥으로 밀고 IoC로 주입받기를 시도 IF 다음 중 하나면 → 구체를 내부로 담는다 (분리 안 함) - 응집도가 높아지는 경우 - 경감되는 고통 대비 설계 비용이 안 맞는 경우 - 기획 문서의 언어(변경의 단위)와 맞는 경우
Plain Text
복사

예시: ProductCard 분해

ProductCard (요구사항 덩어리) ├─ 이미지 (일반: Image 컴포넌트) ├─ 제목 (일반: Text 컴포넌트) ├─ 가격 (일반: Text + 포맷팅) ├─ 장바구니 버튼 (구체: 이 맥락 특화) └─ 좋아요 버튼 (구체: 이 맥락 특화)
Plain Text
복사
일반(이미지, 제목, 가격)은 일반해로 풀고, 구체(장바구니 버튼, 좋아요 버튼)는 바깥으로 밀어서 IoC로 받는다.

3. 안에서 바깥으로

분해했으면 이제 조립한다. 방향은 안에서 바깥으로.

leaf에서 출발한다

가장 안쪽, 더 이상 분해되지 않는 단위에서 시작한다. 그러면 애초에 큰 추상화가 한 덩어리로 생기기 힘들다.
시작점: AST leaf node (추상화 멘탈모델의 가장 안쪽) 방향: 안 → 바깥 결과: 작은 것들이 조합되어 큰 것이 됨
Plain Text
복사

레이어드 옷차림

좋은 조합은 레이어드 옷차림과 같다. 큰 것도 여러 작은 추상화가 겹겹이 쌓여서 만들어진다.
이너 → 셔츠 → 가디건 → 패딩조끼 → 목도리 → 코트 → 장갑
Plain Text
복사
몇 겹이든 작은 레이어를 겹치고 겹치고 겹칠 수 있다. 각 층은 자기 역할만 하고, 다른 층이 뭘 하는지 모른다. 층 사이는 결합이 끊긴 채 input/output으로만 조합된다.
// 기본 이미지 onClose: withFoo(domainFn, { config }) // 겹겹이 쌓기 onClose: withLogging(withAnalytics(withConfirm(closeFn, {...}), {...}), {...}) ↑ ↑ ↑ ↑ 레이어4 레이어3 레이어2 레이어1(코어)
TypeScript
복사
각 레이어는 자기 책임만 수행한다:
closeFn: 실제 닫기 동작
withConfirm: 확인 대화상자 추가
withAnalytics: 분석 이벤트 추가
withLogging: 로깅 추가
필요한 레이어만 골라서 조합한다. 빼고 싶으면 빼고, 추가하고 싶으면 추가한다.

조립 절차

1. 가장 안쪽(leaf)부터 시작 2. 데이터/계산을 순수함수로 빼냄 3. 일반 → 일반해의 표준 인터페이스 차용 4. 구체 → 바깥으로 밀고 IoC 5. 작은 것들을 조합해서 큰 덩어리 표현 6. 바깥으로 한 층씩 쌓아올림
Plain Text
복사

조립 실패 신호

IF 일반해 중심으로 조립 안 됨 → 추상화 경계 잘못 OR What 추출 잘못 IF props drilling 발생 → 억지로 뚫지 말고 의존성 역전 고려 IF 겹겹이 쌓여서 가장 안쪽에 플래그 필요 → 망함 신호 IF 조합 늘어날 때마다 컴포넌트 수정 → IoC 부족 IF showX 플래그 증가 → IoC로 전환 IF 큰 추상화가 한 덩어리로 생김 → 안에서 밖으로 안 했다는 신호
Plain Text
복사

훅 관련 주의사항

커스텀 훅은 일반 함수와 다르다. React 맥락 + Rule of Hooks 제약이 있다.
IF 내부에 hook 있음 → 커스텀 훅 OK IF 내부에 hook 없음 → use- 붙이지 말고 일반 함수로 IF 커스텀 훅이 커짐 → 데이터/계산을 순수함수로 빼낼 수 있는지 검토 IF 커스텀 훅 만들어야 함 → 일단 슬픈 상황으로 가정, 정당한 What 기반인지 검토
Plain Text
복사
잘 된 형태는 이렇다:
작은 hook에서 나온 데이터 + 순수함수 여러 겹 레이어드 + 사용처 가까이에서 결합
Plain Text
복사

예시: ProductCard 조립

분해 결과를 안에서 바깥으로 조립한다.
// ❌ 큰 덩어리가 한번에 function ProductCard({ product, onAddToCart, onLike }) { const [isLiked, setIsLiked] = useState(false); const [isInCart, setIsInCart] = useState(false); const handleAddToCart = () => { setIsInCart(true); onAddToCart(product); }; const handleLike = () => { setIsLiked(!isLiked); onLike(product); }; return ( <div> <img src={product.image} /> <h3>{product.name}</h3> <p>{formatPrice(product.price)}</p> {showCartButton && ( <button onClick={handleAddToCart}> {isInCart ? '담김' : '장바구니'} </button> )} {showLikeButton && ( <button onClick={handleLike}> {isLiked ? '♥' : '♡'} </button> )} </div> ); }
TypeScript
복사
// ✓ 안에서 바깥으로, IoC로 구체를 받음 function ProductCard({ product, bottomAddon }: { product: Product; bottomAddon?: React.ReactNode; }) { return ( <div> <img src={product.image} /> <h3>{product.name}</h3> <p>{formatPrice(product.price)}</p> {bottomAddon} </div> ); } // 사용처에서 구체를 주입 <ProductCard product={product} bottomAddon={ <Flex> <AddToCartButton product={product} /> <LikeButton product={product} /> </Flex> } />
TypeScript
복사
ProductCard는 "상품 카드"라는 What만 안다. 장바구니 버튼이 뭔지, 좋아요 버튼이 뭔지 모른다. 모르기 때문에 독립적이고, 모르기 때문에 뭐든 받을 수 있다.

Part 4. 인터페이스 확정

4. 형태 결정

분해하고 조립했으면, 이제 각 모듈을 어떤 형태로 드러낼지 결정한다.

일반해가 표준 인터페이스를 제공한다

"이거 뭐랑 닮았지?"를 먼저 묻는다. 일반해에 매칭되면 그 표준 인터페이스를 그대로 차용한다.
일반해
표준 인터페이스
Input
value, onChange, disabled
Toggle
checked, onChange
Selector
value, onChange, options
List
items, renderItem
Dialog
isOpen, onClose
Form
onSubmit, children
"이거 뭐랑 닮았지?" → "Toggle이네" → checked, onChange 그대로 차용 → 끝
Plain Text
복사
표준 인터페이스를 차용하면:
이름을 고민할 필요가 없다
사용자가 이미 알고 있다
예측 가능하다

관용어가 예측 가능성을 만든다

일반해가 없으면 관용어를 찾는다. 이미 널리 쓰이는 형태를 따른다.
on- : 이벤트 핸들러 (onClick, onChange, onClose) is- : boolean 상태 (isOpen, isLoading, isDisabled) has- : 존재 여부 (hasError, hasItems) render- : 렌더 함수 (renderItem, renderHeader) -ed : 과거/수동 상태 (disabled, checked, selected)
Plain Text
복사

콜센터 테스트

관용어도 없으면, 콜센터 테스트를 한다. 고객에게 전화로 이 개념을 설명한다고 상상해본다.
"이 버튼 누르면 모달이 닫혀요" → onClose ✓ "이 버튼 누르면 handleModalDismissAction이 실행돼요" → ✗
Plain Text
복사
자연스럽게 말이 되면 고객 언어다.

도메인 용어 vs 범용 용어

IF 재사용 범위 넓음 → 도메인 용어 제거 (onSelect) IF 내부용 → 도메인 용어 허용 (handleProductSelection)
Plain Text
복사

5. 연결 결정

형태가 정해졌으면, 이제 어떻게 연결할지 결정한다.

관절 심상

컴포넌트의 열린 부분을 관절로 생각한다.
문짝 (콜백): 밖에서 "뭘 할지" 결정 → onClose, onSubmit, onChange 어깨 (슬롯): 밖에서 "뭘 보여줄지" 결정 → children, bottomAddon, header 무릎 (설정): 밖에서 "어떤 변형인지" 결정 → variant, size, disabled 두개골 (고정): 열지 않음 → 내부 상수, 하드코딩
Plain Text
복사

관절 선택 기준

1. "이 관절이 없어도 이 컴포넌트인가?" 체크 2. Yes → 고정 (열지 않음) 3. No → 열어야 함 → 유형 결정
Plain Text
복사
열지 않는 것도 설계다. 모든 것을 열면 복잡해진다.
IF 관절 없어도 컴포넌트 성립 → 고정 (내부 상수) IF 밖에서 "뭘 할지" 결정 → 콜백 (onSubmit, onChange) IF 밖에서 "뭘 보여줄지" 결정 → 슬롯 (children, bottomAddon) IF 밖에서 "어떤 변형인지" 결정 → 설정 (variant, size)
Plain Text
복사

IoC 적용 레벨

Level 1 (기본): children, callback → 문제의 80% 해결 → 가장 먼저 시도 Level 2 (중급): named slots → 복잡한 레이아웃 → header, footer, sidebar 등 Level 3 (고급): render props, compound component, 로직 주입 → 라이브러리급 유연성 → 정말 필요할 때만
Plain Text
복사
IF 간단한 확장 → Level 1 (children, callback) IF 복잡한 레이아웃 → Level 2 (named slots) IF 라이브러리급 유연성 → Level 3 (render props, compound, 로직 주입) IF 자주 쓰는 조합 있음 → Preset으로 닫아둠
Plain Text
복사

기본 이미지

onClose: withFoo(domainFn, { config })
TypeScript
복사
onClose: withFoo(domainFn, { config }) ↑ ↑ ↑ ↑ 관절 레이어 실제로직 설정
Plain Text
복사
구체적인 것은 바깥으로 민다. 결합이 생기면 필사적으로 끊는다.

연결 결정 시 주의

IF 구체적인 것 → 바깥으로 밀어라 IF 결합이 생기면 → 필사적으로 끊어라 IF 목적/의도 없으면 → 하지 마라 IF "혹시 모르니까" → 비용으로만 전가됨 IF 플랫폼/라이브러리 개발자 아닌데 제네릭 빡세게 → 잘못된 신호
Plain Text
복사

6. 점검

형태와 연결이 확정됐으면, 호출자 관점에서 점검한다.

구현 모드와 점검 모드 분리

구현자 관점은 공짜로 얻어진다. 호출자 관점은 의식적 노력이 필요하다. 동시에 하면 작업기억이 터진다.
구현 끝 → 모드 전환 → 점검 시작
Plain Text
복사

두 가지 테스트

주니어 인수인계 테스트
신규 입사한 주니어에게 이 인터페이스를 인수인계해야 한다고 상상해본다.
- 설명해야 할 사정이나 암묵적 맥락이 많은가? - 생각만 해도 지치는가? - 말이 길어지고 숨이 찰 것 같은가? → 하나라도 Yes면 잘못된 것
Plain Text
복사
콜센터 테스트
고객에게 전화로 이 개념을 설명한다고 상상해본다.
- 자연스럽게 말이 되는가? - 전문 용어 없이 설명 가능한가? → 자연스럽게 말이 되면 고객 언어
Plain Text
복사

점검 질문들

- "인터페이스만 보고 안 까봐도 돼?" → 놀람 최소화 - "props가 통로로 전락하지 않았나?" → 본질이 드러나는가 - "이 관절이 왜 필요해?" → 불필요한 열림 - "함수가 받은 것만으로 일할 수 있어?" → 자기완결성
Plain Text
복사

IoC 성공 기준

- "이 컴포넌트는 무엇을 모르는가?" → 모름 목록이 길수록 좋음 - "새 요구사항이 왔을 때 어디를 수정하나?" → 사용처만 수정하면 성공, 컴포넌트 열어봐야 하면 IoC 부족 - "이 컴포넌트를 다른 곳에서 쓸 수 있나?" → 다른 프로젝트에 그대로 가져갈 수 있으면 성공
Plain Text
복사

사용자 거리에 따른 기준

IF 같은 팀 → 좀 거칠어도 됨 IF 다른 팀 → 내부를 몰라야 함, 깨지면 안 됨 IF 외부(라이브러리) → 버전 관리, 하위 호환성 필수
Plain Text
복사

Part 5. 진행 방식

인터페이스 설계에서 점진적이란

인터페이스는 한번에 완벽하게 설계되지 않는다. 사용해봐야 뭘 열어야 하는지 안다.

왜 중요한가

인터페이스는 사용처와의 계약이다. 한번 열면 닫기 어렵다.
잘못 열면 → 사용처가 의존함 → 닫으면 breaking change 안 열어도 될 걸 열면 → 복잡성만 증가 → 유지보수 비용
Plain Text
복사
그래서 처음엔 닫아두고, 긴장이 생길 때 연다.

어떻게 작동하는가

1) 처음엔 최소한으로 닫아둔다
// Step 1: 닫아둠 function ProductCard({ product }: { product: Product }) { return ( <div> <img src={product.image} /> <h3>{product.name}</h3> <p>{formatPrice(product.price)}</p> <AddToCartButton product={product} /> </div> ); }
TypeScript
복사
2) 사용처에서 긴장이 생기면 관절을 연다
// 사용처에서 긴장 발생: // "장바구니 버튼 대신 좋아요 버튼 넣고 싶은데..." // "버튼 두 개 다 넣고 싶은데..." // Step 2: 긴장이 생긴 부분만 연다 function ProductCard({ product, bottomAddon // 이 관절만 열림 }: { product: Product; bottomAddon?: React.ReactNode; }) { return ( <div> <img src={product.image} /> <h3>{product.name}</h3> <p>{formatPrice(product.price)}</p> {bottomAddon} </div> ); }
TypeScript
복사
3) IoC 레벨도 점진적으로 올라간다
Level 1 (children, callback) → 문제의 80% 해결 ↓ 긴장 발생 Level 2 (named slots) → 복잡한 레이아웃 ↓ 긴장 발생 Level 3 (render props, compound) → 라이브러리급 유연성
Plain Text
복사
처음부터 Level 3로 설계하지 않는다. Level 1으로 시작하고, 긴장이 생기면 올린다.

신호를 읽는다

IF showX 플래그가 늘어남 → IoC로 전환할 신호 IF 사용처마다 분기가 생김 → 관절을 열 신호 IF 제네릭이 빡세짐 → 과하게 열었다는 신호 IF "혹시 모르니까" 열고 있음 → 멈출 신호
Plain Text
복사

예시: 점진적 열림

// v1: 완전히 닫힘 <Dialog title="삭제" onClose={close}> 정말 삭제하시겠습니까? </Dialog> // v2: 긴장 발생 ("버튼 텍스트 바꾸고 싶은데") // → 버튼 관절만 연다 <Dialog title="삭제" onClose={close} confirmText="삭제" // 열림 cancelText="취소" // 열림 > 정말 삭제하시겠습니까? </Dialog> // v3: 긴장 발생 ("버튼 영역 자체를 커스텀하고 싶은데") // → 슬롯으로 전환 <Dialog title="삭제" onClose={close}> 정말 삭제하시겠습니까? <Dialog.Footer> // 슬롯으로 열림 <Button variant="danger">삭제</Button> <Button variant="ghost">취소</Button> </Dialog.Footer> </Dialog>
TypeScript
복사
각 단계에서 코드는 돌아간다. 긴장이 느껴질 때만 연다.

부록

체크리스트

□ 0. 설계가 필요한가? - 숙련도, 재사용 범위, 일회성 여부 확인 □ 1. 복잡성 회피 - 일반해에 매칭되는가? - 플랫폼/라이브러리가 풀어놓은 게 있는가? - 인라인으로 단순화할 수 있는가? □ 2. What 인수분해 - 일반과 구체로 나눴는가? - 요구사항의 언어가 경계인가? □ 3. 안에서 바깥으로 조립 - leaf에서 출발했는가? - 레이어드 옷차림처럼 겹쳐지는가? - 구체는 바깥으로 밀었는가? □ 4. 형태 결정 - 일반해/관용어를 따르는가? - 콜센터 테스트 통과하는가? □ 5. 연결 결정 - 필요한 관절만 열었는가? - IoC 레벨이 적절한가? □ 6. 점검 - 주니어 인수인계 테스트 통과하는가? - 콜센터 테스트 통과하는가? - 모름 목록이 긴가? - 사용처만 수정하면 되는가? □ 진행 방식 - 점진적으로 하고 있는가? - 한 번에 하나의 모자만 쓰고 있는가?
Plain Text
복사

나쁜 인터페이스 유형

나쁜 인터페이스들을 고용해야 하는 직원으로 비유해보면:
// 1) 소통 안 하는 음침한 블랙박스 직원 // 아무것도 소통하지 않고 음침하게 자기 영역을 감춤 function 소통안하는음침한블랙박스직원(): JSX.Element // 2) 잡다하게 요구만 많은 직원 // 별로 하는 일도 없는데 잡다하게 많이 요청함 function 잡다하게요구만많은직원(props: { userId: string userName: string userEmail: string userAge: number userGender: string userAddress: string userPhoneNumber: string userCreatedAt: Date userUpdatedAt: Date }): JSX.Element // 3) 오지라퍼 직원 // 남의 것까지 받아서 건드림 function 오지라퍼직원(props: { user: User setUser: (user: User) => void globalSettings: Settings setGlobalSettings: (settings: Settings) => void }): JSX.Element // 4) 횡설수설하는 직원 // 뭘 해달라는 건지 모르겠음 function 횡설수설하는직원(props: { data: unknown flag1: boolean flag2: boolean mode: 'A' | 'B' | 'C' type: string }): JSX.Element
TypeScript
복사

멘보샤 이야기

어떤 직원이 일할 때 멘보샤가 필요함 (eg. props로 멘보샤를 받음) 말이 되나 싶어서 일할 때 멘보샤가 왜 필요하냐고 꾸짖고 뺐음 그러자 공장 창고 물류가 멈춤 알고 봤더니 이 직원이 거래하는 거래처 직원이 멘보샤를 안 주면 일을 안 해주는 것이었음
Plain Text
복사
멘보샤라는 구체에 의존해버렸다. 뇌물/인센티브/협력조건 같은 추상에 의존했어야 했다.
인터페이스는 데이터의 통로가 아니다. 어떤 컴포넌트의 본질을 드러내는 게 인터페이스다.

실패 신호 모음

[조립 실패] - 일반해 중심으로 조립 안 됨 - props drilling 발생 - 겹겹이 쌓여서 가장 안쪽에 플래그 필요 - 조합 늘어날 때마다 컴포넌트 수정 - showX 플래그 증가 - 큰 추상화가 한 덩어리로 생김 [훅 실패] - 내부에 hook 없는데 use- 붙임 - 커스텀 훅이 비대해짐 - 정당한 What 없이 훅 생성 [인터페이스 실패] - 설명해야 할 암묵적 맥락이 많음 - 말이 길어지고 숨이 참 - 제네릭이 빡세짐 - "혹시 모르니까"로 관절 열음 [진행 실패] - 한번에 크게 함 - 두 모자 동시에 씀 - 10분 안에 안 끝남 - 신호 없이 시작함
Plain Text
복사
.