
저는 KCD 캐시노트페이팀(구 금융사업팀)에서 프론트엔드 엔지니어로 재직 중인 Hyunee(황주현)입니다. 이번 글에서는 저희 팀에서 사용 중인 프론트엔드 아키텍처를 소개 드리고자 합니다.
들어가며
아키텍처 격동의 시기
제가 입사한 2023년 10월 당시, 같은 팀의 프론트엔드 엔지니어인 Jerry(박지훈)는 팀 내 프론트엔드 아키텍처를 새롭게 구상하고 있었습니다. 제 입사 시점에는 이미 마무리 단계였기에 구상에는 참여하지 못했지만, 마이그레이션 작업에는 함께할 수 있었는데요. 그 경험을 바탕으로 당시의 이야기를 먼저 소개하려고 합니다. (이하 이전 아키텍처는 '구환경', 새로운 아키텍처는 '신규환경'으로 명칭하겠습니다.)
구 환경의 문제점
제가 경험한 구환경은 ‘아키텍처’라고 부르기엔 구조화가 부족했습니다. 전사적으로 하나의 프론트엔드 레포지토리를 사용하는 모노레포 구조였고, 일정 수준의 규칙은 있었지만 시간이 지나며 관리가 느슨해지고 일관성이 무너진 부분이 많았습니다.
하지만 모든 코드에는 사연이 있듯, 당시의 급박한 비즈니스 상황 속에서는 속도가 우선이었기에 어쩔 수 없는 선택이었다고 생각합니다. 속도와 코드 품질은 어느 정도 상충 관계에 놓여 있기 때문입니다.
이 구조에서 제가 느낀 주요 문제점은 두 가지였습니다.
1. 디렉토리 구조의 혼용에 따른 예측의 어려움
구 환경의 구조는 아래와 같았습니다.
겉으로 보기엔 괜찮아 보일 수 있지만, 실제로는 디렉토리 간 경계가 명확하지 않았습니다. pages에 비즈니스 로직을 포함한 컴포넌트가 있거나, domains에 공통 컴포넌트가 포함되는 등 일관성이 부족했습니다. 이로 인해 코드를 탐색하기 어렵고, 예측 가능한 구조가 아니기 때문에 생산성 저하로 이어졌습니다.

2. 에러 원인 파악의 어려움
서비스를 운영하다 보면 다양한 에러를 마주하게 됩니다. 그러나 모호하게 얽혀 있는 코드 구조 속에서, 해당 에러가 UI의 문제인지, API 응답 문제인지, 혹은 데이터 가공 중 발생한 문제인지 빠르게 판단하기 어렵습니다. VoC(고객의 소리) 대응에는 속도가 굉장히 중요하기 때문에 이는 치명적인 단점이었습니다.
프론트엔드 아키텍처
프론트엔드 생태계의 아키텍처
저희 팀의 아키텍처를 소개하기에 앞서, 간단히 제가 생각하는 프론트엔드 생태계의 아키텍처에 대해 적어보고자 합니다.
무엇이 있는가?


백엔드 생태계에서 아키텍처를 말해보라고 하면 여러 아키텍처가 떠오릅니다. 계층 분리에 중점을 둔 클린 아키텍처나, 포트와 어댑터를 사용하는 헥사고날 아키텍처 같은 것이 있죠. 그럼 프론트엔드 생태계의 아키텍처를 생각해 본다면 무엇이 있을까요? 개인적인 의견으로, 지금까지 많은 발전이 있었지만 아직까지 이렇다 할 바이블 역할의 아키텍처는 없다고 생각합니다. 서버 개발에 비해 비교적 짧은 역사와 매우 빠른 생태계 변화, 그리고 다양한 환경에 대응해야 하는 특징이 이러한 상황에 한 몫 했을 것입니다.
무엇에 집중해야 하는가?
앞서 언급 하였듯이, 사실 프론트엔드 라는 개념은 생겨난지 그렇게 오래되지 않았습니다. Web 개발 이라는 하나의 큰 개념에서 좀 더 세분화되어 프론트엔드와 백엔드 라는 개념이 생겨난 것이죠. 이전까지는 그러한 구분 없이 모두 서버를 만지고 HTML/CSS, JavaScript 등을 작성했습니다.
저는 이 점을 기억해야 한다고 생각합니다. 백엔드 에서 주로 사용하는 개념이라고 해서 프론트엔드 와는 전혀 다른 것이 아니라 사실 근본은 같기 때문입니다. 따라서 아키텍처를 완전히 동일한 구조로 구현하지 않더라도, 그 철학과 원칙을 접목시켜 유연한 구조를 만드는 것이 가능하다고 생각합니다.
CashnotePay팀의 프론트엔드 아키텍처
해당 내용은 실제로 아키텍처를 구상, 구현한 Jerry의 의견과 저의 주관적인 느낀점을 같이 작성하였습니다.
구상
프론트엔드의 계층(Layer)을 분리하자
Refactoring의 저자이자 소프트웨어계의 구루(guru)로 통하는 마틴 파울러(Martin Fowler)는 아래와 같이 주장했습니다.

