본문으로 건너뛰기
Tech Blog

화면을 닮은 모양으로 코드 두기

글 복사 완료!

화면 한 점을 짚는데 코드는 네 층을 내려가야 할 때, 어디서 어긋난 건지 짚어요

·8분·

디자인 검토 자리에서 "이 카드 오른쪽 위에 작은 배지 하나만 추가해주세요" 한 줄을 들었어요. 화면을 보고 있을 땐 답이 너무 명확했죠. "아, 저기." 그런데 에디터를 열면 갑자기 막막해지더라고요. ProductSection 안에 들어가서 ItemList 를 거쳐 ItemRow 를 찾고, 그 안의 ItemMeta 까지 내려가야 그 자리가 나오거든요. 화면에서 한 점을 짚는 데 코드 네 층을 내려가야 한다면, 그 코드는 화면과 다른 모양으로 누워 있는 거예요.

화면을 보고 어디부터 손대지

지난 글 에서 한 프론트엔드 가이드의 "변경하기 쉬운 코드" 4가지 기준 중 가독성을 살펴봤어요. 한 자리에 머물면서 위에서 아래로 읽히는 코드. 이번엔 같은 가이드의 다른 기준, 응집도 쪽으로 한 칸 더 들어가 보려고 해요.

응집도가 가독성과 다른 점은, 읽는 흐름이 아니라 변경의 흐름 을 본다는 거예요. 위에서 본 ItemMeta 사례는 읽기는 멀쩡할 수 있어요. 함수 이름도 알기 쉽고 들여쓰기도 깔끔하다고 쳐도, 디자이너가 "여기 한 점만" 이라고 말한 자리에 도달하려면 네 층을 내려가야 하잖아요. 변경의 출발점이 화면이라면, 코드의 출발점도 그 화면 한 점에서 시작돼야 자연스러워요.

컴포넌트는 화면을 따라간다

React 공식 문서 Thinking in React 의 첫 단계는 "UI 를 컴포넌트 계층으로 쪼개기" 예요.

"a component should ideally only be concerned with one thing. If it ends up growing, it should be decomposed into smaller subcomponents." - React 공식 문서

한 컴포넌트는 이상적으로 한 가지에만 관여하고, 커지면 더 작은 컴포넌트로 쪼개라는 뜻이에요.

이 문서는 컴포넌트를 쪼갤 때 세 가지 시선을 권해요. 프로그래밍의 관심사 분리, CSS 의 셀렉터 단위, 디자인의 레이어 정리. 이 셋이 가리키는 곳이 대체로 일치하면, 거기가 자연스러운 컴포넌트 경계예요. 디자이너가 카드라고 부르는 자리와 데이터 모델에서 한 항목으로 떨어지는 자리가 같은 이름으로 만나면, 그 이름이 곧 컴포넌트 이름이 되거든요.

손에 잡히는 모양으로 보면 이런 식이에요.

function ProductCard({ product }) {
  return (
    <Card>
      <CardBadge>{product.tag}</CardBadge>
      <CardImage src={product.image} />
      <CardMeta name={product.name} price={product.price} />
    </Card>
  );
}

디자인 검토 자리에서 "카드 오른쪽 위 배지" 라고 들었으면 에디터에서 CardBadge 만 열면 끝이에요. 도입에서 봤던 ItemMeta 까지 네 층을 내려가던 코드와 비교하면, 화면 위에서 짚은 자리가 코드에서 같은 이름으로 떨어져 있다는 게 어떤 의미인지 보이실 거예요.

함께 바뀌는 것을 같이

그런데 "화면 단위로 쪼개라" 만으로는 부족해요. 가끔은 화면상으로 같이 보여도 코드는 따로 있어야 하고, 화면에서는 떨어져 있어도 코드는 같이 있어야 할 때가 있거든요. 그럴 때 기준이 되는 게 변화의 축 이에요.

이 기준은 단일 책임 원칙(SRP) 의 원저자 Robert C. Martin 이 정확히 짚어줬어요.

"Gather together the things that change for the same reasons. Separate those things that change for different reasons." - Robert C. Martin

같은 이유로 바뀌는 것들은 같이 모으고, 다른 이유로 바뀌는 것들은 따로 두라는 뜻이에요. 단일 책임 원칙의 진짜 정의는 "한 가지 일만 한다" 가 아니라 "한 가지 이유로만 바뀐다" 였거든요.

