Search

7장_심화_사례

7장. 심화 사례

지금까지는 함수, 컴포넌트 수준의 추상화를 다뤘어요. 이 장에서는 시야를 넓혀서, 추상화가 언어와 플랫폼 수준에서 어떻게 작동하는지 볼게요.
핵심 질문: 언제 직접 통제하고, 언제 플랫폼에 위임할 것인가?
프론트엔드 개발을 하다 보면 우리는 종종 플랫폼(브라우저)에 위임하는 것직접 통제권을 갖는 것 사이의 선택에 직면해요. 뛰어난 엔지니어링이란 모든 것을 직접 제어하는 것이 아니라, 언제 플랫폼을 신뢰하고 작업을 위임할 것인가, 그리고 언제 기꺼이 비용을 지불하고 정교한 통제권을 가져올 것인가를 명확히 판단하고 선택하는 능력이에요.
왜 플랫폼이 패턴을 흡수할까요? 반복 비용을 제거하기 위해서예요. 수천 명의 개발자가 같은 패턴을 반복하면, 그건 집단적 비용이에요. 플랫폼이 그 패턴을 흡수하면 개별 개발자의 비용이 0이 돼요. What(의도)은 그대로인데 How(구현 부담)가 사라지는 거예요.
이 장에서 다루는 것: - 언어 초월 추상화: 같은 What(JSX)이 다른 How(JS, Rust, Python)로 구현되는 원리 - 플랫폼 흡수: 개발자들이 반복하던 패턴이 브라우저/언어 표준이 되는 과정

언어를 초월하는 추상화

JSX: 프로토콜이라서 가능해요

JSX를 봐요.
<Button onClick={handleClick}>클릭</Button>
TypeScript
복사
이 한 줄이 JavaScript에서도, Rust에서도, Python에서도 동작해요. 왜 그럴까요?
JSX는 프로토콜(사양)이에요. 특정 언어에 종속되지 않아요.
┌─────────────────────────────────┐ │ JSX Syntax (프로토콜) │ ← 언어 독립적 사양 │ <div>{expression}</div> │ └──────────┬──────────────────────┘ │ 파싱 ┌──────────▼──────────────────────┐ │ AST (추상 구문 트리) │ ← 중간 표현 (What) │ { type: 'div', │ │ children: [expression] } │ └──────────┬──────────────────────┘ │ 변환 (언어마다 다름) ┌──────────▼──────────────────────┐ │ Target Language Code │ ← 언어별 구현 (How) │ JS: React.createElement() │ │ Rust: html! macro │ │ Python: h() function │ └──────────┬──────────────────────┘ │ 실행 ┌──────────▼──────────────────────┐ │ Runtime (VDOM/HTML) │ └─────────────────────────────────┘
Plain Text
복사
핵심은 AST(추상 구문 트리)예요. JSX 문법이 파싱되면 언어와 무관한 구조(AST)가 돼요. 이 구조가 What이에요. 이걸 각 언어의 런타임이 자기 방식으로 해석해요. 그게 How예요.

언어별 변환

같은 JSX가 언어마다 다른 코드로 변환돼요.
JavaScript (React):
// JSX가 이렇게 변환됨 React.createElement(Button, { onClick: handleClick }, "클릭");
TypeScript
복사
Rust (Yew):
// 같은 구조, 다른 문법 html! { <button onclick={handleClick}>{ "클릭" }</button> }
Rust
복사
Python (Dash):
# 같은 구조, 다른 문법 html.Button("클릭", id="btn", n_clicks=0)
Python
복사
What (AST, 같음): { type: Button, props: { onClick }, children: ["클릭"] }How (변환 결과, 다름): createElement, 매크로, 함수

RSC도 프로토콜이에요