On the whole I've found this to be an effective form of modularization for many applications and one that I regularly use and encourage. It's biggest advantage is that it allows me to increase my focus by allowing me to think about the three topics (i.e., view, model, data) relatively independently.
(전반적으로 나는 이것이 많은 응용 프로그램에 효과적인 모듈화 형태이며 내가 정기적으로 사용하고 장려하는 형태라는 것을 알았다. (나에게) 가장 큰 장점은 세 가지 주제에 대해 비교적 독립적으로 생각할 수 있게 함으로써 내 관심의 범위를 줄일 수 있다는 것이다.) – Martin Fowler
신규 아키텍처를 구상하던 Jerry는 해당 개념을 접목시켰습니다. 비단 프론트엔드, 백엔드에 국한하지 않고, 서비스를 만들 때 역할을 명확하게 분리하고 의존성을 끊어내는 것을 중요하다고 판단했습니다. 명확하게 분리하고 의존성을 끊어낼 수록, 경계가 생기며 책임이 명확해지는 장점을 가질 수 있습니다.
계층을 수직 조각으로 묶고, 조각 간의 의존을 최소화 하자
한글로 하니 이해하기 조금 어려운 문장입니다. 이는 Vertical Slice 아키텍처를 고안한 지미 보가드(Jimmy Bogard)의 개념을 접목시킨 것입니다.

When adding or changing a feature in an application, I'm typically touching many different "layers" in an application. I'm changing the user interface, adding fields to models, modifying validation, and so on. Instead of coupling across a layer, we couple vertically along a slice. Minimize coupling between slices, and maximize coupling in a slice.
(애플리케이션에서 기능을 추가하거나 변경할 때 일반적으로 애플리케이션의 다양한 "레이어"를 만집니다. 사용자 인터페이스를 변경하고, 모델에 필드를 추가하고, 유효성 검사를 수정하는 등의 작업을 하고 있습니다. 레이어 전체에 걸쳐 결합하는 대신 슬라이스를 따라 수직으로 결합합니다. 슬라이스 간의 결합을 최소화하고 슬라이스의 결합을 최대화합니다.) – Jimmy Bogard
저희 팀의 Repository 내부에는 카드매출 바로입금, 매장월세 카드결제 등 여러 가지의 서비스를 가지고 있습니다. 이러한 서비스 들이 각각의 계층을 가지게 하고, 이를 수직 조각으로 묶어 일관된 경험을 할 수 있도록 도와줍니다. 더하여 Co-location 을 통해 함께 변하는 것들을 같은 공간에 두어 변경되는 영역을 큰 노력 없이 알 수 있도록 했습니다. 개인적으로 이러한 구조 덕분에 담당하지 않는 서비스의 코드를 보더라도 어느정도 예측이 된다는 장점이 있었습니다.
구현
이제 실제로 구현된 구조를 보며 앞서 작성한 개념들이 어떻게 접목 되었는지 보겠습니다. 편의상 Provider, Routing은 제외하도록 하겠습니다.
디렉토리 구조
앞서 작성한 개념들을 접목하여 아래와 같은 디렉토리 구조가 표현됩니다.

계층 분리
계층은 하나의 Service 내 Data, Domain, State, Feature 4개로 구분됩니다.

Data Layer
데이터 계층 에서는 API와의 통신 구간이 존재합니다. 해당 계층에서는 API와의 통신이 정상적으로 이루어 졌는지만 판단합니다. 이 때 API의 응답 모델이 올바른지 아닌지는 검증하지 않습니다. 따라서 이 계층을 지났다면 Server와 Client 간의 통신은 정상적으로 이루어졌다고 판단할 수 있습니다.

Domain Layer
도메인 계층 에서는 실제로 프론트엔드 환경에서 각 서비스가 필요로 하는 도메인을 정의하고, 이에 맞게 변환하는 작업을 포함합니다. 또한 각 서비스 별 Repository를 구성하여 구현부에서는 해당 Repository를 이용해서 필요한 도메인 정보를 불러옵니다. 이 처럼 서비스에서 API Response Scheme을 그대로 사용하는 것이 아닌, 프론트엔드에서 한번 더 정의하여 API와의 의존성을 끊어내고, 변화에 유연하게 대처할 수 있도록 합니다.
저희 팀은 zod에서 제공되는 safeParse 메서드를 활용해 데이터 모델의 무결성을 검증합니다. 따라서 해당 계층에서 에러가 발생했다면, API는 정상적으로 응답을 받았지만, 프론트엔드가 정의한 모델과 다른 응답을 주었다고 판단할 수 있습니다.

