[요약]
추상화는 ‘복잡도’를 낮춰야 한다
추상화에 대한 오해
추상화는 왜 공부하기 어려운가?
전문가의 추상화 전략을 단계별로 나누어 훈련해야 함
우리는 정말 프론트엔드 '엔지니어'일까?
"프론트엔드 개발자들은 추상화에 관심이 없는 것 같아 슬픕니다."
이 글은 이런 저의 개인적인 슬픔에서 시작됐습니다. 우리는 스스로를 '엔지니어'라고 부릅니다. 하지만 '공학'을 한다는 자의식이 우리에게 얼마나 남아있을까요?
왜 우리는 스스로를 공학자(엔지니어)라 부르기 어색해할까요?
아마 '공학'이라고 하면 기계, 건축, 화학처럼 물리 법칙의 제약을 받는 분야를 먼저 떠올리기 때문일 것입니다. 다리를 설계하는 엔지니어는 중력, 재료 강도, 바람 저항이라는 자연의 법칙 안에서 최적의 해법을 찾아야 합니다.
하지만 소프트웨어 공학은 다릅니다. 우리의 주된 적이자 제약은 물리 법칙이 아니라 '복잡성(Complexity)' 그 자체입니다.
물리적 한계는 거의 없지만, 코드를 이해하고 관리해야 하는 인간의 인지 능력에는 명확한 한계가 있습니다. 시스템 설계는 바로 이 인지적 제약 내에서 복잡도를 통제하고, 변경 가능성을 관리하며, 이해 가능한 구조를 만드는 싸움입니다.
우리가 이 글에서 다룰 '얽힘 풀기', '단순함 추구', '경계 설계'는 바로 이 소프트웨어 공학의 본질적인 도전 과제를 해결하기 위한 핵심 공법입니다.
"공학은 현실적인 문제를 풀기 위한 효율적이고 경제적인 해법을 찾아 나서는 경험적이고 과학적인 접근 방식의 응용입니다." - <모던 소프트웨어 엔지니어링>, 31p
우리는 노벨상을 타기 위한 기초 과학자가 아닙니다. 우리는 매일 '복잡성'이라는 적과 싸우며 '소유 비용'을 낮춰야 하는 공학자입니다.
하지만 현실은 어떻습니까? "동작"하는 코드를 만드는 데 급급합니다. 거대한 컴포넌트를 파일로 나누고, 긴 함수를 분리하고, 이걸 '추상화'라고 부릅니다.
아닙니다. 그것은 추상화가 아닙니다. 당신이 하고 있는 것은 그저 '분리'와 '추출'일 뿐입니다.
추출은 코드의 일부를 물리적으로 다른 곳으로 옮기는 행위입니다. 긴 함수를 여러 개로 나누는 것이죠. 코드를 짧게 만들어 '읽기 쉽게' 만들 수는 있지만, 그것만으로는 부족합니다. 때로는 복잡함을 그저 다른 함수 뒤로 '숨기는(hiding)' 것에 지나지 않습니다.
추상화(Abstraction)는 다릅니다. 코드가 '무엇을 하는지(What)', '왜 필요한지(Why)' 그 본질을 파악하여 개념화하는 과정입니다. 추상화는 단순히 '숨기는' 행위가 아니라, "아주 명확한 새로운 의미 수준(a new semantic level)"을 창조하는 것입니다. 좋은 추상화는 코드 베이스를 점차 '단순하게(Simple)' 만듭니다.
추상화의 목표는 '복잡도를 낮추는 것'입니다. 당신의 '분리'는 복잡도를 낮췄습니까? 아니면 그저 복잡함을 다른 파일로 옮겨 숨겼을 뿐입니까?
왜 우리는 길을 잃었나: '정석'의 부재
이 문제의식은 프론트엔드 씬의 구조적인 한계와 맞닿아 있습니다.
백엔드 개발자들에게는 '토비의 스프링'이나 '오브젝트'처럼, 구체적인 기술(Spring)의 맥락 안에서 보편적인 공학 원리(IoC, DI, 역할/책임/협력)를 '한판에 꿰어주는' 정석이 존재합니다. 그들은 공통의 언어로 토론하고 숙련됩니다.
우리에겐 그런 '정석'이 없습니다.
'정석'이 없으니, 우리는 '원리'를 보는 대신 '개념어'만 쇼핑합니다. FSD, 아토믹 패턴, 컴파운드 컴포넌트, 커스텀 훅, 혹은 Next.js, Solid, TDD, 디자인 시스템... 수많은 도구와 패턴을 이력서에 추가하는 데만 골몰합니다.
'개념어 쇼핑'에 골몰하는 이 현상은 학습 과학에서 말하는 '유창성 착각(Fluency Illusion)'의 전형적인 증상입니다. 새로운 도구의 문서를 읽는 것은 '학습이 일어난다'(Performance)는 만족감을 주지만, '앞으로 그걸 할 수 있게 되는'(Learning) 진짜 학습과는 다릅니다. 그게 왜 좋은지, 내가 적용한 코드가 정말 '맞게' 쓴 것인지 확인할 '잣대'가 없기 때문입니다.
더 근본적인 문제는, '정석'과 '숙련'의 부재 속에서 우리는 코드의 복잡성 자체를 제대로 인지하지 못한다는 것입니다. 전문가가 즉각적으로 감지하는 '나쁜 냄새(Bad Smell)', 즉 '얽힘'의 징후를 보지 못합니다. 문제가 문제임을 인식하지 못하니 개선의 필요성조차 느끼지 못하고, 어설프게 코드를 '숨기는'(추출) 행위를 '추상화'라고 착각하며 만족하는 악순환에 빠집니다.
FSD(Feature-Sliced Design)를 배울 때, "entities, features, widget 폴더에 뭐가 들어가야 해요?"라는 '정답'을 외우려 합니다. 하지만 FSD의 진짜 핵심은 그 '폴더 구조'가 아닙니다. 그 구조가 강제하는 "단방향 의존성", 즉 import 경로를 린트로 막아버려서 개발자가 '마구잡이 import'를 하지 못하게 막는 '사고의 가드레일' 그 자체입니다.
'정답(폴더)'을 외우는 것이 아니라, '문제(의존성)'을 생각하게 만드는 것이 핵심인데도 말이죠.
적용해보다가 잘 안되면 다른 도구로 넘어갈 뿐, '왜' 실패했는지 그 뒤에 숨어 있는 원리를 파고들지 않습니다. 그 결과 연차가 쌓여도 수평 확장만 할 뿐, 질적인 개선은 일어나지 않습니다. 노동 생산성은 정체되고, 더 가치 있는 일을 하지 못하며, 업무에 대한 만족도도 떨어집니다.
우리는 '새로운 개념'을 쇼핑할 것이 아니라, Input, Dialog, Form처럼 이미 수십 년간 합의된 '원형(Archetype)'을 학습해야 했습니다. onSelect, onSubmit, isOpen, isLoading처럼 도메인을 넘어서는 보편적 언어를 익혔어야 했습니다.
추상화의 본질: '얽힘(Complecting)'을 풀고 '단순함(Simple)'에 이르는 길
"추상화는 플랫폼 개발자나 백엔드 개발자들이 하는 것 아닌가요?"
이 질문에 답하기 위해, 우리는 공학의 가장 중요한 두 단어, '단순함(Simple)'과 '쉬움(Easy)'을 구분해야 합니다.
•
Easy (쉬운): '친숙한' 것, '가까이 있는' 것입니다. 튜토리얼이 잘 되어 있고, 시작하기 '쉬운' 프레임워크가 여기에 해당합니다. 주관적인 느낌입니다.
•
Simple (단순한): '엮이지 않은' 것, '한 겹'을 의미합니다. 각 부분이 독립적이고 자율적입니다. 객관적인 상태입니다.
'복잡성(Complexity)'의 어원은 'Complect(함께 엮다)'입니다. 추상화란 이 '얽힘'을 푸는 행위입니다. 복잡한 시스템은 여러 요소가 서로 '얽혀(Complected)' 있는 상태입니다. 한 부분을 수정하면 다른 부분이 깨지고, 전체를 알아야만 부분을 이해할 수 있습니다.
우리는 '개념어 쇼핑'을 하며 'Easy(쉬운)' 도구를 전전했지만, 정작 시스템은 점점 더 복잡해졌습니다. 왜일까요? 그 도구들이 'Simple(단순함)'을 보장해주지 않았기 때문입니다.
조금만 더 풀어서 설명해볼까요?
'쉬운(Easy)' 실타래는 금방 엮을 수 있지만, 얽히고 나면 풀기 어렵고 집을 지을 수도 없습니다.
반면, '단순한(Simple)' 레고 블록은 각자 독립적이지만(캡슐화), 정해진 규약(인터페이스)으로 쉽게 결합하여(조합) 거대한 구조물도 만들 수 있습니다.
이것이 바로 모듈성(Modularity)입니다. 좋은 추상화는 코드의 모듈성을 높여, 레고처럼 재사용 가능하고, 교체 가능하며, 변경의 영향을 최소화하는 '단단한 벽돌'을 만듭니다.
추상화의 진짜 본질은 "공통점을 뽑는 것"이 아니라, "더 복잡하고 어려운 것을 만들기 위해 이 '얽힘'을 풀어서 '단순하게(Simple)' 만드는 것"입니다. 세부사항을 감추는 이유는, 그 세부사항이 다른 것과 '얽혀있기' 때문입니다.
그렇다면 이 '얽힘'은 어떻게 풀 수 있을까요?
바로 무엇을 드러내고(What) 무엇을 숨길지(How) 신중하게 '선택'하고 '설계'하는 과정 그 자체입니다. 추상화는 단순히 '숨기는' 기술이 아니라, 시스템의 본질적인 'What'(핵심 개념, 책임, 인터페이스, 새로운 의미 수준)을 정의하고, 그 외의 모든 부가적인 'How'(구현 세부사항, 내부 로직)는 캡슐화하여 감추는 지적인 설계 활동입니다. 이 'What'을 어떻게 선정하고 정의하느냐가 추상화의 질을 결정합니다.
'번역'되지 않은 선배들의 유산
우리는 프론트엔드가 '뚝 떨어져 있는 특수한 섬'이 아니라, 공학의 역사 위에 서 있음을 깨달아야 합니다. 우리에겐 '뿌리'가 있었고, 단지 그 원칙들이 아직 '번역'되지 않았을 뿐입니다.
•
시스템 레벨 (네트워크 통신):
◦
웹 서버와 통신하려면 원래 DNS 조회, TCP 연결 수립 및 관리, 데이터 분할/재조립 등 여러 저수준 네트워크 작업들이 복잡하게 상호작용해야 했습니다. HTTP 프로토콜은 이러한 하위 계층의 복잡한 상호작용(얽힘)을 표준화된 메시지 형식(GET /index.html HTTP/1.1)이라는 '단순한' 인터페이스 뒤로 감추고 격리했습니다.
◦
이 명확한 추상화 레이어 덕분에, 개발자는 더 이상 하위 네트워크 계층의 복잡함에 신경 쓰지 않고 '어떤 자원을 달라(What)'는 요청에만 집중하여 웹 통신을 할 수 있게 되었습니다. 이후 브라우저의 fetch API는 이 HTTP 통신마저 한 번 더 감싸, XMLHttpRequest 시절의 상태 관리 복잡함까지 '흡수'하여 네트워크 요청을 더욱 간결하게 만들었습니다.
•
플랫폼/언어 레벨 (비동기 처리):
◦
콜백(Callback) 방식에서는 개발자가 직접 비동기 작업의 상태(대기, 성공, 실패) 추적, 에러 전파, 중첩된 호출 흐름(콜백 지옥) 제어를 수동으로 관리해야 하는 복잡성이 있었습니다. 이는 비동기 작업의 **'시간 순서'**를 코드의 '고정된 문(Statement) 구조' 안에 얽어매는 결과를 낳았습니다.
◦
Promise와 async/await는 이러한 상태 추적, 에러 처리, 흐름 제어의 복잡성을 언어/플랫폼 수준에서 흡수했습니다. 비동기 작업을 값(Expression)처럼 다루게 하여, 코드 구조는 단순한 선형을 유지하면서 복잡한 런타임 흐름을 제어할 수 있게 만들었습니다. 덕분에 개발자는 마치 동기 코드처럼 비동기 로직의 '성공 경로'에만 집중할 수 있게 되었죠.
/* Before: 콜백 지옥 - 시간 순서가 구조에 얽힘 */
xhr.open('GET', '/api/user');
xhr.onload = function() {
if (xhr.status === 200) {
var user = JSON.parse(xhr.responseText);
xhr2.open('GET', '/api/posts/' + user.id);
xhr2.onload = function() { /* ... 지옥 시작 */ };
xhr2.send();
}
};
xhr.send();
JavaScript
복사
/* After: 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
복사
•
플랫폼 레벨 (레이아웃): float과 clearfix로 레이아웃을 잡아야 했던 시절의 복잡함은 flexbox와 grid가 '흡수'하여, 개발자가 더 높은 의미 수준에서 레이아웃을 선언적으로 제어할 수 있게 만들었습니다.
•
라이브러리/프레임워크 레벨 (UI 렌더링): 브라우저마다 다르고 장황했던 **DOM API 조작의 '얽힘'**은 jQuery가 호환성과 편의성이라는 '단순한' 인터페이스($('.item').show())로 한 차례 풀었습니다. 하지만 jQuery 역시 **명령형 방식(How)**으로 DOM을 직접 조작하다 보니, 애플리케이션 상태와 UI 표현 사이의 동기화라는 새로운 '얽힘'을 만들었습니다. 이후 React/Vue 같은 현대 프레임워크는 상태(State)가 주어지면 UI(View)가 어떠해야 하는지('What') 만을 선언하는 방식으로, 상태와 UI 동기화의 '얽힘'을 풀어냈습니다 (UI = f(State)).
이러한 '추상화의 흡수' 사례들은 우리에게 중요한 사실을 알려줍니다. 프론트엔드 역시 검증된 공학 원칙들이 통용되는 영역이라는 것입니다. 그렇다면 이 강력한 원칙들의 '뿌리', 즉 우리가 '번역'해야 할 선배들의 유산은 무엇일까요?
1.
유닉스의 교훈: 단순한 인터페이스로 조합하라
유닉스가 50년간 살아남은 힘은 "모든 것은 파일이다(Everything is a file)"라는 단순한 추상화에서 나옵니다. 하드웨어 드라이버, 네트워크 소켓, 프로세스 정보까지 모두 open(), read(), write()라는 단일 인터페이스로 통합했습니다.
그리고 이 단순한 '파일'들을 '파이프(|)'로 조합해 더 복잡한 작업을 수행합니다. ls는 파일 목록만, grep은 검색만, wc는 개수만 셉니다. 각자가 '한 가지 일만 잘하는' 단순한 도구이지만, 이 '단단한 벽돌'들이 조합되어 거대한 시스템을 이룹니다.
2.
객체지향의 정수: 역할, 책임, 협력을 설계하라
많은 프론트엔드 개발자가 객체지향을 Class 문법으로 오해하지만, 그 본질은 '역할, 책임, 협력'의 설계입니다.
<객체지향의 사실과 오해>에서는 '앨리스'의 재판을 비유로 들고 있습니다. 판사(역할)는 재판 진행(책임)을 위해 증인 및 배심원과(협력)합니다. 이 원칙은 마이크로서비스 아키텍처, 팀의 조직 구조, 그리고 React 컴포넌트 설계까지 관통합니다.
하지만 우리는 이 '번역'의 순간을 매일 놓치고 있습니다. ‘개념어’만 소비하고 그 뒤의 '본질'인 이 보편 원칙들을 우리 코드에 적용하는 데 실패하고 있습니다. 다음 섹션에서는 이 '번역의 순간'에서 비숙련자와 숙련자가 어떻게 다르게 생각하는지 구체적으로 살펴보겠습니다.
'일상적 추상화'로 변화에 적응하자
하지만 이런 예시를 보다보면 '거대한 추상화'만을 추상화로 착각할 수 있습니다. 나와 거리가 먼 이야기라고 생각하는 것이죠.
아닙니다. async/await나 flexbox 같은 '거대한 추상화'가 우리를 편하게 해준 것처럼, 그 동일한 '얽힘 풀기'와 '단순화' 과정이 프론트엔드 서비스 개발자인 '우리'의 코드에서, '매일', '일상적'으로 일어나야 합니다.
오히려 귀찮고 반복적인 일이 많고 요구사항 변경이 잦은 서비스 개발자에게 이 '일상적 추상화'를 점진적으로 쌓아가는 능력이 더욱 절실하게 필요합니다. 왜일까요?
1.
반복과의 싸움 (점진적 개선의 기회):
우리는 수많은 폼, 리스트, 모달을 반복해서 만듭니다. 처음부터 완벽한 추상화를 만들 필요는 없습니다.
// Bad: 반복을 인지하지 못하고 계속 복붙
// ... ProductCard price logic ...
// ... CartItem price logic ...
// Good: 반복을 인지하고 점진적으로 추상화
// Step 1: 그냥 만듦 -> Step 2: formatPrice 함수 추출 -> Step 3: PriceDisplay 컴포넌트 생성
function PriceDisplay({ price, discount }) { /* ... */ }
TypeScript
복사
PriceDisplay 예시처럼, 처음에는 복사-붙여넣기를 하더라도(Phase 1: 인식), 두 번째 비슷한 코드를 만날 때 공통 로직을 추출하고(Phase 2: 추출), 세 번째에는 **패턴을 발견하여 더 나은 추상화(Phase 3: 패턴화)**로 발전시키는 경제적이고 점진적인 접근이 현실적입니다.
중요한 것은 '반복'을 인지하고 개선하려는 **'의도'**입니다.
2.
변경에 대한 적응 (유연성의 확보):
서비스 요구사항은 수시로 변합니다. "모든 가격에 부가세 포함 표시 추가해주세요"라는 요청이 왔을 때, 이론적으로는 PriceDisplay 같은 추상화 단위가 있다면 변경 지점이 명확해지고 수정 비용이 훨씬 줄어들어야 합니다. 추상화는 완벽한 예측이 아니라, 변경이 발생했을 때 적응하기 더 쉬운 구조를 만들어가는 과정이니까요.
하지만 여기서 당신은 이렇게 반문할지도 모릅니다. "해봤는데, 오히려 추상화했더니 변경에 대응하기 더 힘들었어요!" 네, 충분히 그럴 수 있습니다. 그리고 그 경험 때문에 추상화 자체를 불신하게 되었을 수도 있습니다.
만약 당신의 경험이 그랬다면, 잠시 멈춰서 다음 질문들을 스스로에게 던져볼 필요가 있습니다.
•
경계 설정 실패 (잘못된 '단위'): 혹시 서로 다른 이유로 변경될 책임들을 하나의 단위로 잘못 묶었거나 (낮은 응집도), 너무 많은 책임을 부여하여 (SRP 위반) 단위 자체의 경계가 부적절하지 않았나요?
•
인터페이스 설계 실패 (잘못된 '연결'): 단위 자체는 괜찮았을지라도, 외부와 소통하는 **'계약'(인터페이스)**이 너무 구체적이거나(구현 누수), 많은 것을 요구하거나(Fat Interface), 외부의 세부사항에 깊이 의존하여(강한 결합) 연결 방식이 잘못되지 않았나요?
•
추상화 시점/수준 실패 (잘못된 '시도'): 혹시 표면적 유사성만 보고 본질적으로 다른 것들을 성급하게 일반화했거나(섣부른 일반화), 패턴이 명확해지기도 전에 너무 일찍 추상화를 시도하여(시기상조 추상화) 오히려 유연성을 해치지는 않았나요?
추상화는 강력한 도구지만, 잘못 사용하면 오히려 코드를 더 복잡하고 변경하기 어렵게 만들 수 있습니다. 당신의 경험이 힘들었다면, 그것은 추상화 자체가 틀렸다는 증거가 아니라, **당시 당신이 내렸던 설계 결정이나 추상화의 '수준'과 '방식'이 문제 상황에 적합하지 않았다는 귀중한 '데이터'**일 수 있습니다.
핵심은 완벽한 예측이 아니라, 실패를 통해 배우고 점진적으로 더 나은 추상화, 즉 변경에 더 잘 '적응'할 수 있는 구조를 만들어가는 '능력'을 기르는 것입니다. 이 과정 자체가 바로 우리가 추구하는 '숙련'입니다.
3.
초기 압박 부재 vs. 지속적 개선:
플랫폼 개발자는 처음부터 범용성을 고려해야 하지만, 서비스 개발자는 당장의 기능 구현에 집중할 수 있습니다. 이것이 함정이 될 수도 있지만, 기회이기도 합니다.
처음부터 거창한 추상화를 시도하기보다, 실제 코드를 통해 배우고, 반복과 변경 속에서 '경제적인' 판단을 내려 점진적으로 추상화 수준을 높여가는 것이 더 실용적입니다. 다만, 이러한 개선 기회를 지속적으로 놓치게 되면, 수정 비용이 점차 증가하고 기술 부채가 쌓여 결국 'Easy'했던 시작이 나중에는 감당하기 어려운 복잡성으로 돌아옵니다.
추상화는 **'언젠가 한 번 하는 것'이 아니라, '지속적으로 관심을 가지고 개선해나가는 활동'**입니다.
하지만 우리는 이 **점진적인 개선의 '순간'**을 매일 놓치고 있습니다. '개념어'만 소비하고 그 뒤의 '본질'인 이 보편 원칙들을 우리 코드에 적용하고, 필요한 시점에 적절한 수준으로 추상화하는 데 실패하고 있습니다.
다음은 우리가 흔히 놓치는 순간에 대한 예시 두 가지입니다.
순간 1: Prop을 넘길 때
•
비숙련자의 생각: "컴포넌트에 데이터를 넘긴다."
•
숙련자의 생각: "이 컴포넌트의 의존성을 주입한다."
이 작은 관점의 차이가 모든 것을 바꿉니다. '데이터를 넘긴다'고 생각하면, useContext는 그저 "prop drilling을 피하게 해주는 편리한 도구"가 됩니다. 그래서 어디서든 useAuth(), useCart()를 호출해 전역 상태에 거미줄처럼 의존하는 괴물 컴포넌트를 만듭니다.
하지만 '의존성을 주입한다'고 생각하면, useContext는 '암묵적 의존성'을 만드는 수많은 선택지 중 하나일 뿐입니다. props로 user={user}를 넘기는 것은 '명시적 의존성'을 선택하는 '설계적 결단'이 됩니다. 이 결단 덕분에 컴포넌트는 재사용 가능해지고 테스트하기 쉬워집니다.
숙련자는 의존성을 명시적으로 주입함으로써 컴포넌트의 **캡슐화(Encapsulation)**를 강화합니다. 컴포넌트는 자신이 알아야 할 최소한의 정보(props)만 알고, 나머지는 외부에 위임합니다. 이로 인해 컴포넌트 내부의 **응집도(Cohesion)**는 높아지고 외부와의 결합도는 낮아집니다. useQuery가 useState와 useEffect를 사용하는 방식보다 응집도가 높은 이유와 같습니다.
순간 2: Prop 이름을 지을 때
•
비숙련자의 생각: "이벤트 핸들러 이름을 짓는다."
•
숙련자의 생각: "컴포넌트의 '계약'을 정의한다."
"이름을 짓는다"고 생각하면, onProductSelect처럼 '지금 당장' 구현해야 할 구체적인 이름을 짓게 됩니다.
하지만 '계약(Contract)'을 정의한다고 생각하면, 이 컴포넌트의 **'본질'**이 무엇인지 고민하게 됩니다. 이 컴포넌트의 본질은 'Product를 선택'하는 것일까요, 아니면 그저 '무언가(id)를 선택했다는 사실을 외부에 알리는 것'일까요? 만약 후자라면, 우리는 onSelect라는 '일반적인' 이름을 선택할 것입니다.
이 onSelect라는 이름이 바로 '추상화 벽'입니다. 이 벽 덕분에 부모 컴포넌트는 자식이 'Product'를 다루는지 'User'를 다루는지 알 필요가 없어집니다. onProductSelect라고 짓는 순간, 이 벽은 무너지고 부모와 자식은 'Product'라는 구체적인 구현에 강하게 결합됩니다.
숙련자는 일반적인 인터페이스(onSelect)를 사용함으로써 캡슐화의 경계를 명확히 합니다. 컴포넌트 내부는 '무엇을 선택하는지'라는 구체적인 구현을 숨기고, 외부는 '선택이 일어났다'는 추상적인 사실만 알게 됩니다. 이는 컴포넌트가 **하나의 명확한 책임(응집도)**에 집중하게 만듭니다.
우리가 익혀야 할 것은 'DI', 'IoC', '추상화 벽' 같은 단어들이 아닙니다. 그 단어들이 탄생할 수밖에 없었던 **"이 책임은 누구의 것인가?", "이 컴포넌트의 진짜 본질은 무엇인가?"**라고 질문하는 '사고방식' 그 자체입니다.
이 '번역'에 성공했을 때, 우리는 비로소 '변경'을 만났을 때 확연히 다른 경험을 하게 됩니다. 시간이 흘러 이전에 만들었던 '단단한 벽돌'이 재사용되고, 인지 부하를 줄여주며, 점진적으로 진화하는 경이로운 순간을 마주하게 됩니다.
결국 추상화란, 이 '단단한 벽돌'을 만들기 위해 **'의미의 경계'**를 긋는 사고방식입니다. 코드를 나누는 것이 아니라, **'책임'**을 나누는 것입니다.
추상화 공부의 '이상한 점': 왜 '번역'이 실패하는가?
추상화를 이야기한다는 건, 방금 말했듯, 결국 '복잡도'를 잘 다루고 싶다는 것입니다.
하지만 우리가 이 '뿌리'(유닉스, OOP)를 공부하려 할 때, 이상한 지점과 마주칩니다. '번역'이 실패하는 것이죠.
•
공감이 안 됩니다: 예시를 찾아보면 대부분 class와 상속을 활용한 객체지향 예제입니다.
•
가짜 공감에 빠집니다: 예를 들면, 상속 기반 패턴을 배운 뒤 이를 React 함수형 컴포넌트에 억지로 적용하기 위해 복잡한 로직으로 가득 찬 커스텀 훅을 만듭니다. 하지만 대체로 React의 강점인 **합성(Composition)**을 활용하는 더 단순하고 유연한 방법은 놓친 채, **오히려 코드를 더 얽히게 만든 '잘못된 번역'**일 뿐일 가능성이 높습니다.
•
적용하기 어렵습니다: 고전적인 디자인 패턴은 우리가 매일 마주하는 실무(e.g., 상태 관리, UI 렌더링)와 거리가 있어 보입니다.
결국 우리가 '공감'하지 못하고 '적용'하기 어려웠던 이유는, 우리가 마주한 '프론트엔드의 특수한 복잡도'와 맞지 않는, '변경에 취약한' 잘못된 추상화 방식을 억지로 끼워 맞추려 했기 때문입니다.
그렇다면 우리가 해결해야 할 '복잡도의 종류'는 무엇이 다를까요?
전통적인 예제들이 주로 '계산'의 복잡도를 다룬다면, 프론트엔드의 '복잡도'는 **'동기화'**의 복잡도입니다. 우리의 고약한 특수성은 다음과 같습니다.
1. 시간과 상태의 얽힘
백엔드가 **"무엇을 계산할 것인가"**가 복잡하다면, 프론트엔드는 **"무엇과 무엇을 일치시킬 것인가"**가 복잡합니다.
서버 상태, 로컬 상태, URL, UI, 사용자 입력... '시간'의 흐름에 따라 변하는 이 모든 '상태'들이 서로 얽혀 있습니다.
2. 구현과 의존성의 얽힘
Context API는 이 얽힘을 푸는 도구였지만, '암묵적 의존성'이라는 또 다른 '얽힘'을 만들었습니다.
function CheckoutButton() {
// 컴포넌트 '구현'이 '전역 상태'와 강력하게 얽혀있습니다.
const auth = useAuth();
const cart = useCart();
// ...
}
JavaScript
복사
이 코드는 CheckoutButton이 '결제 버튼 표시'라는 자신의 핵심 책임 외에, **'인증/장바구니 상태 접근 및 관련 로직 실행'**이라는 다른 레이어의 책임까지 암묵적으로 떠안게 만듭니다.
이것이 왜 문제일까요?
•
강한 결합: CheckoutButton은 이제 AuthContext와 CartContext의 존재뿐 아니라 그 내부 구현 없이는 작동할 수 없습니다. Context 구현이 변경되면 버튼 컴포넌트도 영향을 받습니다.
•
재사용성 파괴: 이 버튼은 해당 Context들이 없는 다른 환경(다른 프로젝트, Storybook 등)에서는 전혀 재사용할 수 없습니다.
•
테스트 어려움: 버튼 하나를 테스트하기 위해 AuthProvider, CartProvider 등 전체 의존성 환경을 구축하고 모킹해야 합니다. 테스트 비용이 급증합니다.
•
숨겨진 복잡성: useContext 호출 뒤에 숨겨진 의존성 때문에, 컴포넌트의 실제 복잡성과 필요한 '계약'이 인터페이스(props)만 봐서는 전혀 드러나지 않습니다. 이는 추상화가 아니라 복잡도를 숨긴 것에 가깝습니다.
결국, '쉬워 보이는' Context 남용은 컴포넌트의 독립성을 해치고 시스템 전체를 더 얽히게(Complected) 만들어 변경과 테스트를 어렵게 만듭니다. 이것이 '구현과 의존성의 얽힘'의 대표적인 모습입니다. (이 얽힘을 푸는 방법은 이후 챕터에서 다룹니다.)
3. 제어권의 혼돈 (누구에게 모달 열기에 대한 책임을 줄 것인가?)
백엔드가 'DI 컨테이너'를 통해 명시적으로 제어권을 관리하는 반면, 프론트엔드는 제어권이 혼돈 속에 흩어져 있습니다.
"모달 열기"라는 단순한 기능조차 우리는 5가지 이상의 방식으로 구현합니다.
•
useState → 컴포넌트 자체적으로 제어
•
부모가 내리는 isOpen prop → 부모가 제어
•
Redux/Jotai → 전역 상태
•
?modal=open 같은 URL 쿼리스트링 → 라우터가 제어
한 프로젝트 안에 이 모든 방식이 혼재할 때, 데이터 흐름은 추적 불가능한 카오스가 됩니다.
이 특수한 복잡도 위에서 '보편 원칙'을 적용하는 '번역기'가 없었기에, 우리의 '분리'는 실패했습니다.
'분리'는 했지만 '계산' 로직이 '액션' 로직에 전염되어 버린 fetchData(setData, navigate) 예제처럼, 혹은 useContext를 남용해 재사용과 테스트가 불가능해진 CheckoutButton 예제처럼, 우리는 잘못된 추상화를 양산해왔습니다.
'숙련'을 위한 선언
이것이 우리가 이 '프로젝트'를 시작하는 이유입니다.
우리의 목적은 프론트엔드 맥락 안에서 이 '보편 원칙'을 '번역'하고 '체화'하는 **'학습 구조'**를 제공하는 것입니다.
이 '번역'에 필요한 것은 거창한 이론이 아닙니다. 우리에게 실패를 안겨준 '공감 안 되는 예시'가 아니라, 충분한 상황 설명, 맥락에 맞는 예시 코드, 명확한 적용 절차, 그리고 변경에 대응해보는 '의도적 수련'의 경험입니다.
이 프로젝트는 당신이 다음과 같은 **'사고의 전환'**을 이루도록 도울 것입니다:
•
'분리'를 넘어 '추상'으로: 단순히 코드를 나누는 것을 넘어, 코드의 **'본질(What)'**과 **'책임'**을 파악하고 **'의미의 경계'**를 긋는 법을 배웁니다. 코드를 '해독'하는 것이 아니라 **'독서'**할 수 있는 눈을 기릅니다. 
•
'공통점 찾기'를 넘어 '본질 설계'로: '현재의 공통점'에서 출발하는 변경에 취약한 설계를 버리고, 구현해야 하는 대상의 **'본질'**을 먼저 파악하여 **'원형(Archetype)'**과 **'보편적 언어'**에 기반한 **'예측 가능한 인터페이스'**를 설계하는 법을 익힙니다. 
•
'얽힘 수용'을 넘어 '얽힘 풀기'로: 프론트엔드 특유의 '얽힘'(시간/상태, 구현/의존성, 제어/뷰)을 당연하게 받아들이는 대신, '계산'과 '액션'을 분리하고, **'명시적 의존성'**을 통해 **'캡슐화'와 '응집도'**를 높여 이 '얽힘'을 적극적으로 푸는 기술을 훈련합니다. 
•
'도구 의존'을 넘어 '원칙 기반'으로: useContext를 쓸지 props를 쓸지 고민하기 전에, **"이 책임은 누구의 것인가?", "이 둘은 얽혀있는가?"**를 먼저 질문하는 습관을 들입니다. QueryString 예제처럼, '도구' 너머의 **'원칙'**을 보고 문제 해결의 '강한 방법'을 구축하는 여정을 경험합니다. 
이 프로젝트의 목표는, 당신이 이 과정을 마쳤을 때, 어제 짠 코드를 어색하게 느끼고 '단순하고(Simple) 얽히지 않은' 단단한 벽돌을 쌓아 올릴 수 있는 **'스타터 킷'**을 쥐여주는 것입니다.
복잡성이라는 피할 수 없는 제약 속에서, 명확한 의미 수준을 창조하고 단순함을 추구하며 시스템을 제어하는 것. 이것이 바로 소프트웨어 '공학자'의 길입니다. 엔지니어로서의 당신을 기대합니다. 
그래서 이제 무엇을 어떻게 해야 한다가 필요하지 않나?