React Server Components도 마찬가지예요. RSC는 서버-클라이언트 경계를 넘을 때 직렬화/역직렬화를 어떻게 처리할지에 대한 프로토콜이에요.
[Server] [Client] Server Component │ 실행 ▼ JSX 객체 │ 직렬화 (RSC Payload) ▼ ─────────────────────────────────▶ 역직렬화 │ ▼ React Element
Plain Text
복사
프로토콜이기 때문에 React가 아닌 다른 런타임에서도, 규격에 맞게 구현체만 만들면 사용할 수 있어요.

왜 중요한가

JSX가 프로토콜이라는 걸 알면:
이식성 이해: React 개발자가 Yew(Rust)를 배울 때 진입 장벽이 낮은 이유
구조 재사용: 같은 컴포넌트 설계 패턴이 여러 언어에 적용되는 이유
사고방식 유지: 언어가 바뀌어도 “선언적 UI”라는 멘탈 모델은 그대로인 이유
핵심: 좋은 추상화는 프로토콜/사양이 돼요. What(AST)을 정의하면, How(구현)는 언어마다 달라도 돼요.

플랫폼이 복잡도를 흡수한다

JSX가 프로토콜이 되어 언어를 초월한 것처럼, 더 큰 패턴도 있어요. 플랫폼 자체가 진화하면서 복잡도를 흡수하는 거예요.

레이아웃의 진화

Before 신호: clearfix 패턴 반복, JavaScript로 높이 계산, 레이아웃 의도 흩어짐
Before: Float 시절
/* Clearfix 핵 - 모든 컨테이너마다 필요 */ .container::after { content: ""; display: table; clear: both; } .left { float: left; width: 30%; } .right { float: right; width: 70%; }
CSS
복사
// 같은 높이 맞추기 - CSS로 불가능해서 JS 필요 function equalizeHeights(elements) { const maxHeight = Math.max(...elements.map(el => el.offsetHeight)); elements.forEach(el => el.style.height = maxHeight + 'px'); }
JavaScript
복사
개발자가 직접 레이아웃 로직(clearfix, float 정리, 높이 계산)을 관리했어요. Float는 원래 텍스트 감싸기용인데, 레이아웃에 오용된 거예요.
After: CSS Grid
/* 의도만 선언 - 플랫폼이 복잡도 흡수 */ .container { display: grid; grid-template-columns: 3fr 7fr; align-items: stretch; /* 높이 자동 맞춤 */ }
CSS
복사
메커니즘:
Float 시대: 개발자 → clearfix 관리 + JS 높이 계산 → 레이아웃 Grid: 개발자 → 의도 선언 → 브라우저 레이아웃 엔진
Plain Text
복사
What: “왼쪽 3, 오른쪽 7 비율로 배치, 높이는 같게”
How: 브라우저가 알아서 처리
왜 중요한가: - 의도(What)만 선언하면 됨 - 구현 세부사항(How)은 브라우저 책임 - 리플로우 최적화도 브라우저가 처리 - 개발자가 신경 쓸 필요 없음
복잡도가 어디로 갔을까요? 플랫폼(브라우저)이 흡수했어요.

애니메이션의 진화

Before 신호: 매 프레임 JS 실행, easing 함수 직접 구현, progress 상태 수동 관리
Before: requestAnimationFrame 수동 구현
// 수동 애니메이션 구현 function animate(element, from, to, duration) { const start = performance.now(); function easeInOut(t) { // easing 함수 직접 구현 return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; } function frame(time) { const progress = (time - start) / duration; const current = from + (to - from) * easeInOut(progress); element.style.transform = `translateX(${current}px)`; if (progress < 1) { requestAnimationFrame(frame); // 매 프레임 JS 실행 } } requestAnimationFrame(frame); }
JavaScript
복사
JavaScript가 매 프레임 계산하고, 브라우저에 전달해요. 프레임 드롭, 배터리 소모, 코드 복잡도 모두 개발자 책임이에요.
After: Web Animations API
// 선언만 하면 끝 element.animate([ { transform: 'translateX(0)' }, { transform: 'translateX(100px)' } ], { duration: 1000, easing: 'ease-in-out' });
JavaScript
복사
메커니즘:
rAF 시대: JS 스레드 ──매 프레임 계산──▶ 렌더링 ──▶ 화면 Web Anim: 선언 ────────────────────▶ GPU ────▶ 화면
Plain Text
복사
What: “1초 동안 100px 이동, ease-in-out으로”
How: GPU가 처리 (JavaScript 개입 없음)
왜 중요한가: - 60fps 보장 (JS 블로킹 없음) - 배터리 효율 (GPU 오프로드) - 애니메이션 의도만 선언하면 됨
복잡도가 어디로 갔을까요? 브라우저 렌더링 엔진이 흡수했어요.