State Layer
상태 계층에서는 localStorage, sessionStorage, IndexedDB 등의 스토리지와 기타 상태들을 처리하는 계층입니다. 저희 팀의 경우 React-Query 또한 해당 계층에 포함시켜 상태의 개념으로 사용합니다.

Feature Layer
기능 계층은 실제 서비스에 표현되는 UI, hook, util 등이 포함됩니다. 실제 서비스 구현에 해당하는 계층으로 이해할 수 있습니다. directory는 최대한 독립적인 기능으로서 동작 할 수 있는 범위로 나누어 구성합니다.
실제 데이터의 흐름
실제 서비스에서 데이터의 흐름은 아래 그림과 같이 움직이게 됩니다.

API Request
API Response - 이 때 API가 정상적으로 응답했는지 확인
Domain Model 변환 - 이 때 정상적으로 변환되는지 확인
React-query - queryKey 기반 저장
Feature - UI, hook 등 구현부에서 사용
계층 분리의 이점
앞서 간략히 언급 한 것 처럼, 계층이 분리되면 얻게되는 장점이 있습니다. 에러의 경계가 명확해지고 책임소재를 확실히 나눌 수 있다는 것인데요. 어떤 계층에서 에러가 발생 했는지 확인해야 하는 범위가 정해져 있기 때문에 해결 속도를 상당히 높일 수 있습니다. 그림과 함께 이해해 보겠습니다.
Data 계층에서 오류 발생

앞서 정의한 내용에 따라, 데이터 계층에서 에러가 발생 할 수 있는 시나리오는 아래와 같습니다.
따라서 해당 계층에서 오류가 발생했다면, 프론트엔드 코드를 분석하는 것이 아니라 동료 BE 개발자나 Network 쪽 로그를 확인해보면 됩니다.
Domain 계층에서 오류 발생

도메인 계층에서는 프론트엔드 에서 별도로 정의한 Model으로 변환 하는 작업을 거칩니다. 따라서 아래와 같은 오류 발생 시나리오가 존재할 수 있습니다.
따라서 해당 계층에서 오류가 발생했다면, 동료 BE 개발자에게 Scheme 변경을 확인해보거나, 변환하는 로직에 버그가 있는지 확인해 보면 됩니다.
State 계층에서 오류 발생

상태 계층에서는 데이터들을 저장하거나 불러오는 작업을 거칩니다. 따라서 일반적인 경우 오류가 발생하지 않으나, 라이브러리 버저닝 문제나 기기 오류 등의 문제가 생길 경우 오류가 발생 할 수 있습니다.
Feature 계층에서 오류 발생

기능 계층에서는 실제로 UI를 표현하거나 상호작용, 그리고 비즈니스 로직이 존재합니다. 따라서 해당 계층에서 오류가 발생했다면 작성했던 코드에 문제가 없는지, 예상치 못한 유저 시나리오는 없었는지 사용자 기록과 함께 확인해 보아야 합니다.
더 자세히 - Domain Layer
앞서 언급한 구조를 보면 Domain 계층에는 Model과 Repository가 존재하는 것을 알 수 있습니다. 이 계층은 해당 아키텍처의 핵심 중 하나 이므로, 이에 대해서 더 세분화 해서 설명해보겠습니다.
Model
Model은 말 그대로 데이터 스키마를 의미합니다. 그런데 왜 프론트엔드가 별도로 스키마를 정의하는 것이 좋을까요? 이를 이해해보기 위해 예시를 들어보겠습니다.
가상의 회원정보 조회 API가 아래와 같은 Response Scheme를 가졌다고 가정해 보겠습니다.

API의 Response Scheme를 보면 이름과 타입을 string으로 내려주고 있습니다. type이라는 데이터는 서비스 전반의 위치에서 사용중이며, type에 따라 동작이 달라지거나 UI가 변경되기도 합니다. 백엔드 개발자와 소통하여 이 타입은 STUDENT와 TEACHER 두 가지의 타입이 존재하는 걸 확인해 두었습니다. 이를 실제 코드로 보자면 아래와 같습니다.

