본문으로 건너뛰기
2023. 8. 12·약 11분

응집도와 결합도로 살펴보는 프론트엔드 코드 설계

세 점씩 모인 두 모듈이 점선 하나로만 닿는 그림

이 글에서 다루는 내용​

응집도는 모듈 안의 코드가 얼마나 관련되어 있는지, 결합도는 모듈 사이의 의존이 얼마나 강한지를 설명합니다. 두 개념의 분류와 React 예제를 살펴보고, 관련 코드를 모으면서 변경의 영향을 줄이는 방법을 정리합니다.




서론: 소프트웨어 구조를 결정하는 두 가지 원칙​

1-1. 변경 범위를 줄이는 구조​

처음에는 기능을 구현하는 것만으로 충분해 보여도 요구사항이 늘면 기존 코드를 계속 고쳐야 합니다. 이때 유지보수성은 어디를 고쳐야 하는지 찾기 쉽고 변경이 예상 밖의 곳으로 번지지 않는가로 드러납니다. 확장성은 새 기능을 더할 때 기존 구조를 얼마나 적게 흔드는가에 가깝습니다.

React 애플리케이션에서는 다음 장면에서 이 차이가 보입니다.

  • React 컴포넌트 설계
    한 컴포넌트에서 데이터 요청과 상태 관리, UI 렌더링을 모두 처리하면 한 기능을 바꿀 때 다른 기능까지 살펴야 합니다.

  • 상태 관리(State Management)
    전역 상태를 넓게 공유하면 많은 컴포넌트가 같은 값과 갱신 방식에 묶입니다. 값이 필요한 범위에 상태를 두면 의존 관계와 렌더링 범위를 줄일 수 있습니다.

  • API 요청 처리
    같은 API 호출 규칙이 여러 곳에 흩어져 있으면 서버 계약이 바뀔 때 수정할 코드도 늘어납니다. 공통 요청과 화면별 가공이 서로 다른 이유로 바뀐다면 그 경계를 나눌 수 있습니다.

응집도(Cohesion)는 관련된 코드를 모으는 기준이고, 결합도(Coupling)는 그 묶음들이 서로 얼마나 강하게 기대는지를 보는 기준입니다.


1-2. 응집도와 결합도​

응집도(Cohesion): 코드 내부의 논리적 연결성​

응집도는 한 모듈이나 컴포넌트 안의 기능이 얼마나 같은 목적을 향하는지를 나타냅니다. 함께 바뀌는 코드가 모여 있다면 응집도가 높습니다.

  • 응집도가 높은 코드

    • 하나의 컴포넌트 또는 모듈이 단일한 책임을 가짐
    • 코드의 논리적 흐름이 일관되고, 가독성이 높음
    • 관련 기능의 변경을 한곳에서 처리하기 쉬움
  • 응집도가 낮은 코드

    • 하나의 모듈이 여러 가지 역할을 담당함
    • 변경이 필요할 때 관련 없는 코드까지 수정해야 할 가능성이 높음
    • 코드의 목적이 불분명하고, 재사용성이 낮음

결합도(Coupling): 모듈 간의 의존성​

결합도는 한 모듈이 다른 모듈의 구조나 동작을 얼마나 많이 알아야 하는지를 나타냅니다. 한쪽의 내부 변경 때문에 다른 쪽도 함께 고쳐야 한다면 결합도가 높습니다.

  • 결합도가 높은 코드

    • 하나의 모듈이 다른 모듈의 내부 구현에 직접적으로 의존함
    • 특정 기능을 변경하면, 여러 곳에서 수정이 필요함
    • 테스트와 디버깅이 어려워짐
  • 결합도가 낮은 코드

    • 모듈 간의 의존성이 최소화됨
    • 한 모듈의 내부 변경이 다른 모듈로 번질 가능성이 낮음
    • 유지보수성과 확장성이 높아짐

응집도와 결합도의 균형이 중요한 이유​

관련된 코드를 모으고 모듈 사이에는 필요한 정보만 주고받는 것이 기본 방향입니다. 그러나 모듈을 너무 잘게 나누면 작은 기능 하나를 이해하려고 여러 파일을 따라가야 하고 호출 관계도 늘어납니다. 다음 기준으로 분리의 이득과 비용을 함께 봅니다.

  • 모듈이 하나의 명확한 책임을 가지도록 유지 (응집도 높이기)
  • 필요한 데이터만 주고받도록 인터페이스를 명확하게 정의 (결합도 낮추기)
  • 의존성 주입이나 이벤트 같은 방식을 목적에 맞게 적용
  • 비즈니스 로직과 UI 로직을 분리하여 독립성 유지



