1. 왜 코드를 분리해도 복잡도가 줄지 않을까?
"이 복잡한 로직들을 분리하면 좀 나아지지 않을까?"
많은 개발자들이 이렇게 생각합니다. 그리고 실제로 그렇게 합니다. 다음 컴포넌트를 보겠습니다.
function ProductList() {
const [cart, setCart] = useState([]);
// ...
const handleAddToCart = async (product) => {
// ...
};
const handleLike = async (productId) => {
// ...
};
return (
<div>
{products.map(product => (
<div key={product.id} className="card">
<img src={product.imageUrl} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.price}원</p>
<div className="actions">
<button
onClick={() => handleAddToCart(product)}
disabled={loadingProductId === product.id}
>
{loadingProductId === product.id ? '담는중...' : '장바구니'}
</button>
<button
onClick={() => handleLike(product.id)}
disabled={likedProducts.has(product.id)}
>
❤️ {likeCounts[product.id] || 0}
</button>
</div>
</div>
))}
</div>
);
}
JavaScript
복사
장바구니 상태, 로딩 상태, 좋아요 상태, 좋아요 개수가 모두 뒤섞여 있습니다. 이 복잡한 코드를 정리하기 위해 개발자는 UI 부분을 컴포넌트로 분리합니다.
function ProductCard({
product,
onAddToCart,
isAddingToCart,
onLike,
isLiked,
likeCount
}) {
return (
<div className="card">
<img src={product.imageUrl} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.price}원</p>
<div className="actions">
<button onClick={onAddToCart} disabled={isAddingToCart}>
{isAddingToCart ? '담는중...' : '장바구니'}
</button>
<button onClick={onLike} disabled={isLiked}>
❤️ {likeCount}
</button>
</div>
</div>
);
}
JavaScript
복사
언뜻 보면 깔끔해 보입니다. 컴포넌트는 작고, props는 명확하며, UI만 담당합니다.
하지만 정말 그럴까요? ProductCard의 인터페이스를 다시 보겠습니다. 너무 구체적인 사항들이 props에 하드코드 되어 있습니다. 기능이 늘어날 경우 변경에 취약할 수 밖에 없는 설계입니다.
이제 "공유하기" 기능을 추가해야 한다고 상상해보겠습니다. onShare props를 추가할까요? "위시리스트" 기능은요? onAddToWishlist, isInWishlist도 추가해야겠죠. 기능이 하나씩 추가될 때마다 ProductCard의 props는 계속 늘어납니다. 이 컴포넌트는 금방 누더기가 될 것입니다.
더 큰 문제는, 이 컴포넌트를 다른 곳에서 재사용하려고 할 때 드러납니다. "상품 정보만 보여주면 되는데, 왜 장바구니 관련 props를 다 넘겨줘야 하지?" 이 컴포넌트의 인터페이스는 더 이상 "의미 있는 계약"이 아니라 단순한 "데이터 통로"가 되어버렸습니다.
코드는 분리되었습니다. 그러나 복잡도는 사라지지 않았습니다. 단지 숨겨졌을 뿐입니다. 복잡도 감소라는 추상화의 본질적 목표에는 도달하지 못했습니다.
그간 학습자들이 추상화에 관해 보거나 따라했던 것은 결국 “무언가 코드를 분리하는 행위”였습니다. 원리를 이해하지 못한 채 피상적으로 따라 하는 수준에 그쳤고, 추상화에 대한 올바른 멘탈 모델을 형성하는 데 실패한 것입니다.
2. 잘못된 진단, 잘못된 처방
왜 이런 일이 벌어질까요? 단순히 “노력이 부족해서”가 아닙니다.
진단이 잘못되면 처방도 잘못되기 쉽습니다. 개발자들이 좋은 코드가 무엇인지, 그것을 만드는 과정이 무엇인지, 필요한 도구가 무엇인지 충분히 알지 못하는 경우가 많습니다.
첫째, 좋은 코드를 만드는 원칙이 무엇인지 모릅니다.
많은 개발자들이 “코드를 물리적으로 분리하는 것”을 “추상화” 또는 “모듈화”와 동일시합니다.
ProductCard 예시처럼, 겉보기엔 정리된 것 같지만 실상은 원본 데이터와 파생된 값이 뒤섞인 채 props로 쏟아집니다. 이렇게 되면 컴포넌트의 인터페이스는 더 이상 “의미 있는 계약”이 아니라 단순한 “데이터 통로”가 됩니다.
웹 표준의 <form>이나 <dialog>가 왜 그렇게 설계되었는지, 어떤 인터페이스가 예측 가능하고 재사용 가능한지에 대한 감각이 부족합니다. 그래서 단순히 “코드를 쪼개면 낫지 않을까”라는 막연한 기대만으로 분리를 시도합니다.
둘째, 좋은 코드를 만드는 과정을 모릅니다.
설령 좋은 코드의 모습을 어렴풋이 안다 하더라도, 그것을 만드는 과정을 모릅니다.
복잡한 코드 앞에서 어디서부터 손을 대야 할지, 어떤 순서로 정리해야 할지 막막합니다. 일단 분리부터 하고 보자는 식으로 접근하면 불필요한 얽힘만 늘어납니다.
변경의 이유가 같은 것들을 어떻게 식별하고, 불필요한 결합을 어떻게 끊어 내며, 의도를 명확하게 드러내는 구조를 어떻게 만들어 가는지에 대한 구체적인 전략이 없습니다.
“얽힘(Complecting)”을 풀어 내는 방법을 모르면, 복잡도는 단지 다른 곳으로 이동할 뿐입니다.
셋째, 원칙을 구현하는 도구가 부족합니다.
“변경의 단위로 묶어라”, “관심사를 분리하라”는 말은 이해했지만, 실제로 의존성을 어떻게 관리해야 하는지, IoC(Inversion of Control)를 어떻게 적용해야 하는지, SOLID 원칙을 구체적으로 어떻게 코드에 반영하는지 모르는 경우가 많습니다.
좋은 추상화를 만드는 조립 테크닉이 없으면, 결국 또다시 물리적 분리에 머물 수밖에 없습니다.
3. 원칙을 배우고, 과정을 익히고, 도구를 얻기
이 교과서는 이 세 가지 문제를 모두 다룹니다.
첫째, 좋은 코드가 가진 일반적인 원칙을 배웁니다.
이미 검증된 일반해를 학습함으로써, 좋은 추상화가 무엇인지에 대한 감각을 형성합니다. 웹 표준의 Form이나 Modal 같은 인터페이스가 왜 그렇게 설계되었는지 이해하면, 그것이 우리가 새로운 추상화를 만들 때의 기준이 됩니다.
“의미 있는 계약”이란 무엇인지, 예측 가능한 인터페이스란 어떤 것인지가 명확해집니다. 더 이상 “그냥 분리하면 되지 않을까”라는 막연한 기대가 아니라, “이 인터페이스는 어떤 책임을 가져야 하는가”를 질문하게 됩니다.
둘째, 좋은 코드를 만드는 과정을 배웁니다.
복잡한 코드 앞에서 어떻게 문제를 진단하고, 불필요한 얽힘을 풀어 내며, 올바른 구조로 재조립하는지 구체적인 전략을 제공합니다.
현재 코드의 ‘의미 수준’이 모호하고 뒤섞여 있음을 깨닫는 것에서 시작해서, 요구사항을 보고 코드의 모습을 예측하고, 사용처에 가까이 인라인하여 본질을 드러낸 뒤, 안쪽에서부터 바깥쪽으로 상향식으로 구조화하는 전 과정을 보여 줍니다.
변경의 단위는 개발자가 자의적으로 정하는 것이 아니라 비즈니스 요구사항이 정해 준다는 핵심 원칙도 배웁니다.
셋째, 구체적인 도구와 기술을 연습합니다.
단순히 원칙을 아는 것과 그것을 실제 코드로 구현하는 것은 다른 문제입니다. IoC를 통해 의존성을 역전시키고, SOLID 원칙을 적용하여 컴포넌트를 조립하며, 책임을 분리하고 인터페이스를 설계하는 구체적인 조립 테크닉을 익힙니다. 이것이 있어야 원칙이 실제 코드가 됩니다.
4. 이 교과서를 통해 얻을 수 있는 것들
교과서를 읽기 전, 여러분의 모습
코드 리뷰에서 이런 대화가 오갑니다.
"이 컴포넌트 너무 복잡한 것 같아요. 분리해볼까요?"
"어디를 어떻게요?"
"음... 일단 함수로 빼볼까요?"
기획자가 말합니다. "여기에 버튼 하나만 추가해주세요." 여러분은 컴포넌트 파일을 열어 props를 추가하고, 조건문을 추가하고, 여러 사용처를 확인합니다. 간단한 요청이 반나절 작업이 됩니다.
복잡한 코드 앞에서 "뭔가 이상한데"라는 느낌은 있지만, 정확히 무엇이 문제인지 설명할 수 없습니다. "분리하면 나아지지 않을까?"라는 막연한 기대로 함수를 만들고 컴포넌트를 쪼개지만, 복잡도는 사라지지 않고 단지 다른 곳으로 이동했을 뿐입니다.
교과서를 읽은 후, 여러분의 모습
같은 코드 리뷰에서 이렇게 말합니다.
"이 컴포넌트는 단일 책임 원칙을 위반하고 있어요. 상품 표시와 장바구니 로직이 얽혀 있습니다. 제어권을 외부로 넘기면 어떨까요?"
동료가 묻습니다. "제어권을 넘긴다는 게 무슨 뜻이죠?"
여러분은 구체적으로 설명할 수 있게 됩니다.
"ProductCard가 장바구니 버튼을 직접 알지 않게 만드는 겁니다. bottomActions로 슬롯만 제공하고, 무엇을 넣을지는 사용하는 쪽에서 결정하게 하죠. 그러면 새 버튼이 추가되어도 ProductCard는 수정하지 않아도 됩니다."
기획자의 요청에 여러분은 사용처 한 곳만 열어 버튼 컴포넌트를 추가합니다. ProductCard는 건드리지 않습니다. 10분이면 끝납니다.
복잡한 코드를 보면 "여기는 시점 이동이 많네요. 관련된 것들이 멀리 떨어져 있어요"라고 진단합니다. "변경의 단위로 묶어서 인라인하면 얽힘이 보일 거예요"라고 개선 방향을 제시합니다.
이 교과서의 학습 방식
각 챕터는 동일한 구조로 진행됩니다.
•
먼저 구체적인 문제 상황을 살펴봅니다. 컴포넌트에 props가 계속 추가되는 상황, 필터링 로직이 여기저기 흩어진 상황. 여러분이 실무에서 겪는 바로 그 상황입니다.
•
다음으로 여러분이 이미 해봤을만한 일반적인 해결 시도들을 살펴봅니다. config 객체로 묶기, 컴포넌트 분리하기, 조건 분기 추가하기. 이것들이 왜 근본적 해결이 아닌지 확인합니다.
•
원칙을 배웁니다. IoC란 무엇인가, 왜 제어권을 넘기는가, 어떤 문제를 해결하는가. 단순한 정의가 아니라, 실제 코드에서 어떻게 작동하는지 여러 예시를 통해 봅니다.
•
이제 연습합니다. 같은 코드를 보고 초보자는 "children props를 쓰네"라고 하고, 숙련자는 "제어권을 외부로 넘겼네"라고 말합니다. 이 차이를 인식하는 훈련을 합니다. 코드 스멜을 맡는 연습을 합니다.
•
마지막으로 실무에 적용합니다. 스스로의 코드에서 "뭔가 답답한 인터페이스"를 찾아 판단 기록 템플릿을 작성합니다. "요약 / 상황 / 판단 / 근거 / 질문"을 정리하고 코치에게 피드백을 받습니다. 이 과정을 반복하며 판단 능력이 형성됩니다.
5. 추상화를 훈련하기 위한 여정에 초대합니다
추상화는 어렵지 않습니다. 단지 올바른 관점이 필요할 뿐입니다.
"코드를 분리한다"는 막연한 행위 대신, "얽힘을 감지하고 푼다"는 명확한 과정을 배우면 됩니다. "뭔가 이상한데"라는 느낌 대신, "여기는 단일 책임 원칙을 위반하고 있어"라는 정확한 언어를 가지면 됩니다. "일단 해보자"는 시도 대신, 변경 용이성과 인지 부하라는 분명한 기준으로 판단하면 됩니다.
이 교과서는 그 관점, 그 과정, 그 언어, 그 기준을 제공합니다. 여러분은 이미 코드를 작성할 줄 압니다. 이제 그 코드를 더 나은 구조로 만드는 방법을 배울 차례입니다.
이제 여러분이 매일 겪는 그 복잡한 코드가, 어떻게 단순해질 수 있는지 함께 확인해봅시다.