데이터 페칭의 진화

Before 신호: readyState 상태 머신, 콜백 중첩, 에러 핸들링 누락 가능
Before: XHR (콜백 지옥)
// 콜백 지옥 시대 const xhr = new XMLHttpRequest(); xhr.open('GET', '/api/user'); xhr.onload = function() { if (xhr.status === 200) { const user = JSON.parse(xhr.responseText); // 중첩된 요청... 지옥의 시작 const xhr2 = new XMLHttpRequest(); xhr2.open('GET', `/api/posts/${user.id}`); xhr2.onload = function() { if (xhr2.status === 200) { const posts = JSON.parse(xhr2.responseText); // 또 중첩... } }; xhr2.send(); } }; xhr.send();
JavaScript
복사
Middle: fetch
// Promise 기반 - 상태 머신 숨김 const user = await fetch('/api/user').then(r => r.json()); const posts = await fetch(`/api/posts/${user.id}`).then(r => r.json());
JavaScript
복사
fetch 뒤에서 일어나는 일 (브라우저가 흡수): - DNS 조회, TCP 핸드셰이크, TLS 협상 - 쿠키 자동 첨부, CORS 검증, 리다이렉트 추적 - 청크 인코딩, gzip 압축/해제
After: React Query
// What만 선언 const { data, isLoading, error } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
JavaScript
복사
React Query가 처리하는 것: - 캐싱, 백그라운드 리페칭, 중복 요청 제거 - 낙관적 업데이트, 에러 재시도 - 로딩/에러/성공 상태 관리
메커니즘 (Layer별 흡수):
XHR: 개발자 → 상태 머신 관리 → 데이터 fetch: 개발자 → Promise → 브라우저가 네트워크 처리 React Query: 개발자 → What 선언 → 라이브러리가 모든 것 처리
Plain Text
복사
What: “유저 데이터를 가져와”
How: 캐싱, 재시도, 에러 핸들링 — 라이브러리가 처리
왜 중요한가: - 개발자는 “뭘 가져올지”만 고민 - “어떻게 가져올지”는 검증된 구현 사용 - Layer별 복잡도 흡수의 전형적 예시
복잡도가 어디로 갔을까요? 라이브러리가 흡수했어요.

폼 검증의 진화

Before 신호: 정규식 반복 작성, 에러 메시지 하드코딩, showError 직접 구현, 검증 로직 흩어짐
Before: JavaScript
// 수동 폼 유효성 검사 function validateEmail(email) { const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // 정규식 반복 작성 return re.test(email); } function validateForm() { const email = document.getElementById('email'); if (!validateEmail(email.value)) { showError('Invalid email'); // 에러 메시지 하드코딩 (국제화 안 됨) return false; } // 다른 필드들도 각각 검증... } function showError(msg) { // 에러 UI 직접 구현... // 접근성? 스크린 리더? 직접 처리해야 함 }
JavaScript
복사
After: HTML5
<!-- 플랫폼이 제공하는 유효성 검사 --> <input type="email" required/> <input type="url" pattern="https://.*"/> <input type="number" min="0" max="100"/>
HTML
복사
브라우저가 흡수하는 것: - 유효성 검사 로직 + 에러 메시지 (국제화 자동) - 에러 UI 표시 + 포커스 이동 - 스크린 리더 지원 (접근성 자동) - Constraint Validation API
What: “이메일 형식이어야 함, 0-100 사이 숫자여야 함”
How: 브라우저 네이티브 검증
왜 중요한가: - 국제화 자동 (브라우저 언어 설정 따름) - 접근성 자동 (스크린 리더 지원) - 일관된 UX (플랫폼 표준) - 정규식 버그 걱정 없음 (검증된 구현)
복잡도가 어디로 갔을까요? HTML 스펙이 흡수했어요.