응집도의 개념과 단계​

2-1. 응집도의 정의와 높은 응집도가 갖는 장점​

응집도가 높은 모듈은 같은 이유로 바뀌는 코드를 모아 한 가지 목적을 드러냅니다. 관련 없는 기능이 한데 섞이면 이름으로 역할을 설명하기 어렵고 변경 범위도 넓어집니다.

높은 응집도를 유지하면 다음과 같은 장점이 있습니다.

  • 역할 파악: 모듈 이름과 내부 코드가 같은 목적을 가리킵니다.
  • 변경 범위: 한 기능과 함께 바뀌는 코드가 한곳에 모입니다.
  • 테스트 범위: 모듈의 입력과 결과가 좁아져 무엇을 검증할지 분명해집니다.
  • 재사용 여부: 독립된 기능은 필요한 곳에서 다시 쓸 수 있습니다. 다만 재사용할 이유가 없는 코드를 억지로 분리할 필요는 없습니다.

2-2. 응집도의 단계​

전통적인 분류에서는 응집도를 다음 단계로 나눕니다.

단계응집도 수준특징
우연적 응집 (Coincidental Cohesion)낮음 🔴기능들이 서로 관련 없이 묶여 있음. 유지보수 어려움.
논리적 응집 (Logical Cohesion)낮음 🔴비슷한 유형의 기능이지만 서로 직접적인 연관이 없음.
시간적 응집 (Temporal Cohesion)낮음~보통 🟡서로 무관한 로직들이 '같은 시점에 실행된다'는 이유로 묶임.
절차적 응집 (Procedural Cohesion)보통 🟡특정 절차(순서)를 따르는 기능들이 함께 존재.
통신적 응집 (Communicational Cohesion)보통 🟡같은 데이터를 사용하거나 같은 입력을 처리하는 기능들이 포함됨.
순차적 응집 (Sequential Cohesion)높음 🟢한 기능의 출력이 다음 기능의 입력으로 사용됨. 자연스러운 흐름이 존재.
기능적 응집 (Functional Cohesion)높음 🟢모듈이 단일 책임을 가지며, 불필요한 기능이 없음. 가장 이상적인 형태.

  1. 우연적 응집 (Coincidental Cohesion)

    • 모듈 내부의 기능들이 아무런 연관 없이 묶여 있는 상태입니다.
    • 예제: utils.js 파일에 문자열 처리 함수, 날짜 포맷팅 함수, 배열 변환 함수 등 서로 관련 없는 기능이 함께 정의된 경우.
  2. 논리적 응집 (Logical Cohesion)

    • 동일 범주의 작업들이 한 모듈에 묶여 있고, 타입이나 플래그 인자로 실제 수행할 기능을 분기하는 상태입니다.
    • 예제: handleInput(type, ...) 하나에 키보드·마우스·터치 등 모든 입력 처리 루틴을 모아 두고, type 인자로 분기하여 실행할 기능을 고르는 경우.
  3. 시간적 응집 (Temporal Cohesion)

    • 서로 무관한 로직들이 '같은 시점에 실행된다'는 이유만으로 한 모듈에 묶인 상태입니다.
    • 예제: 앱 초기화 함수 init()에 로깅 설정, 캐시 초기화, 환경 변수 로드처럼 서로 무관한 설정 로직이 '앱 시작 시점에 실행된다'는 이유로 모여 있는 경우.
  4. 절차적 응집 (Procedural Cohesion)

    • 특정 절차를 따르는 여러 기능이 한 모듈에 포함된 상태입니다.
    • 예제: 하나의 함수에서 여러 단계의 처리를 연속적으로 수행하지만, 각 단계가 서로 독립적인 경우.
  5. 통신적 응집 (Communicational Cohesion)

    • 같은 데이터를 사용하거나, 같은 입력을 처리하는 기능들이 한 모듈에 포함된 상태입니다.
    • 예제: 동일한 데이터 객체를 읽고 수정하는 여러 메서드가 한 클래스에 포함된 경우.
  6. 순차적 응집 (Sequential Cohesion)

    • 한 기능의 출력이 다음 기능의 입력으로 사용되는 경우입니다.
    • 예제: API 데이터를 받아 변환하고, 변환된 데이터를 화면에 렌더링하는 과정이 하나의 모듈에서 처리되는 경우.
  7. 기능적 응집 (Functional Cohesion)

    • 모듈이 하나의 명확한 기능만을 수행하며, 불필요한 요소가 포함되지 않은 상태입니다.
    • 예제: React 컴포넌트가 한 가지 역할만 수행하며, 불필요한 상태나 로직이 포함되지 않은 경우.

