7장. 심화 사례
지금까지는 함수, 컴포넌트 수준의 추상화를 다뤘어요. 이 장에서는 시야를 넓혀서, 추상화가 언어와 플랫폼 수준에서 어떻게 작동하는지 볼게요.
핵심 질문: 언제 직접 통제하고, 언제 플랫폼에 위임할 것인가?
프론트엔드 개발을 하다 보면 우리는 종종 플랫폼(브라우저)에 위임하는 것과 직접 통제권을 갖는 것 사이의 선택에 직면해요. 뛰어난 엔지니어링이란 모든 것을 직접 제어하는 것이 아니라, 언제 플랫폼을 신뢰하고 작업을 위임할 것인가, 그리고 언제 기꺼이 비용을 지불하고 정교한 통제권을 가져올 것인가를 명확히 판단하고 선택하는 능력이에요.
이 장에서 다루는 것:
- 언어 초월 추상화: 같은 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 표준화 | 비용 ↓ |
개발자의 비용이 충분히 낮아지면, 새 기술이 자연스럽게 채택돼요. 플랫폼 흡수가 바로 그 과정이에요.
다음 장에서는 이 추상화 능력을 코드 너머, 삶 전반으로 확장해볼게요.