스크롤 감지의 진화

Before 신호: 스크롤 이벤트 throttle 필요, 스크롤 위치 수동 계산, 메인 스레드 블로킹
Before: scroll 이벤트
// 스크롤 위치 수동 계산 function throttle(fn, delay) { let last = 0; return function(...args) { const now = Date.now(); if (now - last >= delay) { last = now; fn.apply(this, args); } }; } window.addEventListener('scroll', throttle(() => { const scrolled = window.scrollY; const windowHeight = window.innerHeight; const docHeight = document.body.offsetHeight; // 바닥 근처인지 계산 if (scrolled + windowHeight >= docHeight - 100) { loadMore(); // 무한 스크롤 } }, 100));
JavaScript
복사
매 스크롤마다 JavaScript가 실행돼요. throttle을 안 걸면 프레임 드롭, 걸어도 메인 스레드 점유.
After: Intersection Observer
// 플랫폼이 최적화된 관찰 제공 const observer = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { loadMore(); } }); observer.observe(sentinel); // 바닥의 센티넬 요소 관찰
JavaScript
복사
What: “이 요소가 화면에 보이면 알려줘”
How: 브라우저가 최적화된 타이밍에 콜백
왜 중요한가: - 비동기 + 배치 처리 = 성능 보장 - 무한 스크롤, 레이지 로딩의 표준 방식 - 개발자는 “언제”만 신경, “어떻게 감지할지”는 브라우저 책임
복잡도가 어디로 갔을까요? Intersection Observer API가 흡수했어요.

컴포넌트화의 진화

Before 신호: jQuery 의존성, 플러그인 충돌 가능, 초기화 로직 수동
Before: jQuery 플러그인
// 수동 컴포넌트 시스템 $.fn.myComponent = function(options) { return this.each(function() { // 복잡한 초기화 로직 // 스타일 충돌? 네임스페이스? 직접 관리 }); }; // 사용 $('.container').myComponent({ color: 'red' });
JavaScript
복사
After: Web Components
// 플랫폼 레벨 컴포넌트 class MyElement extends HTMLElement { connectedCallback() { this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = ` <style>/* 스타일 격리 자동 */</style> <slot></slot> `; } } customElements.define('my-element', MyElement);
JavaScript
복사
<!-- 네이티브 HTML처럼 사용 --> <my-element>컨텐츠</my-element>
HTML
복사
What: “재사용 가능한 커스텀 요소”
How: Shadow DOM이 스타일 격리, 라이프사이클 자동 관리
왜 중요한가: - 프레임워크 무관 재사용 (React, Vue, 바닐라 JS 어디서든) - 플랫폼 표준 = 오래 살아남음 - jQuery 의존성 제거
복잡도가 어디로 갔을까요? Web Components 스펙이 흡수했어요.

패턴: 좋은 추상화는 표준이 된다

시기
개발자가 처리
플랫폼이 흡수
2010
Float 레이아웃
→ Flexbox/Grid
2012
jQuery 애니메이션
→ CSS Transitions/Animations
2015
XHR 상태 관리
→ fetch + React Query
2018
JS 폼 검증
→ HTML5 Validation
2020
jQuery 플러그인
→ Web Components
패턴이 보이나요?
1.
개발자들이 반복적으로 비슷한 코드를 작성함
2.
라이브러리가 그 패턴을 추상화함
3.
플랫폼(브라우저, 언어)이 그 추상화를 표준으로 흡수함
좋은 추상화는 결국 표준이 돼요. Float 핵은 Flexbox로, jQuery 애니메이션은 CSS로, XHR 보일러플레이트는 fetch로.