분류는 모듈을 평가하는 어휘로 쓰면 됩니다. 실제 코드에서는 기능적 응집을 향하되, 순차적으로 함께 처리해야 하거나 같은 데이터를 다루는 코드까지 무조건 흩어놓지는 않습니다.

// ❌ Bad: 하나의 컴포넌트에서 너무 많은 역할을 담당
function Dashboard() {
const [user, setUser] = useState<User | null>(null);
const [notifications, setNotifications] = useState<Notification[]>([]);

useEffect(() => {
fetch("/api/user")
.then((res) => res.json())
.then(setUser);
fetch("/api/notifications")
.then((res) => res.json())
.then(setNotifications);
}, []);

return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {user?.name}</p>
<ul>
{notifications.map((n) => (
<li key={n.id}>{n.message}</li>
))}
</ul>
</div>
);
}
// ✅ Good: 데이터 처리 로직을 별도 훅으로 분리하여 응집도를 높임
function useUserData() {
const [user, setUser] = useState<User | null>(null);

useEffect(() => {
fetch("/api/user")
.then((res) => res.json())
.then(setUser);
}, []);

return user;
}

function useNotifications() {
const [notifications, setNotifications] = useState<Notification[]>([]);

useEffect(() => {
fetch("/api/notifications")
.then((res) => res.json())
.then(setNotifications);
}, []);

return notifications;
}

function Dashboard() {
const user = useUserData();
const notifications = useNotifications();

return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {user?.name}</p>
<NotificationList notifications={notifications} />
</div>
);
}

function NotificationList({
notifications,
}: {
notifications: Notification[];
}) {
return (
<ul>
{notifications.map((n) => (
<li key={n.id}>{n.message}</li>
))}
</ul>
);
}

2-3. 프론트엔드에서 응집도를 높이는 방법​

프론트엔드에서는 다음 기준으로 관련 코드를 모을 수 있습니다.

  • 컴포넌트 단위를 명확히 정의하기

    • 데이터 요청, 상태 관리, UI 렌더링이 서로 다른 이유로 바뀐다면 컴포넌트와 훅으로 나눕니다.
  • 하나의 모듈이 단일 책임을 가지도록 구성하기

    • Single Responsibility Principle(SRP)은 모듈의 변경 이유를 하나로 모으는 원칙입니다. 파일이나 함수의 크기보다, 같은 이유로 바뀌는 코드인지 살펴봅니다.
    • 예를 들어 API 호출과 화면별 데이터 가공이 서로 다른 이유로 바뀐다면 분리할 수 있습니다. 늘 함께 바뀌는 코드라면 한 모듈에 두는 편이 나을 수도 있습니다.
  • 상태 관리 로직을 분리하기

    • Redux의 slice나 React의 useReducer처럼 함께 바뀌는 상태와 갱신 로직을 묶습니다.
  • 재사용 가능한 유틸리티 함수와 헬퍼 함수 정의하기

    • 여러 곳에서 쓰는 로직은 목적에 맞는 모듈로 옮깁니다. 관련 없는 함수를 utils 하나에 모으면 파일만 분리됐을 뿐 응집도는 낮습니다.



결합도의 개념과 단계​

3-1. 결합도의 정의와 낮은 결합도가 갖는 장점​

결합도가 높으면 한 모듈의 내부 변경이 다른 모듈의 수정으로 이어집니다. 결합도가 낮은 모듈은 공개한 계약만 주고받으므로 내부 구현을 따로 바꾸기 쉽습니다.

낮은 결합도를 유지하면 다음과 같은 장점이 있습니다.

  • 변경 영향: 한 모듈의 내부 구현을 바꿔도 소비자는 그대로 둘 수 있습니다.
  • 테스트 대역: 계약이 좁으면 필요한 의존성만 바꿔 끼울 수 있습니다.
  • 작업 분리: 공개 인터페이스에 합의한 뒤 모듈별로 작업할 수 있습니다.

3-2. 결합도의 단계​

전통적인 결합도 분류를 강한 쪽부터 놓으면 다음과 같습니다.

