////////
Search

추상화 흡수로 복잡도 낮춘 사례

"언제 통제하고, 언제 위임할 것인가"
프론트엔드 개발을 하다 보면 우리는 종종 플랫폼(브라우저)에 위임하는 것과 직접 통제권을 갖는 것 사이의 선택에 직면합니다. 해당 글은 이 흥미로운 줄다리기에 대해 다시 한번 생각할 거리를 던져줍니다.
우리가 JavaScript 라이브러리를 사용하거나 직접 로직을 구현하며 통제권을 확보하려 할 때, 그것은 브라우저가 기본으로 제공하는 기능만으로는 달성할 수 없는, 더 섬세하고 차별화된 사용자 경험을 목표로 하기 때문일 것입니다. 이 통제권은 때로 우리 제품의 핵심 경쟁력이 되기도 합니다.
하지만 이 글이 우리에게 상기시키는 중요한 점은, 우리가 그 통제권을 선택할 때 지불해야 하는 비용을 의식적으로 인지하고 있는가 하는 질문입니다.
가장 좋은 예시가 바로 Scroll Snap(https://lnkd.in/g46aiWfN) 입니다. 이 기능이 이제 Baseline widely available이 된 지금, CSS 몇 줄로 요구사항을 만족시킬 수 있다면, 우리는 기꺼이 네이티브의 성능과 안정성을 선택해야 합니다. 하지만 브라우저의 차이나 요구사항을 충족하지 않는다면, 다른 라이브러리를 채택해야 할 것입니다.
결국 뛰어난 엔지니어링이란 모든 것을 직접 제어하는 것이 아니라, 언제 플랫폼을 신뢰하고 작업을 위임할 것인가, 혹은 플랫폼이 목적에 맞게 동작하는가, 그리고 언제 기꺼이 비용을 지불하고 정교한 통제권을 가져올 것인가를 명확히 판단하고 선택하는 능력에 있는 것 같습니다.
‘브라우저에서 실행되는 코드’를 작성할까? vs ‘브라우저를 호출’하여 실행하는 코드를 작성할까?

플랫폼 진화가 만드는 추상화

정확한 관찰입니다. 웹 플랫폼 자체가 점점 더 많은 복잡도를 흡수하면서, 개발자가 직접 다뤄야 했던 것들이 표준 API로 제공되고 있죠.

1. 레이아웃 복잡도의 흡수

Before (Float 시대)
// Clearfix 핵들... .clearfix::after { content: ""; display: table; clear: both; } // JavaScript로 높이 계산 function equalizeHeights(elements) { const maxHeight = Math.max(...elements.map(el => el.offsetHeight)); elements.forEach(el => el.style.height = maxHeight + 'px'); }
JavaScript
복사
After (Flexbox/Grid)
/* 플랫폼이 복잡도 흡수 */ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); align-items: stretch;
CSS
복사

2. 애니메이션 복잡도의 흡수

Before (jQuery 시대)
// 수동 애니메이션 구현 function animate(element, from, to, duration) { const start = performance.now(); 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); } } requestAnimationFrame(frame); }
JavaScript
복사
After (Web Animations API)
element.animate([ { transform: 'translateX(0)' }, { transform: 'translateX(100px)' } ], { duration: 1000, easing: 'ease-in-out' });
JavaScript
복사

3. 상태 관리의 표준화

Before
// 수동 폼 유효성 검사 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; } }
JavaScript
복사
After (HTML5 Validation)
<!-- 플랫폼이 제공하는 유효성 검사 --> <input type="email" required /> <input type="url" pattern="https://.*" /> <input type="number" min="0" max="100" />
HTML
복사

4. 비동기 처리의 진화

