프론트엔드 추상화 — 언제 만들고, 언제 버려야 하는가

프론트엔드 추상화 — 언제 만들고, 언제 버려야 하는가

SOLID를 프론트엔드 맥락으로 다시 해석하면서, 결국 계속 같은 질문으로 돌아오게 된다.
이건 추상화해야 할까?

  • 훅으로 분리해야 하나?
  • 컴포넌트로 빼야 하나?
  • 공통 유틸로 묶어야 하나?
  • 아니면 그냥 중복을 허용해야 하나?

프론트엔드에서 추상화는 늘 애매하다. 너무 일찍 하면 과해지고, 너무 늦으면 코드가 굳어버린다.
그래서 많은 코드베이스가 “깔끔해 보이지만 바꾸기 어려운 상태”에 머문다.

이 글에서는 추상화를 재사용이나 깔끔함이 아니라, 변화라는 관점에서 다시 정리해보려 한다.

좋은 추상화는 코드를 줄이지 않는다.

변경될 가능성이 있는 축을 코드 바깥으로 밀어낸다.

왜 프론트엔드에서 추상화는 자주 실패하는가

프론트엔드에서 실패한 추상화를 보면 공통점이 있다. 실제로는 한 번만 쓰이는데 미리 일반화되어 있고, 바뀔 일 없는 구조를 과하게 감싸며, 사용하는 쪽보다 정의가 더 복잡하다. 대부분의 경우 추상화의 기준이 잘못 잡혀 있다.

많은 경우 우리는 이렇게 판단한다.
코드가 중복된다 → 추상화해야겠다.
비슷한 UI가 있다 → 공통 컴포넌트로 빼야겠다.
나중에 또 쓸 것 같다 → 미리 만들어두자.

하지만 이 기준은 프론트엔드에서는 잘 작동하지 않는다. 프론트엔드는 요구사항이 자주 바뀌고, 정책이 늦게 정해지며, UI와 데이터의 결합도가 높다. 즉, 중복은 죄가 아닐 수 있지만, 잘못된 추상화는 바로 부채가 된다.

중복 제거는 추상화의 목적이 아니다

추상화에 대해 가장 흔한 오해는 이것이다.
“중복을 제거하면 좋은 설계다”

중복 제거는 결과일 수는 있어도 목적이 되면 안 된다. 중복된 코드 두 줄은 그 자체로는 아무 문제도 없다. 읽기 쉽고, 맥락이 명확하고, 각자 독립적으로 바뀔 수 있다면 그 중복은 오히려 안전하다.

function ActiveUserList({ users }: { users: User[] }) {
  const activeUsers = users.filter((u) => u.isActive);
  return <List items={activeUsers} />;
}
 
function AdminUserList({ users }: { users: User[] }) {
  const adminUsers = users.filter((u) => u.role === "admin");
  return <List items={adminUsers} />;
}

두 컴포넌트에는 분명 중복이 있다. 하지만 이 중복은 위험하지 않다. 각 컴포넌트는 독립적으로 변경될 수 있고, 요구사항이 갈라질 가능성이 높으며, 아직 “같이 바뀔 이유”가 없다. 문제가 되는 건 중복 그 자체가 아니라 같이 바뀌어야 하는 코드가 흩어져 있을 때다.

프론트엔드에서 추상화의 기준은 항상 이 질문으로 돌아와야 한다. 이 코드들은 앞으로도 함께 바뀔까?

좋은 추상화는 “변화의 방향”을 기준으로 한다

프론트엔드에서 추상화가 필요한 지점은 대부분 코드의 양이 많아서가 아니다.

변화의 방향이 불명확하거나, 변화가 자주 일어나는 축이 있을 때 다.

필터, 정렬, 권한처럼 정책이 자주 바뀌는 영역, SSR / CSR / 테스트처럼 실행 환경이 달라지는 영역, 데이터 구조는 바뀌지만 UI는 유지되는 영역이 그렇다. 이런 지점에서는 코드를 그대로 두는 것이 오히려 위험하다. 변화가 시작되는 순간 수정 범위가 예측 불가능해지기 때문이다.

function UserList({
  users,
  filterFn = (u: User[]) => u,
}: {
  users: User[];
  filterFn?: (users: User[]) => User[];
}) {
  return <List items={filterFn(users)} />;
}

여기서 고정된 것은 “리스트를 렌더링한다”는 구조다. 바뀌는 것은 필터 정책이다. 변화 가능성이 높은 축만 바깥으로 밀어냈기 때문에 컴포넌트 자체는 거의 변하지 않는다. 이런 경우에만 추상화는 힘을 가진다.

추상화의 타이밍은 “지금 얼마나 더럽냐”가 아니라 “앞으로 무엇이 바뀔 것 같냐”로 판단해야 한다.

프론트엔드에서 추상화하기 좋은 것들

경험적으로 보면 프론트엔드에서 비교적 안전하게 추상화할 수 있는 대상은 정해져 있다. 정책, 환경 의존 코드, 데이터 변환 로직이다. 이들의 공통점은 UI는 비교적 안정적이지만 로직이나 정책은 계속 바뀐다는 점이다. 이때의 추상화는 재사용을 위한 것이 아니라 변경 비용을 국소화하기 위한 장치다.

프론트엔드에서 추상화하지 말아야 할 것들

단순한 JSX 묶음, 스타일만 다른 컴포넌트, 아직 한 번밖에 쓰이지 않은 로직, 요구사항이 아직 불분명한 영역은 굳이 추상화하지 않는 편이 낫다. 이런 것들을 미리 일반화하면 props가 늘어나고 옵션이 추가되며 문서를 읽지 않으면 쓰기 어려워진다. 그리고 결정적으로 실제로 재사용되지 않는다.

추상화의 최소 단위는 파일이 아니다

프론트엔드에서의 추상화는 항상 파일 단위일 필요가 없다.

function UserList({
  users,
  renderItem,
}: {
  users: User[];
  renderItem: (user: User) => React.ReactNode;
}) {
  return <ul>{users.map(renderItem)}</ul>;
}

바뀌는 부분만 함수로 밀어내는 것만으로도 충분한 추상화가 된다. 프론트엔드에서의 추상화는 클래스 설계가 아니라 경계를 어디에 그을 것인가의 문제다.

언제 추상화를 버려야 하는가

정의를 읽어야만 사용법을 이해할 수 있고, 사용하는 쪽보다 구현이 더 복잡하며, 더 이상 변화하지 않거나 새로운 요구사항을 수용하기 어려워진 추상화는 버려야 한다. 이럴 때 필요한 건 더 많은 옵션이 아니라 과감한 삭제다. 버릴 수 없는 추상화는 좋은 설계가 아니다.

마무리

프론트엔드에서 추상화는 멋있어 보이기 위한 기술이 아니다. 그건 언제나 변화를 가장 싸고 안전하게 받아들이기 위한 선택이어야 한다.

SOLID가 객체 지향의 교리가 아니라 전략이듯, 추상화도 목적이 아니라 수단이다. 중복을 줄이는 것보다 중요한 건 미래의 변경을 예측하고 그 영향을 국소화하는 것이다. 그 기준을 잃지 않는 한, 추상화는 도움이 된다. 그렇지 않다면, 하지 않는 편이 낫다.