Search

5장_인터페이스

5장. 인터페이스: What의 세 가지 얼굴

인터페이스는 What을 표현하는 방법이에요. 4장에서 배운 What/How 분리가 코드에서 어떻게 나타나는지, 이 장에서 다뤄요.
인터페이스를 잘 설계하면 호출자와 구현자 사이에 명확한 경계가 생겨요. 호출자는 “뭘 할 수 있는지”만 알면 되고, 구현자는 “어떻게 할지”를 자유롭게 결정할 수 있죠.
왜 인터페이스가 어려울까요? 관점 전환 비용 때문이에요. 구현자 관점(내가 어떻게 짤까)은 자동으로 떠올라요. 호출자 관점(다른 사람이 어떻게 쓸까)은 의식적 노력이 필요해요. 구현하면서 동시에 호출자 관점을 유지하려면 두 관점을 동시에 들고 있어야 해요. 그게 고비용 작업이에요.
인터페이스는 이 비용을 구조적으로 낮춰줘요.
이 장에서 다루는 것:
인터페이스 = What의 표현: What/How 분리를 코드로 어떻게 나타내는가
세 가지 관점: 일반해, 협력, What 드러내기

What/How 분리의 코드 버전

4장에서 What/How 분리를 배웠어요. 프로그래밍에서는 이걸 인터페이스/구현 분리라고 불러요.
교과서 용어
프로그래밍 용어
What
인터페이스
How
구현
What 안정
인터페이스에 의존하라
How 변화
구현은 언제든 바뀔 수 있다
인터페이스라는 단어가 어렵게 느껴질 수 있어요. 하지만 본질은 간단해요:
인터페이스 = “뭘 할 수 있는지”를 약속한 것
인터페이스는 세 가지 얼굴을 가지고 있어요. 각각 살펴볼게요.

5.1 첫 번째 얼굴: 일반해

왜 일반해(표준 패턴)가 중요할까요? 호출자가 뭘 할 수 있을지 예측할 수 있으면, “이건 뭘 하는 건가?” 시뮬레이션 비용이 줄어들어요. 웹 표준 패턴을 따르면 이 비용이 거의 0이 돼요.

이미 존재하는 해결책에 매칭하기

일반해는 발명하는 게 아니라 발견하는 것이에요. 프론트엔드 세계에는 이미 수십 년간 검증된 패턴들이 있어요.
프론트엔드 아키타입 (이미 존재하는 일반해) ├── Input (value, onChange) ├── Toggle (checked, onChange) ├── Selector (value, onChange, options) ├── List (items, renderItem) ├── Dialog/Modal (isOpen, onClose) ├── Form (onSubmit, children) └── Router (path, element)
Plain Text
복사
이 패턴들은 HTML 표준과 주요 라이브러리에서 이미 확립되어 있어요. 새로 만들 필요 없이 매칭만 하면 돼요.

일반해 매칭하는 사고 흐름

“월을 선택하는 컴포넌트”를 만든다고 해볼게요.
// 🤔 사고 흐름 // 1. 본질이 뭐지? → "값을 선택하는 것" // 2. 아키타입은? → Input (값을 입력/선택) // 3. 표준 인터페이스? → value, onChange, min, max // ✅ 결과: HTML input[type='month']와 동일한 인터페이스 <MonthSelector value={selectedMonth} onChange={setSelectedMonth} min="2024-01" max="2024-12" />
TypeScript
복사
처음 보는 개발자도 MonthSelector의 사용법을 예측할 수 있어요. “아, Input 계열이니까 value랑 onChange가 있겠네?”
핵심: 일반해를 찾는 게 아니라, 본질에 맞는 일반해에 매칭하는 거예요.

예측 가능성 > 재사용성

많은 개발자가 “재사용할 수 있는 컴포넌트”에 집착해요. 하지만 진짜 중요한 건 예측 가능성이에요.
// ❌ 구현을 드러내는 인터페이스 <Navigation month={month} setMonth={setMonth} isDisabled={disabled} /> // setMonth? 상태 setter를 직접 넘기네? 내부 구현이 보여요 // ✅ 표준을 따라 예측 가능 <MonthSelector value={month} onChange={setMonth} disabled={disabled} /> // value, onChange, disabled - HTML 표준과 동일
TypeScript
복사
인터페이스의 추상화 레벨을 맞춰야 해요.
Navigation이라는 이름은 “어딘가로 이동한다”는 높은 수준의 What이에요. 그런데 props는 month, setMonth처럼 “월 상태를 관리한다”는 낮은 수준의 How예요. 이름과 props의 추상화 레벨이 안 맞아요.
MonthSelector는 이름도 props도 “월을 선택한다”는 같은 레벨이에요. 웹 표준을 따르면 설명 없이도 사용법을 알아요.