이 가이드의 응집도 정의도 같은 결이에요.

"수정되어야 할 코드가 항상 같이 수정되는지"

응집도가 높다는 건, 한 가지를 바꾸려고 했을 때 함께 바꿔야 할 것들이 한 자리에 모여 있다는 뜻이에요.

재사용은 함정이 될 수 있다

여기서 한 단계 더 비틀어 볼게요. "재사용 가능한 컴포넌트로 추출하라" 는 조언은 익숙해요. 그런데 이 조언만 따라가면 응집도가 오히려 무너질 때가 있어요.

같은 가이드는 useOpenMaintenanceBottomSheet 라는 훅의 사례를 들어요. 점검 안내 시트를 여는 동작을 여러 페이지에서 공유하려고 훅 하나로 묶었어요. 처음엔 깔끔해 보여요. 그런데 페이지마다 요구사항이 조금씩 달라져요. 어느 페이지는 시트를 띄울 때 로깅을 추가해야 하고, 다른 페이지는 시트가 닫힌 뒤 별도 라우팅을 타야 해요. 디자인이 살짝 다른 페이지도 생기죠. 공용 훅은 점점 if 분기로 가득 차고, 한 페이지를 고치려다 다른 페이지가 깨지는 일이 따라와요.

코드로 비교해 보면 그림이 또렷해져요.

function useOpenMaintenanceBottomSheet(pageType) {
  return () => {
    if (pageType === "checkout") logEvent("checkout_open");
    openBottomSheet({
      onClose: () => {
        if (pageType === "profile") router.push("/home");
        else if (pageType === "checkout") router.back();
      },
    });
  };
}
 
function openCheckoutMaintenance() {
  logEvent("checkout_open");
  openBottomSheet({ onClose: () => router.back() });
}

위 훅을 두 페이지가 같이 쓴다면 한 페이지가 새 요구사항을 들고 올 때마다 분기가 한 줄씩 늘어나요. 아래처럼 페이지마다 작은 함수를 두면 길이는 비슷해도, 한 곳을 고칠 때 다른 곳이 흔들리지 않거든요.

"페이지마다 동작이 달라질 여지가 있다면, 공통화 없이 중복 코드를 허용하는 것이 더 좋은 선택이에요." - Frontend Fundamentals

겉모습이 같다고 묶지 말고, 같이 변하는지 를 묻고 묶으라는 뜻이에요.

여기서 헷갈리지 말아야 할 게 있어요. "재사용하지 마라" 가 아니에요. 같이 변할 게 확실해진 후에만 묶으라 는 거예요. 같이 변할지 아직 모르는 단계에서 미리 묶으면, 나중에 따로 빼내는 비용이 처음에 묶지 않은 비용보다 훨씬 커지거든요. 재사용은 출발점이 아니라 후행 결과예요.

한 걸음 더

1대1 매핑은 컴포넌트 안에서만 적용되는 게 아니에요. 디렉토리 구조도 같은 원리로 정리할 수 있어요. 흔히 components/, hooks/, utils/ 처럼 타입별로 폴더를 나누지만, 기능 하나를 삭제할 때 흩어진 파일을 일일이 추적해야 한다는 단점이 있어요. 반대로 domains/Profile/, domains/Cart/ 처럼 도메인별로 묶으면, 기능이 사라질 때 폴더 하나만 들어내면 끝이에요.

CSS 쪽으로 가도 비슷한 결의 이야기가 있어요. BEM 방법론 의 Block 은 화면에서 한 단위로 인지되는 영역에 그대로 이름을 붙이는 방식이에요. 이름이 곧 화면 단위가 되고, 그 이름을 따라가면 같이 바뀌는 코드가 모여 있어요. 컴포넌트, 디렉토리, 클래스 이름. 표면이 다를 뿐 결국 같은 원리를 다른 층위에서 적용하는 거예요.

가독성 다음은 응집도, 그다음은 결합도. 한 자리에서 한 흐름을 다 읽어낼 수 있는 코드와, 한 자리에서 한 변경을 다 끝낼 수 있는 코드. 이 두 가지가 만나는 지점에서 변경하기 쉬운 코드가 비로소 나와요. 한 자리를 고쳤을 때 다른 곳이 같이 흔들리지 않는 결합도 이야기는, 다른 자리에서 한 번 더 풀어볼게요.

참고 자료

관련 글