최근에 사이드 프로젝트를 개발하려고 하는 순간..!
몇달 되지도 않았지만 쌓여있는 레거시 코드들을 보면서 뭐가 이렇게 급해서 모든 기능들이 한 파일안에 전부 들어가 있는지..
가독성도 떨어지고 기능 하나 수정하려고 스크롤을 위 아래로 왔다 갔다 하다가 깨달았다.
이거 분리해야겠다고..
그래서 한 파일에 몰려있던 UI, 로직, API 호출을 쪼개고, Custom Hook으로 분리하고, 유틸 함수로 빼냈다.
나름 뿌듯했다.
그런데 피드백을 받고 깨달았다.
나는 "관심사 분리"를 너무 좁게 생각하고 있었다.
내가 한 건 "함수 단위 분리"였다. 사이드 이펙트 줄이기, 재사용성 높이기, 코드 정리하기. 틀린 건 아니지만, 관심사 분리의 일부분일 뿐이었다.
알고 보니 관심사 분리는 어떤 관점에서 바라보느냐에 따라 완전히 다른 이야기가 된다.
관심사 분리란 ? (SoC, separation of concerns)
https://ko.wikipedia.org/wiki/%EA%B4%80%EC%8B%AC%EC%82%AC_%EB%B6%84%EB%A6%AC
관심사 분리(SoC)는 프로그램을 구별된 부분으로 나누는 설계 원칙이다.
각 부분은 하나의 관심사(Concern)만 담당한다.
여기서 관심사란 " 프로그램 코드에 영향을 미치는 정보의 집합 " 을 의미한다.
이게 좀 추상적인데, 쉽게 말하면 아래와 같다.
| UI 렌더링 | 화면에 뭘 어떻게 보여줄 것인가 |
| 데이터 페칭 | 서버에서 데이터를 어떻게 가져올 것인가 |
| 상태 관리 | 데이터를 어디에 어떻게 저장할 것인가 |
| 유효성 검증 | 입력값이 올바른지 어떻게 판단할 것인가 |
| 에러 처리 | 문제가 생기면 어떻게 대응할 것인가 |
이렇게 각각이 하나의 "관심사"인 것이다.
그리고 관심사 분리는 단순히 코드를 쪼개는 것이 아니다.
"정보를 잘 정의된 인터페이스가 있는 코드 부분 안에 캡슐화시킴으로써 달성한다"
즉, 분리된 각 모듈은
1. 내부 구현을 숨기고 (캡슐화)
2. 외부에는 인터페이스만 노출한다.
"useLogin" 이라는 커스텀 hook을 예로 들자면
내부에서 API를 어떻게 호출하는지, 에러를 어떻게 처리하는지 사용하는 쪽은 몰라도 된다.
const { login, isLoading, error } = useLogin()
내부 구현은 숨기고 useLogin이라는 인터페이스만 노출 시키는 것이다.
이를 왜 해야하는가?
유지보수 하기 좋은 코드란 결국 미래에 어떤 변화가 발생했을 때 쉽게 대응할 수 있는 코드 이다.
그러러면 미리 미래에 발생할 가능성이 있는 일들에 대해 고민해야 한다.
- UI 디자인이 바뀌면?
- API 스펙이 변경되면?
- 새로운 기능이 추가되면?
- 라이브러리를 교체해야 하면?
- 다른 팀원이 이 코드를 수정해야 하면? 한꺼번에 이 모든 걱정을 하면서 코드를 짜야 한다면 상당히 골치 아프다.
한번에 한 가지 걱정만 하자
관심사 분리는 프로그램을 관심사 별로 쪼개서 가능하면 한 번에 한 가지 걱정만 하게 만드는 것이다.
- UI를 수정할 때는 UI만 걱정한다.
- API를 수정할 때는 API만 걱정한다.
- 비즈니스 로직을 수정할 때는 로직만 걱정한다.
관심사 분리가 잘 이뤄지면?
| 이점 | 설명 |
| 단순화 | 각 부분이 작아지고 이해하기 쉬워진다. |
| 유지보수 자유도 | 한 부분을 수정해도 다른 부분에 영향이 없다. |
| 독립적 개발 | 팀원들이 서로 다른 모듈을 병렬로 작업 가능 |
| 모듈 재사용 | 분리된 모듈을 다른 곳에서도 사용 가능 |
| 점진적 업그레이드 | 전체를 바꾸지 않아도 일부만 개선 가능 |
"다른 부분의 세세한 사항을 모르더라도, 또 해당 부분들에 상응하는 변경을 취하지 않더라도 하나의 관심사의 코드 부분을 개선하거나 수정할 수 있게 된다"
이게 관심사 분리의 궁극적인 목표이다.
하지만 관심사 분리에도 비용이 들어간다..ㅠ
위키피디아에서 "인터페이스의 추가는 필수이며 실행에 쓰이는 더 순수한 코드가 있는 것이 일반적이다" 라고 하는데
관심사 분리를 하게 됐을때
- 파일 수가 늘어나게 되고
- 추상화 레이어가 추가되고
- 코드 흐름을 파악하려면 여러 파일을 봐야하는것
- 과도한 분리는 오히려 복잡도를 높인다는 점 때문에 "적절한 수준"을 찾는 것이 중요하다.
관심사 분리의 다양한 관점
처음 리팩토링을 할 때 내 기준은 단순했다.
- 이 함수가 너무 길다 -> 쪼개자
- 이 로직이 반복된다 -> 유틸로 빼자
- API 호출이 컴포넌트에 있다 -> Hook으로 분리하자
틀린건 아니지만 이건 함수 / 모듈 단위의 분리일 뿐이었다.
관심사 분리에 대해서 공부하다보니 관심사 분리는 어떤 기준으로 나누느냐에 따라서 완전히 다른 이야기가 된다.
1. 수평적 분리 (레이어 분리)
애플리케이션을 역할/계층에 따라 수평으로 분리하는 방식이다.
Presentation Layer (UI, 화면 표시)
↓
Business Layer (비즈니스 로직)
↓
Persistence Layer (데이터 접근, API)
↓
Database Layer (저장소)
각 게층은 자신의 책임만 수행하고, 다른 게층의 내부 구현은 알지 못한다.
핵심 규칙은 의존성의 방향인데
- 상위 계층은 하위 계층에 의존하며
- 하위 계층은 상위 계층을 모른다.
Presentation Layer는 Business Layer를 의존하지만, Business Layer는 Presentation Layer의 존재를 모른다.
그저 전달받은 데이터로 로직을 수행할 뿐이다.
이를 프론트엔드에 적용한다면 ??
features/auth/
├── components/ # Presentation Layer
│ └── LoginForm.tsx
├── hooks/ # Business Layer
│ └── useLogin.ts
└── api/ # Persistence Layer
└── authApi.ts
- LoginForm : UI 렌더링만 담당, 비지니스 로직은 모름
- useLogin : 로그인 로직 수행, API 내부 구현 모름
- authApi : 실제 서버 통신. UI가 어떻게 생겼는지 모름
이렇게 분리한다면 디자인이 바뀌어도 API 코드를 건드릴 필요가 없고, API 스펙이 수정되어도 UI 컴포넌트는 영향 받지 않는다.
이것이 Layered Architecture 의 핵심이다.
(Layered Architecture에 대한 자세한 내용은 다음 글에서 다룰 예정이다.)
2. 수직적 분리 ( 도메인/기능 분리 )
기능이나 도메인 단위로 수직으로 분리하는 방식이다.
features/
├── auth/ # 인증 도메인
├── payment/ # 결제 도메인
└── campaign/ # 캠페인 도메인
`auth`를 수정할 때 `payment` 폴더를 열 일이 없다. 도메인 간 의존성을 최소화하는 게 핵심이다.
정리하자면 수평적 분리는 역할/계층을 기준으로 나뉘고 수직적 분리는 기능/도메인 기준으로 나뉘게 된다.
실제로 이 둘을 적용한다면 다음과 같은 구조를 가질 수 있다.
features/
├── auth/ # 수직 분리 (도메인)
│ ├── components/ # 수평 분리 (레이어)
│ ├── hooks/
│ ├── api/
│ └── types/
├── payment/ # 수직 분리 (도메인)
│ ├── components/ # 수평 분리 (레이어)
│ ├── hooks/
│ ├── api/
│ └── types/
수직으로 도메인을 나누고, 각 도메인 내에서 수평으로 레이어를 나눌 수 있게 된다.
3. 횡단 관심사 분리 (Cross-cutting Concerns)
여러 모듈에 공통으로 나타나는 관심사를 분리하는 방식이다.
횡단 관심사란 특정 도메인에 속하지 않고 모든 곳에서 필요한 기능을 말한다.
- 로깅
- 에러 핸들링
- 인증/권한 체크
- 성능 모니터링
이런 기능들은 auth, payment 등 어디에도 속하지 않지만 모든 곳에서 필요하다.
예를 들어 모든 API 함수에서 로깅, 에러 처리가 반복된다면
const login = async () => {
try {
console.log('API 호출 시작')
const res = await axios.post('/login')
console.log('API 호출 성공')
return res.data
} catch (e) {
console.error('API 호출 실패')
handleError(e)
}
}
공통 로직을 한 곳으로 분리해 관심사 분리를 할 수 있는것이다.
const apiClient = axios.create()
apiClient.interceptors.request.use(config => {
console.log('API 호출 시작', config.url)
return config
})
apiClient.interceptors.response.use(
response => {
console.log('API 호출 성공')
return response
},
error => {
console.error('API 호출 실패')
return handleApiError(error)
}
)
이제 개별 API 함수는 핵심 로직만 담으면 된다.
const login = async (credentials) => {
const res = await apiClient.post('/login', credentials)
return res.data
}
참고로 이런 접근 방식을 AOP(Aspect-Oriented Programming) 라고 부른다.
프론트엔드에서는 어떻게 횡단 관심사 처리를 할 수 있을까?
| 관심사 | 분리 방법 |
| API 에러 처리 | Axios Interceptor |
| 로딩 상태 | React Query, Suspense |
| 인증 체크 | Route Guard, HOC |
| 전역 에러 | Error Boundary |
| 토스트 알림 | Context + Provider |
4. 모듈화 & 디자인 패턴
기능을 작은 단위로 나누고, 검증된 패턴을 적용해 관심사를 구조화하는 방식이다.
디자인 패턴은 관심사 분리를 어떻게 구현할 것인가에 대한 답이다.
예를 들어 MVC 패턴 을 보면 아래와 같이 각각의 관심사가 명확히 분리되어 있다.
Controller → 사용자 요청 처리, 흐름 제어
Model → 데이터와 비즈니스 로직
View → 화면 표시
View 가 바뀌어도 Model은 영향받지 않고, Model이 바뀌어도 Controller 로직은 그대로다.
대표적인 패턴은 아래와 같다.
| 패턴 | 분리 기준 | 설명 |
| MVC | Model / View / Controller | 데이터, 화면, 제어 로직 분리 |
| MVVM | Model / View / ViewModel | 데이터, 화면, 화면 로직 분리 |
| Custom Hook 패턴 | 상태+로직 / 렌더링 | React에서 사용되는 방식 |
React로 프론트엔드 개발할때 흔하게 쓰이는 Custom Hook + 컴포넌트 패턴은 아래와 같다.
// useLogin.ts - 로직 담당
export const useLogin = () => {
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
const login = async (credentials: LoginCredentials) => {
setIsLoading(true)
try {
const result = await loginApi(credentials)
return result
} catch (e) {
setError('로그인 실패')
} finally {
setIsLoading(false)
}
}
return { login, isLoading, error }
}
// LoginForm.tsx - UI 담당
export const LoginForm = () => {
const { login, isLoading, error } = useLogin()
return (
{error && {error}}
{isLoading ? '로그인 중...' : '로그인'}
)
}
UI가 어떻게 생겼든 `useLogin`은 동일하게 동작한다.
로직이 어떻게 돌아가든 `LoginForm`은 받은 데이터를 보여주기만 한다.
마무리
처음에는 "함수를 쪼개면 관심사 분리"라고 생각했다.
한 파일에 몰려있던 코드를 Custom Hook으로 빼고, 유틸 함수로 분리하고, 나름 뿌듯해했다.
하지만 피드백을 받고 나서야 깨달았다. 내가 한 건 관심사 분리의 일부분일 뿐이었다.
관심사 분리는 단순히 코드를 나누는 것이 아니라
"무엇을 기준으로 나눌 것인가" 를 먼저 정의하는 것이다.
- 레이어로 나눌 것인가 ? ( 수평적 분리 )
- 도메인으로 나눌 것인가 ? ( 수직적 분리 )
- 공통 기능을 어떻게 처리할 것인가 ? ( 횡단 관심사 )
- 어떤 패턴을 적용할 것인가 ? ( 디자인 패턴 )
그리고 가장 중요한 건 " 왜 이렇게 나눴는지 " 설명할 수 있어야 한다는 것이다.
다음 글에서는 글에서 언급한 Layered Architecture 에 대해 프론트엔드 관점에서 더 깊에 다뤄보겠다!
'공부 > 기술 개념 정리' 카테고리의 다른 글
| React 와 Vue 비교하기 (1) | 2026.01.29 |
|---|---|
| 단방향 vs 양방향 상태관리: React와 Vue의 핵심 차이 (0) | 2026.01.29 |
| OAuth 프로토콜 (7) | 2025.06.27 |
| Yarn Berry란? (1) | 2025.06.27 |
| 패키지 매니저의 정의와 차이 (yarn, pnpm, npm) (0) | 2025.06.27 |