충분히 익숙한 코드죠. 그런데 만약 어떠한 이유로 예고 없이 STUDENT, TEACHER 이외에 MENTO 라는 타입이 추가 되었다면 어떻게 될까요?
정답은 “어떻게 될 지 알 수 없다” 입니다. type 이라는 값이 어떻게 쓰여지고 검증되고 있는지에 따라 에러를 throw 할 수도 있고, 의도하지 않은 UI가 보여질 수 있으며, 아예 아무것도 보이지 않을 수도 있습니다. 확실하게 말 할 수 있는 건 의도한대로 동작되지 않을 것이라는 겁니다.
API의 Response Scheme가 의논 없이 변경 된 것 부터 잘못 된 게 아닌가요?
그럴 수 있습니다. 하지만 지금 잘못을 한 쪽이 누구인지는 중요한 것이 아닙니다. 먼저 직면한 문제를 빠르게 해결해서 서비스를 정상화 하는 것이 더 중요합니다. 또한, 의외로 실무를 진행하다 보면 이러한 상황이 종종 발생할 수 있습니다. 가장 중요한 건 이 상황이 발생했을 때 최대한 빨리 인지 하고 해결 하는 것입니다.
그러나 앞서 서술했듯이 이는 인지가 될 수도, 안 될 수도 있습니다. 또한 인지가 되었다고 하더라도, API의 Response Scheme가 변경 되어서 이러한 이슈가 발생했다는 것을 알기 까지에는 정말 많은 코드를 읽어보아야 할 것입니다.
하지만 Domain 계층을 두고 해당 계층에서 데이터 정합성을 확인하게 되면 범위를 Domain 계층으로 한정 지을 수 있습니다.
이를 그림으로 요약해보면 아래와 같습니다.

따라서 프론트엔드에서 직접 모델을 구성하고 검증하는 것은 경계를 확실히 구분짓게 하고, 에러 트래킹을 용이하게 만듭니다. API의 Response Scheme와 프론트엔드에서 구성할 모델이 동일 하더라도 진행하는 이유입니다.
Repository
Domain 계층에는 Model 외에 Repository라 칭하는 것이 존재합니다. 이는 객체지향의 Repository Pattern에 사용되는 것과 비슷하다고 이해하면 좋습니다. (Repository Pattern에 대해선 서술하지 않습니다.)