콜백 지옥 시대
xhr.open('GET', '/api/user'); xhr.onload = function() { if (xhr.status === 200) { const user = JSON.parse(xhr.responseText); xhr2.open('GET', `/api/posts/${user.id}`); xhr2.onload = function() { // 지옥... }; } };
JavaScript
복사
Promise → Async/Await
// 언어 레벨에서 복잡도 흡수 const user = await fetch('/api/user').then(r => r.json()); const posts = await fetch(`/api/posts/${user.id}`).then(r => r.json());
JavaScript
복사

5. 컴포넌트화의 표준화

Before (jQuery 플러그인)
// 수동 컴포넌트 시스템 $.fn.myComponent = function(options) { return this.each(function() { // 복잡한 초기화 로직 }); };
JavaScript
복사
After (Web Components)
// 플랫폼 레벨 컴포넌트 class MyElement extends HTMLElement { connectedCallback() { this.attachShadow({ mode: 'open' }); } } customElements.define('my-element', MyElement);
JavaScript
복사

6. 스크롤 관련 복잡도

Before
// 스크롤 위치 수동 계산 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
복사
After (Intersection Observer)
// 플랫폼이 최적화된 관찰 제공 const observer = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { loadMore(); } }); observer.observe(sentinel);
JavaScript
복사

플랫폼 진화의 패턴

1.
성능 민감한 부분을 네이티브로
requestAnimationFrame
ResizeObserver
PerformanceObserver
2.
자주 쓰이는 패턴을 표준으로
fetch API
FormData
URLSearchParams
3.
접근성을 기본으로
dialog element
details/summary
ARIA 속성들

이것이 시사하는 바

"좋은 추상화는 결국 표준이 된다"
React의 개념들도 점차 표준으로:
Virtual DOM → Incremental DOM proposals
JSX → Template literals
Hooks → TC39 Signals proposal
프론트엔드 개발자가 만든 추상화가 좋으면 플랫폼이 흡수합니다. jQuery가 $querySelector가 된 것처럼.
이는 추상화가 일시적 해결책이 아니라 진화의 방향임을 보여줍니다.

추상화의 층위: HTTP를 해부하면

Layer 0: 원시 소켓 통신

// 1990년대 초, 직접 소켓 프로그래밍 int sockfd = socket(AF_INET, SOCK_STREAM, 0); connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); // 수동으로 HTTP 문자열 구성 char *message = "GET /index.html HTTP/1.0\r\nHost: example.com\r\n\r\n"; send(sockfd, message, strlen(message), 0); // 응답을 바이트 단위로 읽기 char buffer[1024]; recv(sockfd, buffer, 1024, 0); // "HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n<html>..."
C
복사

Layer 1: HTTP 프로토콜 표준화

[HTTP 스펙 RFC 2616] Request = Request-Line *(Header CRLF) CRLF [Body] 이 "문자열 규약"이 모든 언어를 연결하는 다리가 됨
Plain Text
복사

Layer 2: 언어별 HTTP 라이브러리

# Python response = requests.get('http://example.com') # Ruby response = HTTParty.get('http://example.com') # JavaScript (Node.js) const response = await axios.get('http://example.com')
Python
복사
모두 같은 HTTP 프로토콜을 구현했지만, 각 언어의 관례를 따름

Layer 3: 브라우저의 Fetch API

// 브라우저가 HTTP 복잡도를 완전히 흡수 const response = await fetch('/api/users'); const data = await response.json(); // 뒤에서 일어나는 일: // 1. DNS 조회 // 2. TCP 핸드셰이크 // 3. TLS 협상 (HTTPS의 경우) // 4. HTTP 요청 포맷팅 // 5. 청크 인코딩 처리 // 6. 압축/해제 (gzip) // 7. 쿠키 자동 첨부 // 8. CORS 검증 // 9. 리다이렉트 추적
JavaScript
복사

Layer 4: 프레임워크 레벨 추상화

// React Query가 또 한 번 추상화 const { data, isLoading, error } = useQuery({ queryKey: ['users'], queryFn: fetchUsers }); // 뒤에서 처리되는 것들: // - 캐싱 // - 백그라운드 리페칭 // - 중복 요청 제거 // - 낙관적 업데이트 // - 에러 재시도
JavaScript
복사

추상화가 숨긴 복잡도의 실체

CSS Grid가 숨긴 것

// Before: 수동 그리드 계산 function createGrid(container, items, columns) { const containerWidth = container.offsetWidth; const gutterSize = 20; const itemWidth = (containerWidth - (gutterSize * (columns - 1))) / columns; items.forEach((item, index) => { const row = Math.floor(index / columns); const col = index % columns; item.style.position = 'absolute'; item.style.width = itemWidth + 'px'; item.style.left = (itemWidth + gutterSize) * col + 'px'; item.style.top = (item.offsetHeight + gutterSize) * row + 'px'; }); } // After: CSS Grid .container { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
JavaScript
복사

React가 숨긴 DOM 조작

// Before: 수동 DOM diffing function updateList(oldItems, newItems) { const patches = []; // 1. 삭제된 아이템 찾기 oldItems.forEach((old, i) => { if (!newItems.find(n => n.id === old.id)) { patches.push({ type: 'REMOVE', index: i }); } }); // 2. 추가된 아이템 찾기 newItems.forEach((newItem, i) => { if (!oldItems.find(o => o.id === newItem.id)) { patches.push({ type: 'ADD', index: i, item: newItem }); } }); // 3. 변경된 아이템 찾기 // ... 더 복잡한 로직 // 4. 패치 적용 applyPatches(patches); } // After: React function List({ items }) { return items.map(item => <Item key={item.id} {...item} />); }
JavaScript
복사

추상화의 계층 구조

Application Code (우리가 쓰는 코드) ↓ Framework (React, Vue) ↓ Libraries (Lodash, Axios) ↓ Web APIs (Fetch, WebSocket) ↓ JavaScript Engine (V8) ↓ Browser Engine (Blink) ↓ Operating System APIs ↓ Network Protocols (TCP/IP) ↓ Hardware (네트워크 카드, CPU)
Plain Text
복사
각 층은 아래 층의 복잡도를 완전히 감춰서, 우리는 최상위 층만 다루면 됩니다.

핵심: 인터페이스가 만드는 경계

// fetch는 "인터페이스" // 구현이 XMLHttpRequest든, Node.js의 http 모듈이든 상관없음 const response = await fetch(url); // React Component도 "인터페이스" // 내부가 클래스든 함수든, Fiber든 뭐든 상관없음 <Button onClick={handler} /> // CSS Grid도 "인터페이스" // 브라우저가 어떻게 레이아웃을 계산하든 상관없음 display: grid;
JavaScript
복사
좋은 추상화의 특징:
1.
복잡한 구현을 단순한 인터페이스로 감춤
2.
구현이 바뀌어도 인터페이스는 유지
3.
사용자는 "어떻게"가 아닌 "무엇"만 신경쓰면 됨
이게 바로 HTTP가 30년 넘게 살아남은 이유이고, React가 클래스에서 함수로 바뀌어도 JSX는 그대로인 이유입니다.