단계결합도 수준특징
내용 결합 (Content Coupling)높음 🔴한 모듈이 다른 모듈의 내부 데이터를 직접 수정. 강한 의존성.
공통 결합 (Common Coupling)높음 🔴여러 모듈이 전역 변수를 공유하여 강하게 연결됨. 수정이 어렵고 예측 불가능한 오류 발생 가능.
외부 결합 (External Coupling)보통 🟡여러 모듈이 외부에서 정한 포맷·프로토콜·인터페이스에 의존하는 상태.
제어 결합 (Control Coupling)보통 🟡한 모듈이 다른 모듈의 실행 흐름을 직접 결정. 플래그 값을 넘겨 제어하는 방식.
스탬프 결합 (Stamp Coupling)낮음 🟢모듈 간 데이터 구조를 공유하지만 일부만 사용. 데이터 의존성이 있지만 부분적으로 독립적.
자료 결합 (Data Coupling)낮음 🟢모듈 간 필요한 데이터만 전달하며 불필요한 의존성이 최소화됨. 가장 이상적인 결합도.

  1. 내용 결합 (Content Coupling)

    • 한 모듈이 직접 다른 모듈의 내부 데이터를 수정하거나, 내부 로직을 강하게 참조하는 경우입니다.
    • 예제: 다른 모듈이 export한 내부 변수를 직접 재할당·변경하거나, 다른 컴포넌트의 ref를 통해 내부 DOM·상태를 직접 조작하는 경우.
// ❌ Bad: 부모가 자식의 ref를 통해 내부 DOM을 직접 조작 (내용 결합)
function Parent() {
const inputRef = useRef<HTMLInputElement>(null);

const clearSearch = () => {
// 자식이 관리해야 할 내부 DOM의 값과 스타일을 부모가 직접 바꿔버린다
if (inputRef.current) {
inputRef.current.value = "";
inputRef.current.style.borderColor = "red";
}
};

return (
<div>
<SearchInput inputRef={inputRef} />
<button onClick={clearSearch}>Clear</button>
</div>
);
}

function SearchInput({
inputRef,
}: {
inputRef: React.RefObject<HTMLInputElement>;
}) {
// 내부 구현(input DOM)이 그대로 바깥에 노출된다
return <input ref={inputRef} placeholder="검색어 입력" />;
}
// ✅ Good: 자식은 공식 인터페이스(props)만 노출하고, 부모는 그 인터페이스로만 소통
function Parent() {
const [keyword, setKeyword] = useState("");

return (
<div>
<SearchInput value={keyword} onChange={setKeyword} />
<button onClick={() => setKeyword("")}>Clear</button>
</div>
);
}

function SearchInput({
value,
onChange,
}: {
value: string;
onChange: (value: string) => void;
}) {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="검색어 입력"
/>
);
}
  1. 공통 결합 (Common Coupling)

    • 여러 모듈이 전역 변수를 공유하는 경우입니다.
    • 예제: 여러 모듈이 동일한 전역 변수를 직접 읽거나 수정할 때 발생하는 결합도입니다. 예를 들어, 여러 모듈이 하나의 전역 mutable 객체(예: window 객체의 속성)를 직접 읽고 쓰거나, 광범위한 스토어 상태에 여러 컴포넌트가 무분별하게 의존하는 경우가 이에 해당합니다.
  2. 외부 결합 (External Coupling)

    • 여러 모듈이 외부에서 정한 데이터 포맷, 통신 프로토콜, 장치 인터페이스 등에 함께 의존하는 경우입니다.
    • 예제: 여러 모듈이 외부 기관이 정한 파일 포맷을 직접 읽어, 그 포맷이 바뀌면 함께 수정해야 하는 경우.
  3. 제어 결합 (Control Coupling)

    • 한 모듈이 다른 모듈의 실행 흐름을 직접 결정하는 경우입니다.
    • 예제: 함수가 매개변수로 플래그 값을 받아 분기문을 통해 다른 기능을 수행하는 경우.
  4. 스탬프 결합 (Stamp Coupling)

    • 모듈 간 데이터 구조를 공유하지만, 그중 일부 데이터만 사용하는 경우입니다.
    • 예제: 하나의 객체를 여러 모듈이 전달받지만, 일부 필드만 사용하는 경우.
  5. 자료 결합 (Data Coupling)

    • 모듈 간 필요한 데이터만 전달하며, 불필요한 의존성을 최소화한 상태입니다.
    • 예제: 함수가 필요한 인자만 전달받아 동작하는 경우.

내용 결합과 공통 결합은 변경의 영향을 넓히므로 피합니다. 가능하면 상대 모듈의 내부 구조 대신 필요한 데이터와 공개 인터페이스에만 의존합니다.


