4장. 코드 추상화
드디어 코드예요.
1~3장에서 추상(What/How 분리)과 추상화(그걸 만드는 과정)를 다뤘어요. 이번 장에서는 그걸 코드에 적용해요.
코드 추상화란
코드 추상화를 “함수 추출”이나 “클래스 분리” 같은 거라고 생각하기 쉬워요. 틀린 건 아니지만, 그건 기법이에요. 본질이 아니에요.
코드 추상화의 본질은 1장에서 배운 거예요. What과 How를 분리하는 것. 코드에서도 똑같아요.
•
What: 이 코드가 무엇을 하는가
•
How: 이 코드가 어떻게 하는가
함수를 쪼개도, 파일을 나눠도, 클래스를 만들어도 — What과 How가 섞여 있으면 여전히 복잡해요. 반대로, 함수 하나에 다 들어있어도 What과 How가 분리되어 있으면 읽기 쉬워요.
이번 장에서는 코드에서 What과 How를 분리하는 방법을 다뤄요.
일단 짠다
프론트엔드는 화면에서 시작해요.
디자인 시안을 보고, 화면을 그리고, 코드를 맞춰가요. 처음부터 완벽한 구조를 설계하고 시작하지 않아요. 일단 짜요.
function RegistrationForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [name, setName] = useState('')
const [phone, setPhone] = useState('')
const [emailError, setEmailError] = useState('')
const [passwordError, setPasswordError] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const handleSubmit = async () => {
setIsSubmitting(true)
if (!email.includes('@')) {
setEmailError('이메일 형식이 아니에요')
setIsSubmitting(false)
return
}
if (password !== confirmPassword) {
setPasswordError('비밀번호가 일치하지 않아요')
setIsSubmitting(false)
return
}
// ... 제출 로직
}
return (
<form onSubmit={handleSubmit}>
{/* 입력 필드들 */}
</form>
)
}
TypeScript
복사
useState가 8개예요. 검증 로직이 handleSubmit 안에 섞여 있어요. 복잡해요.
복잡해지는 게 신호예요. “이제 정리할 때”라는 신호.
Inside-Out: 올라가기
복잡해졌으면 정리해요. 어떻게?
안에서 밖으로 올라가요.
1단계: 다 펼쳐놓기
일단 코드를 그대로 봐요. 숨기지 않아요.
// 지금 있는 것들
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [name, setName] = useState('')
const [phone, setPhone] = useState('')
const [emailError, setEmailError] = useState('')
const [passwordError, setPasswordError] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
TypeScript
복사
2단계: 리프부터 이름 붙이기
가장 안쪽, 가장 구체적인 부분부터 봐요.
email, password, confirmPassword, name, phone — 이건 뭐지? 폼 값이에요.
emailError, passwordError — 이건 뭐지? 검증 에러예요.
isSubmitting — 이건 뭐지? 제출 상태예요.
이름이 붙었어요:
- 폼 값들
- 검증 에러들
- 제출 상태
3단계: 올라가며 경계 발견
이름을 붙이다 보니 경계가 보여요. “여기까지가 하나의 덩어리구나.”
// 폼 값들 — 하나의 덩어리
const formValues = {
email: '',
password: '',
confirmPassword: '',
name: '',
phone: ''
}
// 검증 에러들 — 하나의 덩어리
const errors = {
email: '',
password: ''
}
// 제출 상태 — 하나의 덩어리
const isSubmitting = false
TypeScript
복사
더 올라가봐요. 이 세 덩어리를 묶으면 뭐지? 폼 상태예요.
// 폼 상태 전체
interface FormState {
values: FormValues
errors: FormErrors
isSubmitting: boolean
}
TypeScript
복사
What이 발견됐어요. “이 코드는 폼 상태를 관리해요.”
이게 Inside-Out이에요. 안(세부)에서 시작해서 밖(전체)으로 올라가면서 경계를 발견해요. 아래에서 위로 올라가는 방향이에요.
Wishful Thinking: 내려오기
Inside-Out으로 What을 발견했어요. 이제 반대로 가봐요.
밖에서 안으로 내려가요.
“이런 게 있으면 좋겠다”를 먼저 상상해요. 구현이 어떻게 되든 상관없이.
function RegistrationForm() {
// 이런 게 있으면 좋겠다
const form = useForm()
return (
<Form {...form}>
<Field name="email" />
<Field name="password" />
<Field name="confirmPassword" />
<Field name="name" />
<Field name="phone" />
<SubmitButton />
</Form>
)
}
TypeScript
복사
useForm()이 실제로 어떻게 구현되는지는 나중에 생각해요. <Form>과 <Field>가 어떻게 렌더링되는지도 나중에 생각해요. 지금은 What만 있으면 돼요.
What을 먼저 상상하고, How는 나중에 채워요. 위(탑레벨)에서 아래(세부)로 내려가는 방향이에요.
이게 Wishful Thinking이에요. “이미 있다고 가정하고” 생각해요.
왕복
실제로는 한 방향으로만 가지 않아요. 왔다 갔다 해요.
왜 왕복인가
•
Inside-Out만 하면: 세부에서 출발해서 올라가는데, 방향을 잃을 수 있어요. 코드는 정리됐는데 “이게 뭘 하는 건지” 모르겠어요.
•
Wishful Thinking만 하면: 이상적인 인터페이스를 상상했는데, 막상 구현하려니 안 맞아요.
그래서 왕복해요.
구체적인 과정
1.
일단 짠다 — 화면 보면서 코드 작성
2.
복잡해진다 — useState 8개, 로직 얽힘
3.
IO로 올라간다 — 펼쳐놓고, 이름 붙이고, 경계 발견
4.
What 발견 — “폼 상태를 관리하는 거구나”
5.
WT로 내려온다 — “이런 인터페이스가 있으면 좋겠다”
6.
구현하다가 — 빠진 게 보임 (유효성 검사 규칙, 비동기 검증…)
7.
다시 IO로 올라간다 — 새로 발견한 것 반영
8.
다시 WT로 내려온다 — 인터페이스 조정
이걸 반복해요.
예시: 회원가입 폼 왕복
1차 시도 (일단 짬)
function RegistrationForm() {
const [email, setEmail] = useState('')
// ... 8개 useState
const handleSubmit = () => {
if (!email.includes('@')) { /* ... */ }
// 검증 로직이 여기저기
}
}
TypeScript
복사
복잡해요.
IO로 올라감
펼쳐놓고 보니까:
- 값들이 있고
- 에러들이 있고
- 검증 로직이 있어요
경계가 보여요. “폼”이라는 덩어리.
WT로 내려옴
function RegistrationForm() {
const form = useForm({
initialValues: { email: '', password: '', ... },
validate: { email: isEmail, password: minLength(8) }
})
}
TypeScript
복사
“이런 게 있으면 좋겠다.”
구현하다가 — 문제 발견
이메일 중복 검사는 서버에 물어봐야 해요. 동기 검증만 있으면 안 돼요.
다시 IO로 올라감
새로운 경계 발견: 동기 검증 / 비동기 검증
다시 WT로 내려옴
const form = useForm({
validate: {
email: isEmail, // 동기
},
asyncValidate: {
email: checkDuplicate // 비동기
}
})
TypeScript
복사
이렇게 왕복하면서 글로벌 최적에 가까워져요. 한 방향만 가면 로컬 최적에 빠져요.
일반해
왕복을 반복하다 보면 패턴이 보여요.
회원가입 폼, 로그인 폼, 설정 폼, 검색 폼… 다 비슷한 구조예요. 값, 에러, 제출 상태, 검증 로직.
이 패턴에 이름을 붙이면 Form이에요.
React Hook Form, Formik, react-final-form 같은 라이브러리들이 이미 있어요. 이건 누군가가 “폼”이라는 What에 이름을 붙이고, How를 구현해둔 거예요.
이렇게 반복되는 What에 이름을 붙인 것을 일반해라고 불러요.
•
Form: 입력값 + 검증 + 제출
•
Dialog: 열기/닫기 상태 + 내용
•
List: 데이터 배열 + 렌더링 + 페이지네이션
•
Query: 비동기 데이터 + 로딩/에러/성공 상태
일반해를 알면 강력해요. “이건 Form이네” 하고 알아채는 순간, What이 바로 보여요. 이름을 붙일 필요도 없어요. 이미 붙어 있으니까.
3장에서 배운 “구조 사전”이 바로 이거예요. 자주 보이는 What들의 목록.
코드 구조: 1장 개념의 적용
1장에서 배운 추상의 구조가 코드에서 어떻게 나타나는지 볼게요.
레이어
1장에서 레이어(수직 관계)를 다뤘어요. 코드에서는 어떻게 나타날까요?
레스토랑 예시의 코드 버전:
// 상위 레이어: 손님 입장 (What만 보임)
function handleRequest(request: OrderRequest) {
return processOrder(request.items)
}
// 중간 레이어: 주방장 입장 (위에서는 How, 아래에서는 What)
function processOrder(items: MenuItem[]) {
return items.map(item => cook(item))
}
// 하위 레이어: 요리사 입장 (구체적인 How)
function cook(item: MenuItem) {
const recipe = getRecipe(item.name)
return executeRecipe(recipe)
}
TypeScript
복사
위에서 보면 handleRequest만 있어요. “요청을 처리한다.” What이에요.
한 단계 내려가면 processOrder가 보여요. 위에서는 How였지만, 아래에서는 What이에요.
핵심: 상위 레이어의 How가 하위 레이어의 What이에요. 이게 코드에서 레이어가 작동하는 방식이에요.
협력
1장에서 협력(수평 관계)도 다뤘어요. 서로 다른 What들이 각자의 역할을 갖고 메시지로 소통해요.
레스토랑의 수평 협력 코드 버전:
// 각자의 책임(What)을 가진 함수들
function cook(order: Order): Dish[] {
return order.items.map(item => prepare(item))
}
function serve(tableId: number, dishes: Dish[]): void {
deliverTo(tableId, dishes)
}
function checkout(tableId: number): Receipt {
return processPayment(tableId)
}
// 협력: 함수 호출을 통해 소통
function handleCustomer(tableId: number, order: Order) {
const dishes = cook(order) // cook에게 메시지
serve(tableId, dishes) // serve에게 메시지
return checkout(tableId) // checkout에게 메시지
}
TypeScript
복사
각 함수는 자기 역할(What)만 알아요. 다른 함수의 How는 몰라요.
handleCustomer는 조율자예요. “요리해줘”, “서빙해줘”, “계산해줘”라는 메시지만 보내요.
축척
같은 것이 관점에 따라 What도 되고 How도 돼요.
// React 컴포넌트 관점에서
function UserProfile({ userId }: Props) {
const user = useUser(userId) // useUser는 What: "유저 정보를 가져온다"
return <div>{user.name}</div>
}
// useUser 내부 관점에서
function useUser(userId: string) {
// 여기서 useUser는 How가 됨
// 새로운 What: "캐시된 데이터를 가져온다"
return useQuery(['user', userId], () => fetchUser(userId))
}
// fetchUser 내부 관점에서
async function fetchUser(userId: string) {
// 또 한 단계 내려감
// 새로운 What: "HTTP 요청을 보낸다"
return fetch(`/api/users/${userId}`).then(r => r.json())
}
TypeScript
복사
useUser는 UserProfile 관점에서 What이지만, useQuery 관점에서는 How예요.
축척의 실용적 의미: 코드 리뷰할 때 “이게 뭘 하는 거야?”라는 질문에 어느 수준에서 대답할지를 정하는 거예요.
•
높은 축척: “유저 정보를 가져와”
•
중간 축척: “캐시를 확인하고 없으면 API 호출해”
•
낮은 축척: “HTTP GET 요청을 /api/users/:id로 보내”
차원 분리
1장의 곰탕 메뉴판 예시를 코드로 표현해볼게요.
Before: 차원이 분리 안 됨
// 모든 조합을 나열
type MenuItem =
| 'KkoriGomtang_S_Mild'
| 'KkoriGomtang_S_Medium'
| 'KkoriGomtang_S_Spicy'
| 'KkoriGomtang_M_Mild'
// ... 27개 조합
TypeScript
복사
After: 차원별로 분리
// 각 차원을 독립적으로
type MenuType = 'KkoriGomtang' | 'DoganiTang' | 'ModemSuyuk'
type Size = 'S' | 'M' | 'L'
type Spiciness = 'Mild' | 'Medium' | 'Spicy'
// 조합은 구조로 표현
interface Order {
menu: MenuType
size: Size
spiciness: Spiciness
}
TypeScript
복사
What(메뉴, 사이즈, 맵기)이 분리되니까:
- 새 옵션 추가가 쉬움
- 조합이 자동으로 늘어남
- 타입 시스템이 잘못된 조합을 잡아줌
정리
코드 추상화는 코드에서 What과 How를 분리하는 것이에요.
방법
1.
일단 짠다 — 화면 보면서, 복잡해지는 게 신호
2.
Inside-Out — 안에서 밖으로 올라가며 What 발견
3.
Wishful Thinking — 밖에서 안으로 내려오며 인터페이스 정의
4.
왕복 — IO
︎WT 반복하며 글로벌 최적 찾기
5.
일반해 — 반복되는 What에 이름 붙이기
구조
1장에서 배운 것 | 4장에서 적용 |
레이어 (수직) | 상위 How = 하위 What |
협력 (수평) | 각자의 What, 메시지로 소통 |
축척 (관점) | 어느 레벨에서 설명할지 |
차원 분리 | 독립적인 What들의 조합 |
핵심
•
코드 추상화 ≠ 함수 추출
•
코드 추상화 = What/How 분리를 코드에 적용
•
IO로 올라가고, WT로 내려오고, 왕복하며 구조를 찾는다
다음 장에서는 함정과 주의점을 다뤄요.
