클린 아키텍처 적용하기

김민지·2026년 1월 4일

인턴 정리

목록 보기
7/7

원래는 클라이언트 코드를 만졌었는데 어쩌다 보니 어드민쪽을 계속 다루게 되어서 어드민쪽에 먼저 적용했다. (클라쪽에 적용하려고 낑낑대며 쓰던 글은 임시글 어딘가에..)
물론 코드 봐주는 사람이 없어서 나 혼자 낑낑대면서 한거라 제대로 한지는 모르겠다😥

클린아키텍쳐란?
클린 아키텍처는 소프트웨어 설계 원칙 중 하나로, 의존성 방향을 안쪽(비즈니스 로직)으로 향하게 강제하여 유지보수성, 확장성, 테스트 용이성을 높이는 구조이다.

클린 아키텍쳐는 다음을 지켜야 한다.
1. 의존성 방향 제어

  • UI는 도메인을 알 수 있지만, 도메인은 UI를 몰라야 한다.
  1. 관심사의 분리 (Separation of Concerns)
  • 상태 관리, 유효성 검사, API 호출, UI는 모두 서로 다른 계층에 위치
  1. 확장성과 테스트 용이성 확보
  • 특정 도메인의 UI만 바꾸거나 API만 바꿀 때, 다른 부분은 영향을 받지 않음.
  • 테스트 시에도 각각의 훅, 유효성 검사기, 서비스 함수 등을 단위 테스트하기 쉬움.

클린 아키텍쳐는 보통 네 가지 계층으로 나뉜다.
1. Entities / Models: 순수한 비즈니스 규칙 / 데이터 구조

  • 어플리케이션의 핵심 데이터 구조
  • 외부 시스템 (API, DB, UI) 에 전혀 의존하지 않는 순수한 도메인 객체
  1. Use Cases / Business Logic: 도메인 로직 / 유즈케이스 수행 단위
  • 사용자의 의도를 수행하기 위한 절차와 규칙
    예: 공고 작성 로직, 유효성 검사, 폼 상태 관리 등
  1. Interface Adapters: UI, API 호출, Presenter, Formatter 등
  • 외부와 내부 도메인 계층을 이어주는 다리 역할
  • React 컴포넌트, 훅, API 서비스, 포맷터, 상태 어댑터 등이 포함됨
  1. Frameworks & Drivers: 프레임워크, 라이브러리, DB, Next.js 등
  • 실제 애플리케이션을 구동하는 외부 기술 요소
  • 도메인 로직이 직접적으로 의존하지 않도록 해야 함

나는 어떻게 적용했는가
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 간 변환 함수

  1. Use Cases / Business Logic (도메인 로직)
    -> /src/domains
    처음에는 use-cases라고 했었는데 domains로 바꾸게 되었다. use-cases는 processOrder, calculateTax 등 단일 기능을 수행하는 단일 책임 함수를 담당하는 폴더기 때문이다.
    그런데 지금 클린 아키텍쳐를 가장 먼저 도입한 페이지에는 단일 책임 함수를 만들 만한 것이 없는 것 같아서 domain으로 했다. 그래서 그냥 그 안에 career를 두고 그 안에 hooks랑 validator를 넣어서 사용자가 입력한 폼의 상태가 올바른지 확인하는 로직을 넣었다.
export const validateCareerDates = (start: string, end: string) => {
  ...
  return new Date(start) < new Date(end)
}
  1. Interface Adapters (UI/Hook/서비스/포맷터 등)
    -> /src/components, /src/services, /src/hooks
    /src/components: ui컴포넌트들
    /src/services: api 호출 함수들, 백엔드 연동 함수들
    /src/hooks: 여기저기서 공통적으로 사용될 수 있는 api훅들, 상태 관리들
    이 계층은 외부 시스템과 내부 로직을 연결해주는 다리 역할을 하며, 가장 많이 바뀌고, 테스트하기 쉬운 계층이다.

  2. Frameworks & Drivers (Next.js, DB, 라이브러리 등)
    -> /pages 등
    라우팅 등을 하는 곳이다.

제대로 하고 있는 것이 맞길 바란다.


🧼 클린 아키텍처란?

클린 아키텍처는 소프트웨어 설계 원칙 중 하나로, 의존성의 방향을 안쪽으로 향하게 강제하여
유지보수성, 확장성, 테스트 용이성을 높이는 구조입니다.

핵심 원칙: 외부 계층은 내부 계층을 참조할 수 있지만, 내부 계층은 외부에 의존해서는 안 됩니다.

🔍 주요 계층 구조

Frameworks & Drivers ➡ /pages, next.js

Interface Adapters ➡ /components, /services

Use Cases / Domains ➡ /domains/*

Entities / Models ➡ /model/dto

  1. Entities (=Models)

어플리케이션의 핵심 개념과 데이터를 정의하는 계층입니다.

순수한 비즈니스 규칙과 핵심 데이터 구조

외부 시스템 (API, DB, UI) 에 전혀 의존하지 않는 순수한 도메인 객체

순수 JavaScript로 동작 가능한 형태로 작성

  1. Use Cases (=Business Logic)

실제로 사용자의 요청이나 행동을 처리하는 로직을 담당합니다.

ex. 공고 작성, 날짜 유효성 검사 등

Entities를 이용하여 실제 동작을 구현

  1. Interface Adapters

UI, API 호출, Formatter 등

외부(프레임워크, UI, API 등)와 내부 도메인 계층을 이어주는 다리 역할

UI/UX에 따라 자주 바뀔 수 있으므로, 도메인 로직과의 결합도를 낮추는 것이 중요함

  1. Frameworks & Drivers

프레임워크, 라이브러리, 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 일부에만 위와 같이 클린 아키텍처가 적용되었습니다. 구조적으로 완벽하지 않거나 일관성이 떨어지는 부분도 있으나 점차적으로 개선해 나갈 예정입니다.

profile
이건 대체 어떻게 만든 거지?

0개의 댓글