3-3. 프론트엔드에서 결합도를 낮추는 방법​

프론트엔드에서는 다음 방법으로 의존 범위를 줄일 수 있습니다.

  • 전역 상태 관리 최소화

    • 여러 컴포넌트가 공유할 필요 없는 값은 내부 상태(useState)나 가까운 부모의 props에 둡니다.
  • 의존성 주입(Dependency Injection) 사용

    • 컴포넌트가 특정 API 모듈을 직접 만들지 않고 요청 함수를 props나 함수 인자로 받으면 테스트에서 대체할 수 있습니다.
  • 컴포넌트 간 직접 참조 줄이기

    • 여러 단계에 걸친 props 전달이 반복되면 먼저 컴포넌트 합성을 검토하고, 트리 전체가 함께 써야 하는 값에는 Context나 상태 관리 라이브러리를 사용할 수 있습니다.
    • Context를 사용해도 공유 데이터에 대한 의존은 남습니다. Provider의 value가 바뀌면 그 Context를 읽는 소비자가 다시 렌더링되므로, 변경 주기가 다른 값은 Context를 나누거나 소비 범위를 좁힙니다.
  • 비즈니스 로직과 UI 로직 분리

    • API 계약과 화면 표현이 서로 다른 이유로 바뀐다면 요청·가공 로직을 훅이나 모듈로 나눕니다.



응집도와 결합도의 균형 잡기​

4-1. 높은 응집도와 낮은 결합도를 조화롭게 유지하는 전략​

응집도와 결합도는 파일 수로 결정되지 않습니다. 관련 코드를 너무 잘게 나누면 호출과 타입을 통해 서로 더 많이 의존할 수 있고, 한 기능을 이해하려고 여러 파일을 오가야 합니다. 다음 질문으로 경계를 확인합니다.

  • 모듈의 역할을 명확히 정의하기

    • 이 모듈 안의 코드는 같은 이유로 바뀌는가?
  • 모듈 간 인터페이스를 명확히 하기

    • 소비자가 내부 구현이 아니라 공개한 입력과 출력에만 의존하는가?
  • 데이터 흐름을 단방향으로 유지하기

    • 상태가 어디에서 바뀌고 어느 방향으로 전달되는지 추적할 수 있는가?
// ✅ 상태와 변경 경로를 Context와 useReducer에 모음
const CounterContext = createContext<
{ state: number; dispatch: React.Dispatch<Action> } | undefined
>(undefined);

type Action = { type: "increment" } | { type: "decrement" };

function counterReducer(state: number, action: Action): number {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
default:
return state;
}
}

function CounterProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(counterReducer, 0);

return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
}

function useCounter() {
const context = useContext(CounterContext);
if (!context) {
throw new Error("useCounter must be used within a CounterProvider");
}
return context;
}

function Counter() {
const { state, dispatch } = useCounter();

return (
<div>
<p>Count: {state}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</div>
);
}

function App() {
return (
<CounterProvider>
<Counter />
</CounterProvider>
);
}

4-2. 실제 아키텍처 설계에서의 적용​

프론트엔드에서는 모듈화(Modularization), 의존성 주입(Dependency Injection), 이벤트 기반 구조(Event-Driven Architecture) 로 경계를 만들 수 있습니다. 어느 방식이든 새로운 계약과 추적 비용이 생기므로 문제의 크기에 맞춰 사용합니다.


1) 모듈화 (Modularization)​

기능별 모듈화는 함께 바뀌는 코드를 모으고 다른 기능과의 접점을 드러냅니다.

  • UI 컴포넌트 분리:

    • React에서는 기능이 명확히 구분된 컴포넌트를 만들고, 한 컴포넌트가 너무 많은 역할을 하지 않도록 분리합니다.
    • 예제: 버튼, 카드, 리스트 등 재사용 가능한 UI 요소를 따로 모듈화.
  • 비즈니스 로직과 UI 로직 분리:

    • 데이터 처리 로직을 별도의 파일이나 훅(Hook)으로 분리하면, UI 코드와의 결합도를 낮출 수 있습니다.
    • 예제: API 호출을 담당하는 useFetch 훅을 따로 만들고, 이를 여러 컴포넌트에서 재사용.

2) 의존성 주입 (Dependency Injection)​

