내용
아키텍처 관련해서 여쭤보고 싶은게 있습니다ㅎㅎ 바쁘시면 여유 있으실 때 천천히 답변주셔도 괜찮습니다ㅎㅎ
아키텍처 구조를 변경하려한 주 배경은 이렇습니다ㅎㅎ
- 비즈니스 로직, 뷰로직이 컴포넌트 레벨에 혼재됨
- api 응답 구조를 그대로 클라이언트 상태로 쓰고 있어 api 구조 및 키가 변경될 경우 컴포넌트 레벨까지 영향이 감
- 비즈니스 로직이 pinia, 컴포넌트, hook에 분산되어있음 (이건 초기에 제가 잘못한 것 같습니다...ㅎㅎ)
- 1, 3번을 이유로 테스트코드 구현 시 모킹할 데이터가 너무 많고 비즈니스 로직을 따로 테스트할 수 없어 컴포넌트 통합 테스트 밖에 구현할 수 없음
- pinia store 모듈이 감당하는 로직이 점점 커져서 특정 값에 대한 로직을 보고 싶을 때 파악하기가 어려움 (useProductStore 로 예를 들면 재고 관련된 로직이 state, action, getters 에 각각 들어가있는데 코드 줄이 길어지면서 스크롤을 무진장 해야됩니다.) 이와 같은 이유로 아래와 같은 생각을 했습니다.
=> 컴포넌트는 User Action을 받아 비즈니스로직/뷰로직에 위임하고, 비즈니스 로직/뷰 로직의 단위를 쪼개서 코드를 응집하면 보기도 편하고 테스트코드 작성도 편해지지 않을까?
그래서 SRP(단일책임원칙), DIP(의존성역전원칙) 에 대해 공부하기 시작했습니다. (SRP/DIP -> 클린 아키텍처 -> 헥사고날 아키텍처까지 흐름 타고 가버렸습니다...ㅋㅋㅋ)
여기서 여러가지를 고민하게 되었는데요!
- 외부 인터페이스에 대한 의존성을 끊기 위함이라면 프레임워크(Vue), 상태관리 라이브러리(Pinia) 도 의존성을 끊어야하지 않을까?=> Vue, Pinia 는 변경될 일이 없으므로 오버엔지니어링. 요구사항이 변경될 수 있는 컴포넌트, 응답 구조가 변경될 수 있는 API만을 외부 인터페이스로 두고 비즈니스로직으로 내려오는 의존성을 끊어보자.
- 이미 구현되어있는 API 모듈이 많은데 의존성을 끊는 Port 를 모두 만들어야할까?=> 필요없는 공수가 많이 들 것 같다. api 응답을 엔티티(Price, Stock 등)에 맞게 끊는 것으로 인터페이스를 대체하자.
- UseCase 계층으로 pinia store를 사용하는게 나을까? 아니면 hooks를 사용하는게 나을까? 따로 두는게 나을까?=> pinia를 작은 도메인으로 관리하는 건 전역 상태 관리 성능 상 좋지 않다. 따로 두자니 없는 디렉토리를 하나 더 만드는게 부담이 된다. 이미 있는 hooks를 UseCase 계층으로 두자. 리뷰 해주신 내용을 토대로 생각한 현재 아키텍처 구조는 아래와 같습니다! 피드백 왕창 부탁드립니다ㅎㅎ
store와 같은 사이즈의 도메인(현재 도메인 ex) product, review)을 주 도메인, 더 쪼개질 수 있는 도메인을 세부 도메인( ex) inventory.ts, price.ts) 이라고 명칭하겠습니다.
-
src/hooks/[주 도메인]/하위에세부 도메인를 생성하고, 파일 내부에useGetInventory, useGetPrice, useDeleteReview이런식으로 UseCase를 구현 -
여러 주 도메인 에서 사용되는 UseCase의 경우
src/hooks/바로 아래에 위치 -
[세부 도메인].d.ts에 엔티티를 인터페이스로 정의 -
api 모듈에서 return 할 때 엔티티에 맞춰서 포맷팅
-
pinia store 에서는 엔티티에 맞춰서 CRUD 기본 로직, 파라미터없이 state로만 단순하게 만들 수 있는 getter만 구현
-
뷰 로직은 hooks에 두지말고 각 컴포넌트 내부에서 구현 참고 자료
-
DDD, Hexagonal, Onion, Clean, CQRS, ... How I Put it all together
정리
프론트엔드 팀의 소프트웨어 아키텍처 개선 제안을 정리하면 다음과 같습니다:
📋 현재 문제점
주요 이슈 5가지
- 로직 혼재: 비즈니스 로직과 뷰 로직이 컴포넌트에 섞여 있습니다
- API 의존성: API 응답 구조를 그대로 상태로 사용해 변경 시 전체에 영향을 줍니다
- 로직 분산: 비즈니스 로직이 Pinia, 컴포넌트, Hook에 흩어져 있습니다
- 테스트 어려움: 과도한 모킹이 필요하고, 비즈니스 로직 단독 테스트가 불가능합니다
- 코드 가독성: Pinia store가 비대해져 특정 로직 파악이 어렵습니다
🎯 해결 방향
핵심 아이디어: 컴포넌트는 User Action만 받아 처리를 위임하고, 로직을 응집도 높게 분리합니다
적용 원칙
- SRP (단일책임원칙)
- DIP (의존성역전원칙)
💭 주요 고민사항과 결정
프레임워크 의존성을 끊어야 하나?
Vue, Pinia는 변경 가능성이 없으므로 오버엔지니어링. 컴포넌트와 API만 외부 인터페이스로 두고 비즈니스 로직으로 내려오는 의존성을 끊는다.
모든 API에 Port를 만들어야 하나?
필요없는 공수가 많이 든다. 엔티티(Price, Stock 등) 단위로 API 응답을 포맷팅하여 인터페이스 역할을 대체한다.
UseCase 계층 위치: Pinia vs Hooks vs 별도 디렉토리?
Pinia를 작은 도메인으로 관리하면 전역 상태 관리 성능상 좋지 않다. 별도 디렉토리는 부담. 기존 hooks 디렉토리를 UseCase 계층으로 활용한다.
1. 프레임워크 의존성
- 고민: Vue, Pinia도 의존성을 끊어야 하나?
- 결정: 변경 가능성이 없으므로 오버엔지니어링입니다. 컴포넌트와 API만 외부 인터페이스로 관리합니다
2. Port 구현 범위
- 고민: 모든 API에 Port를 만들어야 하나?
- 결정: 엔티티(Price, Stock 등) 단위로 분리하여 인터페이스 역할을 대체합니다
3. UseCase 계층 위치
- 고민: Pinia store vs Hooks vs 별도 디렉토리?
- 결정: 기존 hooks 디렉토리를 UseCase 계층으로 활용합니다
🏗️ 제안 아키텍처 구조
디렉토리 구조
{"root":"src/","children":[{"name":"hooks/","children":[{"name":"[주 도메인]/","children":[{"name":"inventory.ts","children":[]},{"name":"price.ts","children":[]},{"name":"inventory.d.ts","children":[]}]},{"name":"[공통 UseCase]","children":[]}]},{"name":"stores/","children":[{"name":"product.ts","children":[]},{"name":"review.ts","children":[]}]},{"name":"api/","children":[{"name":"[도메인].ts → 엔티티 포맷팅","children":[]}]}]}
역할 분담
- Hooks (UseCase): 비즈니스 로직을 구현합니다
- API 모듈: 엔티티에 맞춰 응답을 포맷팅합니다
- Pinia Store: CRUD 기본 로직과 단순 getter만 구현합니다
- 컴포넌트: 뷰 로직을 담당합니다
용어 정의
- 주 도메인: store 수준의 큰 도메인 (product, review)
- 세부 도메인: 더 작은 단위 (inventory, price) 이 구조로 테스트 용이성, 코드 응집도, 유지보수성을 개선하고자 합니다.
