본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Frontend

응집도를 높이는 전략

작성 2026. 10. 20.추가
목차7개 항목
출처와 참고

토스의 「Frontend Fundamentals: 코드 품질」 응집도 문서들을 바탕으로 재구성한 노트다. 일반 원칙은 Brandon Gregory의 「명확한 코드 작성법」(웹액츄얼리 번역)을, 응집의 종류는 정보처리기사 소프트웨어 설계 범위를 더했다.

응집도란

응집도(Cohesion)는 수정돼야 할 코드가 항상 같이 수정되는지다. 응집도가 높으면 한 부분을 고쳐도 다른 곳이 몰래 깨지지 않는다. 함께 바뀌어야 할 것이 구조적으로 함께 바뀌게 되어 있기 때문이다.

응집도를 높이려면 보통 공통화·추상화를 해야 하고, 그만큼 가독성이 떨어진다. 함께 안 고치면 오류가 나는 경우에만 응집도를 우선한다(변경하기 쉬운 코드의 네 가지 기준).

전략묶는 대상
함께 수정되는 파일을 같은 디렉토리에파일
매직 넘버 없애기값과 그 값에 기대는 코드
폼의 응집도 생각하기필드와 검증 로직

응집의 종류

모듈 안의 요소들이 왜 한데 모였느냐에 따라 일곱 단계로 나눈다. 위로 갈수록 강하고 좋은 응집이다.

순서종류모인 이유
1 (강함)기능적모든 요소가 하나의 기능을 위해 수행된다
2순차적한 활동의 출력이 다음 활동의 입력이 된다
3교환적(통신적)같은 입력·출력을 쓰지만 서로 다른 기능을 한다
4절차적입출력 연관은 없지만 정해진 순서대로 수행해야 한다
5시간적같은 시점에 처리된다 (예: 초기화)
6논리적비슷한 성격의 처리라서 모았다
7 (약함)우연적서로 관련이 없다

낮은 응집의 신호

  • 여러 함수가 같은 변수·DOM 참조를 인자로 주고받는다. 한 객체·클래스로 묶고 인스턴스 속성으로 둔다.
  • Common, utils처럼 관련 없는 것을 한데 모은 덩어리가 있다. 정리한 것 같지만 우연적 응집이라 찾기 어렵다. 데이터 접근, 특정 UI 요소처럼 관심사별 모듈로 나눈다.
  • 클래스의 속성을 소수의 메서드만 쓰거나, 쓰이지 않는 메서드가 있다.
  • 같은 코드가 반복된다. 비슷한 문장은 함수로, 비슷한 함수는 클래스로 묶는다.

응집도를 높이면 결합도는 대개 낮아진다. 둘이 부딪힐 때 무엇을 먼저 챙길지는 변경하기 쉬운 코드의 네 가지 기준에 있다.

1. 함께 수정되는 파일을 같은 디렉토리에 두기

components/, hooks/, utils/, constants/처럼 모듈 종류로만 나누면 어떤 파일이 어떤 파일을 쓰는지 구조에서 보이지 않는다. 기능을 지울 때 관련 훅·유틸이 남아 죽은 코드가 되고, 프로젝트가 커지면 한 폴더에 파일이 수백 개 쌓인다.

함께 바뀌는 파일끼리 도메인 디렉토리로 모은다.

src
├─ components   ← 프로젝트 전체에서 쓰는 것
├─ hooks
├─ utils
└─ domains
   ├─ Domain1   ← Domain1에서만 쓰는 것
   │  ├─ components
   │  ├─ hooks
   │  └─ utils
   └─ Domain2
      └─ ...
  • 다른 도메인을 참조하는 import(../../../Domain2/hooks/useFoo)가 눈에 띄어 잘못된 의존을 바로 알아챈다. ESLint의 no-restricted-imports나 경계 규칙으로 강제할 수도 있다.
  • 기능을 없앨 때 디렉토리 하나를 통째로 지우면 끝난다.

이 방향을 레이어·슬라이스 규칙으로 체계화한 것이 Feature-Sliced Design이고, 역할 기반에서 기능 중심으로 옮겨 온 흐름은 프론트엔드 폴더 구조의 진화에 있다.

