원래는 클라이언트 코드를 만졌었는데 어쩌다 보니 어드민쪽을 계속 다루게 되어서 어드민쪽에 먼저 적용했다. (클라쪽에 적용하려고 낑낑대며 쓰던 글은 임시글 어딘가에..)
물론 코드 봐주는 사람이 없어서 나 혼자 낑낑대면서 한거라 제대로 한지는 모르겠다😥
클린아키텍쳐란?
클린 아키텍처는 소프트웨어 설계 원칙 중 하나로, 의존성 방향을 안쪽(비즈니스 로직)으로 향하게 강제하여 유지보수성, 확장성, 테스트 용이성을 높이는 구조이다.
클린 아키텍쳐는 다음을 지켜야 한다.
1. 의존성 방향 제어
클린 아키텍쳐는 보통 네 가지 계층으로 나뉜다.
1. Entities / Models: 순수한 비즈니스 규칙 / 데이터 구조
나는 어떻게 적용했는가
1. Entities / Models (도메인 모델)
-> /src/model/dto
클린 아키텍처에서 가장 안쪽 계층은 비즈니스 핵심 데이터 구조이다.
보통 백엔드에서는 Entity, Value Object, DTO, ViewModel 등으로 구분하지만,
프론트엔드에서는 DB와 직접 연결된 Entity보다는 API 요청/응답 시 사용하는 DTO 위주로 다루게 된다고 한다. 그래서 일단 폴더명을 model이라고 했고, 안에 dto라는 폴더를 두었다. 아직 다른 폴더를 둘 필요성은 못 느껴서 이렇게만 해두었는데 다른 부분에도 클린 아키텍쳐 적용하다 보면 다른 폴더도 추가될 수도 있다.
export interface PostRecruitmentRequest {
paginationRequestDto: {
page: number
count: number
}
sort?: string
...
cf.
entities: DB와 직접 매핑되는 객체 (백엔드 중심)
dto: 데이터 전송 객체, API 요청/응답 스펙 정의 (프론트 중심)
viewModel: 뷰에 맞게 가공된 데이터 (ex: 날짜 포맷팅 등 포함)
mappers: dto ↔ viewModel 간 변환 함수
export const validateCareerDates = (start: string, end: string) => {
...
return new Date(start) < new Date(end)
}
Interface Adapters (UI/Hook/서비스/포맷터 등)
-> /src/components, /src/services, /src/hooks
/src/components: ui컴포넌트들
/src/services: api 호출 함수들, 백엔드 연동 함수들
/src/hooks: 여기저기서 공통적으로 사용될 수 있는 api훅들, 상태 관리들
이 계층은 외부 시스템과 내부 로직을 연결해주는 다리 역할을 하며, 가장 많이 바뀌고, 테스트하기 쉬운 계층이다.
Frameworks & Drivers (Next.js, DB, 라이브러리 등)
-> /pages 등
라우팅 등을 하는 곳이다.
제대로 하고 있는 것이 맞길 바란다.
🧼 클린 아키텍처란?
클린 아키텍처는 소프트웨어 설계 원칙 중 하나로, 의존성의 방향을 안쪽으로 향하게 강제하여
유지보수성, 확장성, 테스트 용이성을 높이는 구조입니다.
핵심 원칙: 외부 계층은 내부 계층을 참조할 수 있지만, 내부 계층은 외부에 의존해서는 안 됩니다.
🔍 주요 계층 구조
Frameworks & Drivers ➡ /pages, next.js
↑
Interface Adapters ➡ /components, /services
↑
Use Cases / Domains ➡ /domains/*
↑
Entities / Models ➡ /model/dto
어플리케이션의 핵심 개념과 데이터를 정의하는 계층입니다.
순수한 비즈니스 규칙과 핵심 데이터 구조
외부 시스템 (API, DB, UI) 에 전혀 의존하지 않는 순수한 도메인 객체
순수 JavaScript로 동작 가능한 형태로 작성
실제로 사용자의 요청이나 행동을 처리하는 로직을 담당합니다.
ex. 공고 작성, 날짜 유효성 검사 등
Entities를 이용하여 실제 동작을 구현
UI, API 호출, Formatter 등
외부(프레임워크, UI, API 등)와 내부 도메인 계층을 이어주는 다리 역할
UI/UX에 따라 자주 바뀔 수 있으므로, 도메인 로직과의 결합도를 낮추는 것이 중요함
프레임워크, 라이브러리, DB, Next.js 등
ex. /pages, _app.tsx, _document.tsx 등
실제 애플리케이션을 구동하는 외부 기술 요소
가장 바깥에 위치하며, 내부 도메인에 영향을 주지 않아야 함
✅ Admin에 적용한 클린 아키텍처
1️⃣ Entities / Models (도메인 모델 계층)
📁/src/model/dto
보통 백엔드에서는 Entity, Value Object, DTO, ViewModel 등으로 구분하지만,
프론트엔드에서는 DB와 직접 연결된 Entity보다는 API 요청/응답 시 사용하는 DTO 위주로 다루게 됩니다.
이에 model이라는 이름을 사용했고, 그 하위에 dto 폴더만 구성했습니다.
model 안에는 viewModels, mapper 등이 들어갈 수도 있다고 합니다. 필요 시 확장 가능합니다.
/src/model/dto/career/CareerDto.ts
export interface PostRecruitmentRequest {
paginationRequestDto: {
page: number
count: number
}
sort?: string
...
}
2️⃣ Use Cases / Business Logic (도메인 로직 계층)
📁 /src/domains
처음에는 /use-cases라는 폴더명을 고민했지만, 단일 책임 함수가 아니라 도메인 단위로 로직을 관리하고 싶어서 /domains로 이름을 지었습니다..
예를 들어 career 도메인 안에는 사용자 입력 폼에 대한 상태 관리 훅, 날짜 유효성 검사기 등이 들어 있습니다.
/src/domains/career/hooks/useNewCareerWriteForm.hook.ts
const [formData, setFormData] = useState({ ... })
const handleChange = (key, value) => {
setFormData(prev => ({ ...prev, [key]: value }))
}
/src/domains/career/validators/validateCareerDates.ts
export const validateCareerDates = (start: string, end: string) => {
return new Date(start) < new Date(end)
}
3️⃣ Interface Adapters (UI/API 서비스 등)
📁 /src/components, /src/services, /src/hooks
/components: 순수 UI 컴포넌트, 입력 필드, 테이블 등
...
const SelectedJobTags = ({ selected, onRemove, options }: Props) => {
if (!selected.length) return null
return (
{selected.map((uuid) => (
<Chip
key={uuid}
label={findJobLabel(uuid, options)}
onDelete={() => onRemove(uuid)}
color="primary"
/>
))}
)
}
...
/services: API 호출 모듈, 백엔드 연동 함수
/src/service/career/getWriteNewCareerSSRData.ts
...생략
const fetchRegisterOptions = async () => {
const fetchPromises = CHECKBOX_FIELDS.map((field) => {
const type = fieldToTypeMapping[field]
return getRegisterOptions(accessToken, type)
.then((res) => ({ field, options: res?.[type] || [] }))
.catch(() => ({ field, options: [] }))
})
const results = await Promise.all(fetchPromises)
return results.reduce(
(acc, { field, options }) => {
acc[field] = options
return acc
},
{ jobUuids: [], educationLevels: [], region: [], deadlineType: [] },
)
}
const fetchCompanyList = async () => {
const res = await getRecruitmentCompany(accessToken)
return res?.length ? res : []
}
const [registerOptions, companyList] = await Promise.all([
fetchRegisterOptions(),
fetchCompanyList(),
])
...생략
/hooks: 여러 도메인에서 재사용 가능한 hook들. /domain 내의 hooks는 그 도메인 내에서만 사용됨
도메인 로직에 직접적으로 의존하지 않도록 합니다. 추후에 단위테스트를 진행할 경우 테스트의 대상이 될 가능성이 높습니다.
4️⃣ Frameworks & Drivers (Next.js, 외부 라이브러리)
📁 /pages, 외부 패키지
Next.js의 페이지 라우팅 구조 (/pages, _app.tsx, _document.tsx)
SSR 처리, 쿠키/세션, 전역 레이아웃 등 프레임워크 중심 처리
외부 라이브러리 (/lib, canvasjs, slick-carousel 등)도 해당 계층에 포함
도메인 로직에는 영향을 주지 않도록 합니다.
현재 Admin 일부에만 위와 같이 클린 아키텍처가 적용되었습니다. 구조적으로 완벽하지 않거나 일관성이 떨어지는 부분도 있으나 점차적으로 개선해 나갈 예정입니다.