플랫폼 진화의 3가지 방향

1. 성능 민감한 부분을 네이티브로 - requestAnimationFrame - 프레임 동기화 - ResizeObserver - 크기 변화 감지 - PerformanceObserver - 성능 측정 - IntersectionObserver - 교차 영역 감지
2. 자주 쓰이는 패턴을 표준으로 - fetch API - HTTP 요청 - FormData - 폼 데이터 직렬화 - URLSearchParams - 쿼리 스트링 파싱 - AbortController - 요청 취소
3. 접근성을 기본으로 - <dialog> element - 모달 접근성 자동 - <details>/<summary> - 아코디언 접근성 자동 - ARIA 속성들 - 스크린 리더 지원
핵심: 개발자가 반복해서 만들던 것들이 플랫폼으로 내려가요. React의 개념들도 점차 표준으로: - Virtual DOM → Incremental DOM proposals - JSX → Template literals - Hooks → TC39 Signals proposal
jQuery가 $querySelector가 된 것처럼, 좋은 추상화는 플랫폼이 흡수해요.

실무에 적용하기

이 패턴을 알면 뭐가 좋을까요?

1. 트렌드를 읽을 수 있어요

“이 라이브러리가 하는 일이 뭐지?” → 나중에 플랫폼이 흡수할 가능성이 있어요.
React Server Components → 서버/클라이언트 경계 추상화
Tailwind → 유틸리티 클래스 패턴 → CSS에 흡수될까?

2. 미래를 대비할 수 있어요

플랫폼이 흡수할 패턴을 미리 알면, 마이그레이션이 쉬워요.
jQuery → 네이티브 JS로 마이그레이션 쉬웠던 이유: 같은 What을 표현했기 때문
Float 레이아웃 → Flexbox 마이그레이션 쉬웠던 이유: 레이아웃이라는 What이 같았기 때문

3. 추상화 수준을 선택할 수 있어요

플랫폼 네이티브: 의존성 없음, 하지만 기능 제한
라이브러리: 편의성 높음, 하지만 의존성
직접 구현: 완전한 제어, 하지만 복잡도
상황에 따라 적절한 수준을 선택해요.

4. 시스템을 설계할 수 있어요

지금까지 배운 What/How 분리, 사실은 책임 경계 설정이에요.
What: “뭘 하고 싶은가” (의도)
How: “누가 어떻게 할 것인가” (구현 주체)
이 질문을 계속하면, 자연스럽게 시스템 설계 역량이 생겨요.

예시 1: API 책임 이동

프론트엔드에서 여러 API를 조합하고 변환하는 코드가 있다고 해볼게요.
// Before: 프론트엔드가 조합 + 변환 const user = await fetch('/api/user'); const posts = await fetch(`/api/posts?userId=${user.id}`); const comments = await Promise.all( posts.map(post => fetch(`/api/comments?postId=${post.id}`)) ); const profileData = transformForProfile(user, posts, comments);
JavaScript
복사
What/How로 보면: - What: “프로필 화면에 필요한 데이터” - How: 프론트엔드가 3개 API 조합 + 변환
질문: “이 How가 여기 있어야 하나?”
// After: 서버(BFF)가 조합 + 변환 const profileData = await fetch('/api/profile');
JavaScript
복사
What: 똑같음 — “프로필 화면에 필요한 데이터”
How: 서버가 조합해서 하나의 엔드포인트로 제공
책임 경계를 재설정한 거예요. 이게 시스템 설계의 핵심이에요.

예시 2: 컴포넌트 라이브러리 설계