2. 매직 넘버 없애기

매직 넘버(magic number)는 뜻을 밝히지 않고 코드에 직접 쓴 숫자다. HTTP 404, 하루 86400초 같은 값이 그렇다.

async function onLikeClick() {
  await postLike(url);
  await delay(300); // 애니메이션 길이?
  await refetchPostLike();
}

300이 애니메이션 완료를 기다리는 값이라면, 애니메이션을 바꿀 때 이 숫자는 같이 바뀌지 않는다. 그러면 서비스가 조용히 깨진다. 같이 바뀌어야 할 두 값 중 한쪽만 바뀌는 상태가 낮은 응집도다.

const ANIMATION_DELAY_MS = 300;
 
async function onLikeClick() {
  await postLike(url);
  await delay(ANIMATION_DELAY_MS);
  await refetchPostLike();
}

상수에 이름이 생기면 읽는 사람도 의도를 안다(가독성). 더 나아가 애니메이션 정의와 같은 곳에서 상수를 export해 값의 출처를 하나로 두면 한쪽만 바뀔 여지가 사라진다.

3. 폼의 응집도 생각하기

폼은 검증 로직을 어디에 묶을지에 따라 두 가지로 나뉜다.

방식검증이 사는 곳장점약점
필드 단위 응집각 필드의 register(name, { validate })필드가 독립적이다. 한 필드를 고쳐도 다른 필드에 영향이 없고 재사용하기 쉽다폼 전체 흐름이 흩어진다
폼 전체 단위 응집하나의 스키마(Zod 등) + resolver검증이 한곳에 모여 흐름이 보이고 상태가 중앙에서 관리된다필드끼리 묶여 재사용성이 떨어진다
// 폼 전체 단위: react-hook-form + Zod 스키마
import * as z from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
 
const schema = z.object({
  name: z.string().min(1, "이름을 입력해주세요."),
  email: z.email("유효한 이메일 주소를 입력해주세요."),
});
 
export function Form() {
  const { register, formState: { errors }, handleSubmit } = useForm({
    defaultValues: { name: "", email: "" },
    resolver: zodResolver(schema),
  });
 
  return (
    <form onSubmit={handleSubmit(submit)}>
      <input {...register("name")} placeholder="이름" />
      {errors.name && <p>{errors.name.message}</p>}
      <input {...register("email")} placeholder="이메일" />
      {errors.email && <p>{errors.email.message}</p>}
      <button type="submit">제출</button>
    </form>
  );
}

Zod 4부터 이메일 같은 문자열 형식 검증은 z.email()처럼 최상위 함수로 쓰고, z.string().email()은 더 이상 권장되지 않는다.

변경의 단위가 필드인지 폼인지로 고른다.

필드 단위가 맞을 때폼 전체 단위가 맞을 때
필드마다 복잡하거나 비동기인 검증(아이디 중복 확인, 추천 코드 확인)모든 필드가 하나의 기능을 이룬다(결제 정보, 배송 정보)
같은 필드와 검증을 다른 폼에서도 쓴다이전 단계 입력이 다음 단계에 영향을 주는 단계별 폼(회원가입, 설문)
필드끼리 서로 참조한다(비밀번호 확인, 총액 계산)

정리

  • 응집도는 "같이 바뀌어야 할 것이 같이 바뀌게 되어 있는가"다.
  • 파일은 종류가 아니라 함께 바뀌는 단위로 모은다. 지울 때 폴더째 지울 수 있으면 잘 모인 것이다.
  • 의미 있는 숫자는 이름 붙인 상수로, 가능하면 그 값을 정의하는 쪽과 한곳에 둔다.
  • 폼 검증은 변경 단위에 맞춰 필드 단위와 스키마 기반 폼 단위 중 고른다.

관련 노트: 소프트웨어 설계와 공학, 변경하기 쉬운 코드의 네 가지 기준, 결합도를 낮추는 전략, 프론트엔드 폴더 구조의 진화, Feature-Sliced Design, 폼과 제어 컴포넌트

원본 기록2

댓글

GitHub 계정으로 댓글을 남길 수 있어요.