주의: 구현 세부사항을 인터페이스로 노출하지 않기

아키타입을 따를 때 흔한 실수가 있어요. 구현 세부사항을 인터페이스로 끌어올리는 것이에요.
// ❌ 구현 세부사항(동기/비동기)을 인터페이스로 분리 const form = useForm({ validate: { email: isEmail, // 동기 검증 }, validateOnServer: { email: checkDuplicate, // 비동기 검증 }, });
TypeScript
복사
이 인터페이스의 문제점:
동기/비동기는 구현 방식(How)이지, 책임(What)이 아니에요
사용자가 알아야 할 것: “email을 어떤 규칙으로 검증할지”
사용자가 몰라도 되는 것: “그 검증이 동기인지 비동기인지”
// ✅ What만 표현: 검증 규칙만 선언 const form = useForm({ validate: { email: [isEmail, checkDuplicate], // 뭘로 검증할지만 }, }); // 라이브러리 내부가 알아서 처리: // - 동기면 바로 실행 // - 비동기면 await // - 순서대로 실행, 첫 실패에서 멈춤
TypeScript
복사
핵심: “이 값이 유효한가?”는 하나의 책임이에요. 동기든 비동기든 구현 세부사항일 뿐, 인터페이스를 나눌 이유가 없어요.

보편적 언어

개발 세계에는 이미 합의된 “보편적 언어”가 있어요:
영역
보편적 언어
상태
value, onChange, disabled
동작
open/close, show/hide
상태 표시
loading/success/error
이벤트
onSubmit, onCancel, onSelect
// ❌ 자의적인 언어 showProductModal(); startUserFetch(); clickProductSelection(); // ✅ 보편적 언어 open(); loading; onSelect();
TypeScript
복사

언제 적용하면 좋을까

적극적으로 적용할 대상 (High ROI):
디자인 시스템 컴포넌트 (Button, Modal, Input 등)
여러 팀/프로젝트에서 공유하는 모듈
앱 내에서 2~3회 이상 유사하게 구현된 기능
굳이 적용 안 해도 되는 대상 (Low ROI):
일회성 이벤트 페이지
매우 특수한 도메인 로직
다른 곳에서 재사용될 가능성이 없는 코드

5.2 두 번째 얼굴: 협력

호출자와 구현자 사이의 계약

인터페이스의 두 번째 얼굴은 협력 구조예요.
┌─────────────┐ ┌─────────────┐ │ 호출자 │ ──────▶ │ 인터페이스 │ │ (사용하는 쪽) │ │ (계약) │ └─────────────┘ └──────┬──────┘ │ ┌──────▼──────┐ │ 구현자 │ │ (만드는 쪽) │ └─────────────┘
Plain Text
복사

역할 분담

호출자가 말하는 것 (What):
// "유저 정보를 가져와줘" const user = await fetchUser(id);
TypeScript
복사
호출자는 “뭘 하고 싶은지”만 말해요. 어떻게 하는지는 관심 없어요.
구현자가 결정하는 것 (How):
// 구현자가 알아서 결정 // - REST API vs GraphQL // - 캐싱 여부 // - 에러 처리 방식 // - 재시도 로직 async function fetchUser(id: string): Promise<User> { // ... 복잡한 구현 }
TypeScript
복사
구현자는 “어떻게 할지”를 결정해요. 호출자는 몰라도 돼요.
인터페이스는 둘 사이의 약속:
// 약속: "id를 주면 User를 돌려줄게" fetchUser(id: string): Promise<User>
TypeScript
복사

좋은 협력의 조건

호출자 관점에서 설계하기
// ❌ 구현자 편의 중심: 호출자가 How를 알아야 함 function fetchUser( id: string, useCache: boolean, // 캐시 쓸지 말지? retryCount: number, // 몇 번 재시도? timeout: number // 타임아웃? ): Promise<User>; // ✅ 호출자 편의 중심: What만 말하면 됨 function fetchUser(id: string): Promise<User>;
TypeScript
복사
좋은 인터페이스는 호출자의 의도를 먼저 생각해요. 구현 세부사항은 내부에서 합리적으로 결정해요.