특정 프로젝트에 강결합된 버튼 컴포넌트가 있어요.
// Before: 프로젝트에 강결합 function Button({ onClick }) { return ( <button onClick={onClick} className="bg-toss-blue text-white px-4 py-2 rounded" > 확인 </button> ); }
TypeScript
복사
What/How로 보면: - What: 불명확 — 버튼인데 “확인”만 되고, 토스 스타일만 됨 - How: 스타일, 텍스트가 하드코딩됨
What을 먼저 설계하면:
// After: What(인터페이스)을 먼저 정의 interface ButtonProps { children: React.ReactNode; // What: 어떤 내용이든 onClick?: () => void; // What: 클릭 시 동작 variant?: 'primary' | 'secondary' | 'ghost'; // What: 스타일 의도 disabled?: boolean; // What: 비활성 상태 } function Button({ children, onClick, variant = 'primary', disabled }: ButtonProps) { // How: 스타일 구현은 여기서 결정 const styles = getVariantStyles(variant); return <button onClick={onClick} disabled={disabled} className={styles}>{children}</button>; }
TypeScript
복사
What: 버튼이 받을 수 있는 것들 (인터페이스)
How: 스타일 구현, 애니메이션은 내부에서
이게 Wishful Thinking이에요. “이상적인 인터페이스가 뭘까?”부터 시작하는 거죠.

예시 3: 문자열을 구조로 다루기

코드를 변환하는 도구를 만든다고 해볼게요.
// Before: 정규식으로 직접 조작 function addConsoleLog(code) { return code.replace( /function(\w+)\((.*?)\)\{/g, 'function $1($2) {\n console.log("$1 called");' ); }
JavaScript
복사
문제: 문자열 패턴 매칭은 구조를 모르는 채 텍스트만 보는 거예요.
// After: 구조로 변환 → 구조 조작 → 다시 문자열 import { parse, traverse, generate } from '@babel/core'; function addConsoleLog(code) { const ast = parse(code); // 문자열 → 구조 traverse(ast, { FunctionDeclaration(path) { const funcName = path.node.id.name; path.get('body').unshiftContainer('body', createConsoleLog(funcName) // 구조 조작 ); } }); return generate(ast).code; // 구조 → 문자열 }
JavaScript
복사
What/How로 보면: - What: “함수 선언문에 로그를 추가하고 싶다” - How (Before): 정규식으로 텍스트 패턴 매칭 - How (After): AST로 구조 파악 → 구조 조작 → 재생성
문자열을 “값”이 아니라 “구조”로 보는 관점 — 이것도 추상화예요.

핵심: 추상화 연습 = 시스템 설계 연습

추상화에서 하는 질문
시스템 설계에서 하는 질문
“이 코드의 What은?”
“이 모듈의 책임은?”
“How가 여기 있어야 하나?”
“이 로직은 어느 레이어에?”
“What 우선으로 설계하면?”
“인터페이스 먼저 정의하면?”
시스템 설계는 “백엔드를 해봐야 는다”가 아니에요.
What/How를 분리하고, 책임 경계를 설정하는 연습을 반복하면 자연스럽게 갖춰져요.

정리

심화 주제
핵심
언어 초월 추상화
프로토콜/사양이 언어 독립성을 만듦 (JSX, RSC)
플랫폼 흡수
복잡도가 플랫폼으로 이동
진화 패턴
반복 → 라이브러리 → 표준
추상화의 큰 그림:
1.
개인 수준: 함수 추출, 인터페이스 설계 (4장)
2.
팀 수준: 공통 패턴, 커스텀 훅 (5장)
3.
생태계 수준: 라이브러리, 프레임워크, 플랫폼 표준 (6장)
추상화는 코드를 정리하는 것에서 끝나지 않아요. 생태계 전체가 진화하는 방식이에요.

비용은 어디로 이동했나요?

시기
개발자 비용
플랫폼 비용
결과
Float 시대
clearfix 관리
없음
개발자 부담
Flexbox
브라우저에 위임
자동 계산
비용 ↓
XHR
상태 관리
fetch 표준화
비용 ↓
개발자의 비용이 충분히 낮아지면, 새 기술이 자연스럽게 채택돼요. 플랫폼 흡수가 바로 그 과정이에요.
다음 장에서는 이 추상화 능력을 코드 너머, 삶 전반으로 확장해볼게요.