이는 Feature 계층에서 곧바로 Data 계층에 접근하지 못하도록 하는 역할을 합니다. Data 계층은 오로지 Domain 계층의 Repository에서만 접근하도록 구성하고, 추상화 된 interface로써만 사용하도록 강제합니다. 따라서 서비스의 모든 곳에서는 Repository에서 받아온 정제된 데이터를 사용하게 됩니다.
이 같은 특징은 Feature 계층에서 Data 계층 (API)에 대해 전혀 신경쓰지 않아도 되도록 만듭니다. Feature 계층은 UI, Business Logic 등 워낙 다양한 종류를 가지고 있기 때문에 디버깅이 비교적 어렵습니다. 이를 Domain 계층의 Repository에서 추상화해 중앙 집중처리 하게 되면 관리하는 포인트를 하나로 좁히고 디버깅을 용이하게 할 수 있습니다.
장점
해당 아키텍처로 약 1년 이상 개발 및 운영해 보며 느꼈던 가장 큰 장점을 정리해 보면 아래와 같습니다.
계층과 그에 따른 역할이 명확히 분리되어 있어, 에러 트래킹 시간 많이 감소
Co-locate 및 Vertical Slice를 통해 담당하지 않은 서비스임에도 코드를 이해하는 시간 많이 감소
장점만 있는 것은 아닙니다
하지만 무엇이든지 완벽한 것은 없듯이, 해당 아키텍처도 장점만 지니고 있는 것은 아닙니다. 제가 느꼈던 단점을 정리해보면 아래와 같습니다.
보일러 플레이트 코드 증가
프론트엔드 개발자가 직접 Data Model을 구상하고, 이를 Repository 에서 검증하는 절차가 있음에 따라 작성해야 하는 보일러 플레이트 코드의 양이 많아집니다. 또한, 단순 API Response Scheme를 그대로 가져오는 것이 아닌, 현재 서비스에 적합한 Data Model을 구상해야 하기 때문에 시간이 더 증가하게 됩니다.
프로젝트 온보딩 시간 증가
역할에 따라 계층의 분리가 명확히 되어있기 때문에, 이 컨벤션을 지키는 것이 중요합니다. 흔하게 접하지 않는 구조이기 때문에 새롭게 참여하는 개발자의 경우 온보딩에 일정 시간이 소요되게 됩니다. 또한 개발 중에도 지속적으로 기존 구조를 해치지 않는지 의식하며 개발해야 합니다.
마치며
이번 게시글에서는 프론트엔드 환경의 아키텍처와 CashnotePay팀의 아키텍처에 대해 소개해보았습니다. 앞서 작성하였듯이 장점과 단점이 존재했지만, 개인적으로는 장점이 더욱 큰 아키텍처라고 생각합니다.
그러나, 당연하게도 아키텍처에는 정답이 없습니다.
각각 처해진 조직의 환경이나 서비스에 따라 최적의 아키텍처를 선택하는 것이 중요하다고 생각합니다. 이 게시글 또한 저희 팀의 아키텍처가 정답이라는 것이 아닌, 이 같은 개념들을 프론트엔드 아키텍처에도 접목할 수 있다는 것을 소개하고 싶었습니다.
본인이 속한 조직에서도, 최적의 아키텍처를 구성하기 위해 고민해 보는 것은 어떨까요?
저는 KCD 캐시노트페이팀(구 금융사업팀)에서 프론트엔드 엔지니어로 재직 중인 Hyunee(황주현)입니다. 이번 글에서는 저희 팀에서 사용 중인 프론트엔드 아키텍처를 소개 드리고자 합니다.
들어가며
아키텍처 격동의 시기
제가 입사한 2023년 10월 당시, 같은 팀의 프론트엔드 엔지니어인 Jerry(박지훈)는 팀 내 프론트엔드 아키텍처를 새롭게 구상하고 있었습니다. 제 입사 시점에는 이미 마무리 단계였기에 구상에는 참여하지 못했지만, 마이그레이션 작업에는 함께할 수 있었는데요. 그 경험을 바탕으로 당시의 이야기를 먼저 소개하려고 합니다. (이하 이전 아키텍처는 '구환경', 새로운 아키텍처는 '신규환경'으로 명칭하겠습니다.)
구 환경의 문제점
제가 경험한 구환경은 ‘아키텍처’라고 부르기엔 구조화가 부족했습니다. 전사적으로 하나의 프론트엔드 레포지토리를 사용하는 모노레포 구조였고, 일정 수준의 규칙은 있었지만 시간이 지나며 관리가 느슨해지고 일관성이 무너진 부분이 많았습니다.
이 구조에서 제가 느낀 주요 문제점은 두 가지였습니다.
1. 디렉토리 구조의 혼용에 따른 예측의 어려움
구 환경의 구조는 아래와 같았습니다.
겉으로 보기엔 괜찮아 보일 수 있지만, 실제로는 디렉토리 간 경계가 명확하지 않았습니다. pages에 비즈니스 로직을 포함한 컴포넌트가 있거나, domains에 공통 컴포넌트가 포함되는 등 일관성이 부족했습니다. 이로 인해 코드를 탐색하기 어렵고, 예측 가능한 구조가 아니기 때문에 생산성 저하로 이어졌습니다.
2. 에러 원인 파악의 어려움
서비스를 운영하다 보면 다양한 에러를 마주하게 됩니다. 그러나 모호하게 얽혀 있는 코드 구조 속에서, 해당 에러가 UI의 문제인지, API 응답 문제인지, 혹은 데이터 가공 중 발생한 문제인지 빠르게 판단하기 어렵습니다. VoC(고객의 소리) 대응에는 속도가 굉장히 중요하기 때문에 이는 치명적인 단점이었습니다.
프론트엔드 아키텍처
프론트엔드 생태계의 아키텍처
저희 팀의 아키텍처를 소개하기에 앞서, 간단히 제가 생각하는 프론트엔드 생태계의 아키텍처에 대해 적어보고자 합니다.
무엇이 있는가?
백엔드 생태계에서 아키텍처를 말해보라고 하면 여러 아키텍처가 떠오릅니다. 계층 분리에 중점을 둔 클린 아키텍처나, 포트와 어댑터를 사용하는 헥사고날 아키텍처 같은 것이 있죠. 그럼 프론트엔드 생태계의 아키텍처를 생각해 본다면 무엇이 있을까요? 개인적인 의견으로, 지금까지 많은 발전이 있었지만 아직까지 이렇다 할 바이블 역할의 아키텍처는 없다고 생각합니다. 서버 개발에 비해 비교적 짧은 역사와 매우 빠른 생태계 변화, 그리고 다양한 환경에 대응해야 하는 특징이 이러한 상황에 한 몫 했을 것입니다.
무엇에 집중해야 하는가?
앞서 언급 하였듯이, 사실 프론트엔드 라는 개념은 생겨난지 그렇게 오래되지 않았습니다. Web 개발 이라는 하나의 큰 개념에서 좀 더 세분화되어 프론트엔드와 백엔드 라는 개념이 생겨난 것이죠. 이전까지는 그러한 구분 없이 모두 서버를 만지고 HTML/CSS, JavaScript 등을 작성했습니다.
저는 이 점을 기억해야 한다고 생각합니다. 백엔드 에서 주로 사용하는 개념이라고 해서 프론트엔드 와는 전혀 다른 것이 아니라 사실 근본은 같기 때문입니다. 따라서 아키텍처를 완전히 동일한 구조로 구현하지 않더라도, 그 철학과 원칙을 접목시켜 유연한 구조를 만드는 것이 가능하다고 생각합니다.
CashnotePay팀의 프론트엔드 아키텍처
해당 내용은 실제로 아키텍처를 구상, 구현한 Jerry의 의견과 저의 주관적인 느낀점을 같이 작성하였습니다.
구상
프론트엔드의 계층(Layer)을 분리하자
Refactoring의 저자이자 소프트웨어계의 구루(guru)로 통하는 마틴 파울러(Martin Fowler)는 아래와 같이 주장했습니다.
신규 아키텍처를 구상하던 Jerry는 해당 개념을 접목시켰습니다. 비단 프론트엔드, 백엔드에 국한하지 않고, 서비스를 만들 때 역할을 명확하게 분리하고 의존성을 끊어내는 것을 중요하다고 판단했습니다. 명확하게 분리하고 의존성을 끊어낼 수록, 경계가 생기며 책임이 명확해지는 장점을 가질 수 있습니다.
계층을 수직 조각으로 묶고, 조각 간의 의존을 최소화 하자
한글로 하니 이해하기 조금 어려운 문장입니다. 이는 Vertical Slice 아키텍처를 고안한 지미 보가드(Jimmy Bogard)의 개념을 접목시킨 것입니다.
저희 팀의 Repository 내부에는 카드매출 바로입금, 매장월세 카드결제 등 여러 가지의 서비스를 가지고 있습니다. 이러한 서비스 들이 각각의 계층을 가지게 하고, 이를 수직 조각으로 묶어 일관된 경험을 할 수 있도록 도와줍니다. 더하여 Co-location 을 통해 함께 변하는 것들을 같은 공간에 두어 변경되는 영역을 큰 노력 없이 알 수 있도록 했습니다. 개인적으로 이러한 구조 덕분에 담당하지 않는 서비스의 코드를 보더라도 어느정도 예측이 된다는 장점이 있었습니다.
구현
이제 실제로 구현된 구조를 보며 앞서 작성한 개념들이 어떻게 접목 되었는지 보겠습니다. 편의상 Provider, Routing은 제외하도록 하겠습니다.
디렉토리 구조
앞서 작성한 개념들을 접목하여 아래와 같은 디렉토리 구조가 표현됩니다.
계층 분리
계층은 하나의 Service 내 Data, Domain, State, Feature 4개로 구분됩니다.
Data Layer
데이터 계층 에서는 API와의 통신 구간이 존재합니다. 해당 계층에서는 API와의 통신이 정상적으로 이루어 졌는지만 판단합니다. 이 때 API의 응답 모델이 올바른지 아닌지는 검증하지 않습니다. 따라서 이 계층을 지났다면 Server와 Client 간의 통신은 정상적으로 이루어졌다고 판단할 수 있습니다.
Domain Layer
도메인 계층 에서는 실제로 프론트엔드 환경에서 각 서비스가 필요로 하는 도메인을 정의하고, 이에 맞게 변환하는 작업을 포함합니다. 또한 각 서비스 별 Repository를 구성하여 구현부에서는 해당 Repository를 이용해서 필요한 도메인 정보를 불러옵니다. 이 처럼 서비스에서 API Response Scheme을 그대로 사용하는 것이 아닌, 프론트엔드에서 한번 더 정의하여 API와의 의존성을 끊어내고, 변화에 유연하게 대처할 수 있도록 합니다.
저희 팀은 zod에서 제공되는 safeParse 메서드를 활용해 데이터 모델의 무결성을 검증합니다. 따라서 해당 계층에서 에러가 발생했다면, API는 정상적으로 응답을 받았지만, 프론트엔드가 정의한 모델과 다른 응답을 주었다고 판단할 수 있습니다.
State Layer
상태 계층에서는 localStorage, sessionStorage, IndexedDB 등의 스토리지와 기타 상태들을 처리하는 계층입니다. 저희 팀의 경우 React-Query 또한 해당 계층에 포함시켜 상태의 개념으로 사용합니다.
Feature Layer
기능 계층은 실제 서비스에 표현되는 UI, hook, util 등이 포함됩니다. 실제 서비스 구현에 해당하는 계층으로 이해할 수 있습니다. directory는 최대한 독립적인 기능으로서 동작 할 수 있는 범위로 나누어 구성합니다.
실제 데이터의 흐름
실제 서비스에서 데이터의 흐름은 아래 그림과 같이 움직이게 됩니다.
API Request
API Response - 이 때 API가 정상적으로 응답했는지 확인
Domain Model 변환 - 이 때 정상적으로 변환되는지 확인
React-query - queryKey 기반 저장
Feature - UI, hook 등 구현부에서 사용
계층 분리의 이점
앞서 간략히 언급 한 것 처럼, 계층이 분리되면 얻게되는 장점이 있습니다. 에러의 경계가 명확해지고 책임소재를 확실히 나눌 수 있다는 것인데요. 어떤 계층에서 에러가 발생 했는지 확인해야 하는 범위가 정해져 있기 때문에 해결 속도를 상당히 높일 수 있습니다. 그림과 함께 이해해 보겠습니다.
Data 계층에서 오류 발생
앞서 정의한 내용에 따라, 데이터 계층에서 에러가 발생 할 수 있는 시나리오는 아래와 같습니다.
API에서 정상적인 응답 (ex. 200 OK) 을 받지 못함
Network 이슈 등으로 응답 자체를 받지 못함
따라서 해당 계층에서 오류가 발생했다면, 프론트엔드 코드를 분석하는 것이 아니라 동료 BE 개발자나 Network 쪽 로그를 확인해보면 됩니다.
Domain 계층에서 오류 발생
도메인 계층에서는 프론트엔드 에서 별도로 정의한 Model으로 변환 하는 작업을 거칩니다. 따라서 아래와 같은 오류 발생 시나리오가 존재할 수 있습니다.
BE API Response Scheme이 별도 공지 없이 변경되어 변환 실패
Model 변환 로직에 버그가 존재
따라서 해당 계층에서 오류가 발생했다면, 동료 BE 개발자에게 Scheme 변경을 확인해보거나, 변환하는 로직에 버그가 있는지 확인해 보면 됩니다.
State 계층에서 오류 발생
상태 계층에서는 데이터들을 저장하거나 불러오는 작업을 거칩니다. 따라서 일반적인 경우 오류가 발생하지 않으나, 라이브러리 버저닝 문제나 기기 오류 등의 문제가 생길 경우 오류가 발생 할 수 있습니다.
Feature 계층에서 오류 발생
기능 계층에서는 실제로 UI를 표현하거나 상호작용, 그리고 비즈니스 로직이 존재합니다. 따라서 해당 계층에서 오류가 발생했다면 작성했던 코드에 문제가 없는지, 예상치 못한 유저 시나리오는 없었는지 사용자 기록과 함께 확인해 보아야 합니다.
더 자세히 - Domain Layer
앞서 언급한 구조를 보면 Domain 계층에는 Model과 Repository가 존재하는 것을 알 수 있습니다. 이 계층은 해당 아키텍처의 핵심 중 하나 이므로, 이에 대해서 더 세분화 해서 설명해보겠습니다.
Model
Model은 말 그대로 데이터 스키마를 의미합니다. 그런데 왜 프론트엔드가 별도로 스키마를 정의하는 것이 좋을까요? 이를 이해해보기 위해 예시를 들어보겠습니다.
가상의 회원정보 조회 API가 아래와 같은 Response Scheme를 가졌다고 가정해 보겠습니다.
API의 Response Scheme를 보면 이름과 타입을 string으로 내려주고 있습니다. type이라는 데이터는 서비스 전반의 위치에서 사용중이며, type에 따라 동작이 달라지거나 UI가 변경되기도 합니다. 백엔드 개발자와 소통하여 이 타입은 STUDENT와 TEACHER 두 가지의 타입이 존재하는 걸 확인해 두었습니다. 이를 실제 코드로 보자면 아래와 같습니다.
충분히 익숙한 코드죠. 그런데 만약 어떠한 이유로 예고 없이 STUDENT, TEACHER 이외에 MENTO 라는 타입이 추가 되었다면 어떻게 될까요?
정답은 “어떻게 될 지 알 수 없다” 입니다. type 이라는 값이 어떻게 쓰여지고 검증되고 있는지에 따라 에러를 throw 할 수도 있고, 의도하지 않은 UI가 보여질 수 있으며, 아예 아무것도 보이지 않을 수도 있습니다. 확실하게 말 할 수 있는 건 의도한대로 동작되지 않을 것이라는 겁니다.
그럴 수 있습니다. 하지만 지금 잘못을 한 쪽이 누구인지는 중요한 것이 아닙니다. 먼저 직면한 문제를 빠르게 해결해서 서비스를 정상화 하는 것이 더 중요합니다. 또한, 의외로 실무를 진행하다 보면 이러한 상황이 종종 발생할 수 있습니다. 가장 중요한 건 이 상황이 발생했을 때 최대한 빨리 인지 하고 해결 하는 것입니다.
그러나 앞서 서술했듯이 이는 인지가 될 수도, 안 될 수도 있습니다. 또한 인지가 되었다고 하더라도, API의 Response Scheme가 변경 되어서 이러한 이슈가 발생했다는 것을 알기 까지에는 정말 많은 코드를 읽어보아야 할 것입니다.
하지만 Domain 계층을 두고 해당 계층에서 데이터 정합성을 확인하게 되면 범위를 Domain 계층으로 한정 지을 수 있습니다.
이를 그림으로 요약해보면 아래와 같습니다.
따라서 프론트엔드에서 직접 모델을 구성하고 검증하는 것은 경계를 확실히 구분짓게 하고, 에러 트래킹을 용이하게 만듭니다. API의 Response Scheme와 프론트엔드에서 구성할 모델이 동일 하더라도 진행하는 이유입니다.
Repository
Domain 계층에는 Model 외에 Repository라 칭하는 것이 존재합니다. 이는 객체지향의 Repository Pattern에 사용되는 것과 비슷하다고 이해하면 좋습니다. (Repository Pattern에 대해선 서술하지 않습니다.)
이는 Feature 계층에서 곧바로 Data 계층에 접근하지 못하도록 하는 역할을 합니다. Data 계층은 오로지 Domain 계층의 Repository에서만 접근하도록 구성하고, 추상화 된 interface로써만 사용하도록 강제합니다. 따라서 서비스의 모든 곳에서는 Repository에서 받아온 정제된 데이터를 사용하게 됩니다.
이 같은 특징은 Feature 계층에서 Data 계층 (API)에 대해 전혀 신경쓰지 않아도 되도록 만듭니다. Feature 계층은 UI, Business Logic 등 워낙 다양한 종류를 가지고 있기 때문에 디버깅이 비교적 어렵습니다. 이를 Domain 계층의 Repository에서 추상화해 중앙 집중처리 하게 되면 관리하는 포인트를 하나로 좁히고 디버깅을 용이하게 할 수 있습니다.
장점
해당 아키텍처로 약 1년 이상 개발 및 운영해 보며 느꼈던 가장 큰 장점을 정리해 보면 아래와 같습니다.
계층과 그에 따른 역할이 명확히 분리되어 있어, 에러 트래킹 시간 많이 감소
Co-locate 및 Vertical Slice를 통해 담당하지 않은 서비스임에도 코드를 이해하는 시간 많이 감소
장점만 있는 것은 아닙니다
하지만 무엇이든지 완벽한 것은 없듯이, 해당 아키텍처도 장점만 지니고 있는 것은 아닙니다. 제가 느꼈던 단점을 정리해보면 아래와 같습니다.
보일러 플레이트 코드 증가
프론트엔드 개발자가 직접 Data Model을 구상하고, 이를 Repository 에서 검증하는 절차가 있음에 따라 작성해야 하는 보일러 플레이트 코드의 양이 많아집니다. 또한, 단순 API Response Scheme를 그대로 가져오는 것이 아닌, 현재 서비스에 적합한 Data Model을 구상해야 하기 때문에 시간이 더 증가하게 됩니다.
프로젝트 온보딩 시간 증가
역할에 따라 계층의 분리가 명확히 되어있기 때문에, 이 컨벤션을 지키는 것이 중요합니다. 흔하게 접하지 않는 구조이기 때문에 새롭게 참여하는 개발자의 경우 온보딩에 일정 시간이 소요되게 됩니다. 또한 개발 중에도 지속적으로 기존 구조를 해치지 않는지 의식하며 개발해야 합니다.
마치며
이번 게시글에서는 프론트엔드 환경의 아키텍처와 CashnotePay팀의 아키텍처에 대해 소개해보았습니다. 앞서 작성하였듯이 장점과 단점이 존재했지만, 개인적으로는 장점이 더욱 큰 아키텍처라고 생각합니다.
그러나, 당연하게도 아키텍처에는 정답이 없습니다.
각각 처해진 조직의 환경이나 서비스에 따라 최적의 아키텍처를 선택하는 것이 중요하다고 생각합니다. 이 게시글 또한 저희 팀의 아키텍처가 정답이라는 것이 아닌, 이 같은 개념들을 프론트엔드 아키텍처에도 접목할 수 있다는 것을 소개하고 싶었습니다.
본인이 속한 조직에서도, 최적의 아키텍처를 구성하기 위해 고민해 보는 것은 어떨까요?