왜 호출자 관점이 어려울까?

다른 사람의 관점을 취하는 건 뇌의 전두엽을 많이 쓰는 고비용 작업이에요.
내 관점: 공짜로 떠올라요 (System 1)
다른 관점: “이 사람은 뭘 알고, 뭘 모를까?”를 시뮬레이션해야 해요 (System 2)
구현하면서 동시에 호출자 관점을 유지하기 어려운 이유예요. 인지 자원이 이미 “구현”에 쓰이고 있거든요.
실천 팁: 구현 끝나고 “호출자 관점 점검 시간”을 따로 두세요. 동시에 하려면 작업기억이 터져요.
0장 비용 프레임으로 보면: 호출자 관점이 특히 비용이 높은 이유는 세 축 모두에서 비용이 발생하기 때문이에요. 시간 축(관점 전환의 이득이 나중), 관점 축(시뮬레이션 에너지), 자동화 축(구현과 관점 이중 소모).

실무에서 호출자 관점 고민하기

인터페이스를 설계할 때 세 가지를 고민해보세요.
1. 맥락 부여: 호출자가 알아야 할 것만
// ❌ 과한 맥락: 호출자가 내부 구조를 알아야 함 interface Props { userId: string; userCache: Map<string, User>; fetchStrategy: 'lazy' | 'eager'; retryCount: number; } // ✅ 필요한 맥락만: 호출자는 "누구"만 알면 됨 interface Props { userId: string; }
TypeScript
복사
호출자가 알 필요 없는 How(캐시, 전략, 재시도)를 노출하면, 호출자의 인지 부하가 커져요.
2. 표현: 의도가 드러나는 이름
// ❌ 구현 노출: "상태를 설정한다" <Component setIsOpen={setIsOpen} /> // ✅ 의도 표현: "열기/닫기 동작" <Component onOpen={handleOpen} onClose={handleClose} />
TypeScript
복사
setIsOpen은 구현(상태 setter)을 드러내요. onOpen/onClose는 의도(동작)를 드러내요.
3. 자유도 제어: 할 수 있는 것만 허용
// ❌ 과도한 자유도: 뭐든 할 수 있음 interface Props { config: Record<string, any>; } // ✅ 적절한 자유도: 정해진 옵션 중 선택 interface Props { size: 'small' | 'medium' | 'large'; variant: 'filled' | 'outlined'; }
TypeScript
복사
자유도가 높으면 “뭘 넣어야 하지?” 혼란이 생겨요. 자유도를 제어하면 “이 옵션 중에서 고르면 되는구나”가 돼요.

실무 예시: React 커스텀 훅

// ❌ 협력이 안 되는 훅: 호출자가 너무 많이 알아야 함 function useUser(id: string) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // ... 호출자가 이 세 가지 상태를 다 관리해야 함 } // ✅ 협력이 잘 되는 훅: 호출자는 What만 const { data: user, isLoading, error } = useQuery(["user", id], fetchUser); // 캐싱, 재시도, 에러 처리는 useQuery가 알아서 함
TypeScript
복사

5.3 세 번째 얼굴: What 드러내기

이름만 봐도 뭔지 알게 하기

인터페이스의 세 번째 얼굴은 What을 드러내는 것이에요.
Before: What이 숨어있음
// ❌ 함수명이 의도를 말해주지 않음 function process(data: any, flag: boolean, mode: string) { // ...무슨 작업을 하는 건지 코드를 읽어야 알 수 있음 } // ❌ 파라미터가 의도를 말해주지 않음 function handle(a: number, b: number, c: boolean) { // a, b, c가 뭔지 모름 }
TypeScript
복사
After: What이 드러남
// ✅ 함수명이 의도를 말해줌 function validateOrder(order: Order): ValidationResult { // "주문을 검증한다"가 바로 보임 } // ✅ 파라미터가 의도를 말해줌 function getShippingCost({ weight, distance, isExpress }: ShippingParams): number { // 배송비에 뭐가 필요한지 바로 보임 }
TypeScript
복사

좋은 인터페이스 체크리스트