의존성 주입은 한 모듈이 구체 구현을 직접 만들지 않고 외부에서 필요한 기능을 받는 방식입니다. 소비자는 정해진 계약에만 의존하고 테스트에서는 구현을 바꿔 끼울 수 있습니다.

  • API 요청 모듈을 주입하여 컴포넌트 독립성 유지

    • API 요청 로직을 컴포넌트 내부에서 직접 호출하지 않고 주입받아 사용하도록 분리하면, 요청 방식을 바꾸거나 테스트용 구현으로 대체하기 쉬워집니다.
  • 의존성을 함수나 props로 전달하여 결합도 낮추기

    • React에서는 props를 활용하여 특정 기능을 주입받아 사용할 수 있습니다.
    • 예제: 부모 컴포넌트에서 자식 컴포넌트로 핸들러 함수를 전달하여 직접적인 의존성을 줄임.

3) 이벤트 기반 구조 (Event-Driven Architecture)​

이벤트 기반 아키텍처에서는 모듈이 상대 구현을 직접 참조하는 대신 이벤트로 통신합니다. 이벤트 이름과 payload 형식, 전달 순서에 대한 결합은 남으며, 실행 흐름을 추적하는 비용도 고려해야 합니다.

  • 이벤트 버스(Event Bus) 사용

    • 컴포넌트 간 데이터 공유가 필요할 때, 직접 참조하는 대신 이벤트를 활용하여 처리할 수 있습니다.
    • 예제: EventEmitter로 독립적인 모듈 사이에 이벤트를 전달할 수 있습니다. 컴포넌트가 구독했다면 해제 시점도 함께 관리해야 합니다.
  • Pub-Sub 패턴 적용

    • 특정 모듈에서 발생한 이벤트를 여러 개의 다른 모듈이 구독(Subscribe)하여 사용할 수 있도록 하면 결합도를 줄일 수 있습니다.
    • 예제: Redux에서는 dispatch로 상태 변경 요청을 전달하고, 컴포넌트는 selector로 필요한 상태를 구독합니다. 이는 일반 이벤트 버스와 달리 중앙 상태의 변경 경로를 관리하는 구조입니다.




결론: 변경 이유와 의존 범위로 판단하기​

5-1. 코드의 복잡성을 줄이면서 유지보수성을 높이는 방법​

실무에서는 모듈의 크기보다 무엇이 함께 바뀌고 어디까지 영향을 주는지를 봅니다.

  • 기능 단위로 모듈을 구성하여 응집도를 높이기

    • API 요청, 데이터 가공, UI 렌더링이 서로 다른 이유로 바뀌는지 확인하고 경계를 정합니다.
  • 불필요한 의존성을 줄이고 결합도를 낮추기

    • 공유할 필요가 없는 값은 전역 상태에 두지 않습니다. AuthContext와 ThemeContext처럼 변경 이유가 다른 상태는 분리할 수 있습니다.
  • 코드 중복을 줄이고 재사용성을 높이기

    • 여러 컴포넌트에서 같은 이유로 반복되는 로직은 유틸리티 함수나 커스텀 훅으로 모읍니다.
  • 단순한 해결책을 우선 적용하기

    • 지역 상태로 충분하다면 useState를 쓰고, 실제 의존 문제가 생길 때 더 큰 구조를 도입합니다.

5-2. 응집도와 결합도를 고려한 현실적인 설계 방향​

"응집도는 높이고 결합도는 낮춘다"만으로 경계가 자동으로 정해지지는 않습니다. 프로젝트 규모와 변경 빈도, 팀이 추적할 수 있는 복잡도를 함께 봅니다.

  • 작은 단위부터 모듈화

    • API 호출과 UI처럼 변경 이유가 분명한 경계부터 나누고, 상태 관리나 데이터 가공은 필요할 때 분리합니다.
  • 필요한 곳에만 추상화

    • 단순한 기능은 함수나 훅으로 충분할 수 있습니다. 별도 계층이 변경 비용을 줄일 때만 추가합니다.
  • 팀이 같은 경계를 찾을 수 있게 만들기

    • hooks/, context/ 같은 폴더 규칙은 코드를 어디에서 찾아야 하는지 팀이 합의하는 데 씁니다. 폴더 이름만으로 책임이 분리되지는 않습니다.

관련된 코드는 같은 곳에서 바뀌게 하고, 모듈 사이에는 필요한 정보만 전달합니다. 분리한 뒤 파일과 계약이 더 늘었다면 작은 변경이 실제로 쉬워졌는지 다시 확인합니다.


References​

좋은 사람들과 재미있는 일을 하며 열정적이고 즐겁게 살고 싶은 개발자