1. 한 문장으로 설명 가능
// ✅ "유저의 권한을 확인한다" function checkUserPermission(user: User, action: Action): boolean; // ❌ 한 문장으로 설명하기 어려움 (여러 가지 일을 함) function processUserAndSendNotificationIfNeeded( user: User, config: Config ): Result;
TypeScript
복사
2. 호출자가 How를 몰라도 사용 가능
// ✅ How를 몰라도 됨 const formatted = formatCurrency(10000); // "10,000원" // ❌ How를 알아야 함 const formatted = formatCurrency(10000, "KRW", "ko-KR", true, false); // true, false가 뭔지 알아야 쓸 수 있음
TypeScript
복사
3. 구현이 바뀌어도 호출 코드 안 바뀜
// 인터페이스 const user = await fetchUser(id); // 구현이 REST → GraphQL로 바뀌어도 // 호출 코드는 그대로
TypeScript
복사

5.4 세 관점의 통합

세 얼굴은 결국 같은 것을 다른 각도에서 본 거예요.
관점
질문
잘된 인터페이스
일반해
다른 상황에도 쓸 수 있나?
여러 구현을 품을 수 있음
협력
호출자가 이해하기 쉬운가?
호출자 의도 중심 설계
What 드러내기
이름만 봐도 뭔지 아는가?
의도가 명확히 보임

세 관점으로 점검하기

// 점검할 인터페이스 function sendNotification( userId: string, type: "email" | "sms" | "push", message: string ): Promise<boolean>;
TypeScript
복사
일반해 관점:
“알림을 보낸다”라는 일반해 제공
email/sms/push 어느 것이든 같은 방식으로 호출
협력 관점:
호출자는 “누구에게, 어떤 수단으로, 무슨 내용”만 말함
발송 실패 시 재시도 같은 How는 내부에서 처리
What 드러내기 관점:
sendNotification - 의도 명확
파라미터가 각각 뭔지 명확

어느 관점부터 볼까?

상황에 따라 시작점이 달라요.
상황
먼저 볼 관점
이유
새 컴포넌트 설계
일반해
이미 있는 패턴에 매칭
기존 코드 리팩토링
What 드러내기
현재 의도 파악 먼저
API/훅 설계
협력
호출자 관점 먼저
: 세 관점은 순서가 아니라 렌즈예요. 같은 코드를 세 렌즈로 보면 다른 게 보여요.

5.5 주의: interface 키워드 ≠ 인터페이스 개념

TypeScript에는 interface 키워드가 있어요. 하지만 이것과 개념으로서의 인터페이스는 달라요.

키워드로서의 interface

// TypeScript 문법 interface User { name: string; email: string; } // 이건 "타입 정의"일 뿐 // 개념적 인터페이스가 아닐 수 있음
TypeScript
복사

개념으로서의 인터페이스

// 함수 시그니처도 인터페이스 function validateOrder(order: Order): ValidationResult // API 경계도 인터페이스 POST /api/users { name: string, email: string } // React 컴포넌트 props도 인터페이스 <Button onClick={handler} disabled={isLoading}> 저장 </Button> // 커스텀 훅 반환값도 인터페이스 const { data, isLoading, error } = useQuery(...)
TypeScript
복사
핵심: 인터페이스는 What/How 경계가 있는 모든 곳에 존재해요.
interface 키워드: 타입 정의 문법
인터페이스 개념: What/How 경계 (훨씬 넓은 의미)

정리

얼굴
핵심
적용
일반해
이미 존재하는 아키타입
본질 파악 → 표준 패턴 매칭
협력
호출자-구현자 사이의 계약
호출자 의도 먼저
What 드러내기
이름이 의도를 말해줌
한 문장 설명 가능

왜 세 얼굴을 합치면 비용이 줄어들까요?

호출자 입장에서 “뭘 하는지(What)”만 알아도 되는 상태가 돼요. 관점 전환 비용 ↓, 예측 가능성 ↑. System 2 에너지가 절약되니까 비용 < 이익이 되어 자연스럽게 그 함수를 사용하게 돼요.
인터페이스 설계 체크리스트:
다른 상황에도 쓸 수 있는가? (일반해)
호출자가 How를 몰라도 되는가? (협력)
이름만 봐도 뭘 하는지 아는가? (What 드러내기)
구현이 바뀌어도 호출 코드가 안 바뀌는가?
다음 장에서는 실무 사례와 함정을 다뤄요. 인터페이스 설계가 실제 코드에서 어떻게 나타나는지, 어떤 실수를 피해야 하는지 볼게요.