Engineering
SwiftUI와 TCA를 활용한 NOL 홈 리브랜딩
NOL Tech Blog야놀자
2025년 8월 7일
원문에서 보기 ↗클린 아키텍처로 팀과 함께 성장하기

이 이미지는 생성형 AI를 활용해 제작하였습니다.
하나의 앱을 오랜 시간 정성껏 개발하다 보면, 차곡차곡 쌓인 경험이 어느새 팀의 큰 자산이 됩니다.
하지만 장기적으로 서비스를 운영해온 팀이라면 누구나 한 번쯤 마주하는 기술적 고민이 있기 마련입니다. 저희 역시 10년 넘게 발전해 온 서비스를 함께 만들어오며 성장해왔지만, 그 과정에서 점점 복잡해진 코드 구조와 유지보수의 어려움이라는 현실적인 문제에 직면하게 되었습니다.
이 글에서는 저희가 NOL 앱의 홈 화면에 클린 아키텍처, SwiftUI, 그리고 The Composable Architecture (이하 TCA) 를 도입한 과정을 소개하며, 그 고민을 어떻게 풀어나갔는지 공유드리고자 합니다.
소개

앱 개발팀은 NOL(야놀자) 모바일 앱을 개발하고 있으며, 존재하는 여러 화면 중에서 사용자가 앱에서 가장 먼저 마주하는 홈 화면은 사용자 경험 측면에서 매우 중요한 역할을 합니다.
2025년 리브랜딩 작업 당시, 홈 화면은 더욱 다양한 화면 구성과 새로운 디자인 시스템, 그리고 새롭게 정의된 인터랙션에 대응해야 했습니다. 또한, 앞으로 추가될 다양한 기능들을 고려할 때, 높은 수준의 유연성과 복잡도를 동시에 요구받는 상황이었습니다.
이 과정에서 저희는 기존 방식대로 홈 화면을 제작할 것인지에 대해 고민하게 되었습니다. 저희 팀이 사용하던 자체 프레임워크는 XIB 기반 UI, 코드 기반 UI, SwiftUI까지 폭넓게 지원하며, 내부 로그 처리 등 필요한 기능을 자체적으로 해결할 수 있을 만큼 완성도 높은 구조였습니다. 그러나 점점 복잡해지는 요구사항에 따라 유지보수와 학습 비용이 지속적으로 증가하고 있었습니다.
초기에는 생산성을 높이기 위한 좋은 선택이었지만, 이번 리브랜딩처럼 기존 방식과는 다른 접근이 필요한 경우에는 오히려 그 구조가 팀의 피로도를 높이는 원인이 된다는 점을 실감하게 되었습니다.
이에 따라 저희는 UI 프레임워크로 SwiftUI를, 상태 관리는 TCA를 활용하며, 이를 Presentation Layer로 사용하는 클린 아키텍처를 도입하기로 결정했습니다. 새로운 홈 화면은 이 구조를 바탕으로 설계되었습니다.
기술 선택 배경
SwiftUI
SwiftUI는 애플이 2019년에 발표한 새로운 UI 구성 방식으로, 기존 UIKit과는 다음과 같은 차별점을 갖습니다
- 선언형 프로그래밍: 명령형 코드 방식과 달리, 화면에 무엇이 있는지를 선언적으로 표현합니다.
- Swift 최신 기능과 통합: async/await 등 최신 Swift 문법과 자연스럽게 통합됩니다.
앞서 언급한 것처럼 저희는 기존에 코드 기반 UI를 사용해왔습니다. 하지만 이 방식은 명령형 프로그래밍에 기반해 있어, 복잡한 화면일수록 UI와 상태 간의 흐름을 추적하기 어려웠고, 시니어와 주니어 개발자 간 퍼포먼스 격차가 커지는 원인이 되기도 했습니다.
기존 자체 UI 프레임워크의 유지보수에 어려움을 느끼고 있던 터라 SwiftUI의 도입을 적극적으로 검토하고 있었습니다. 다만 당시 최소 지원 버전이 iOS 14였고, SwiftUI는 아직 실제 서비스에 적용하기에는 부족한 점이 있어 고민이 따랐습니다. 그러던 중 2023년 WWDC에서 저희가 우려하던 부분들이 점차 개선되고 있음을 확인했고, 최소 지원 버전을 iOS 15로 상향한 시점부터는 미래 가치를 고려해 SwiftUI 도입을 결정했습니다.
화면 구성 측면에서는 확실한 생산성 향상이 있었지만, 상태 관리 측면에서는 한계도 존재했습니다. 특히 비즈니스 로직과 화면 상태 간의 관계를 명확히 정리하고 추적하는 데 어려움이 있었습니다.
TCA: The Composable Architecture
이 문제를 해결하기 위해 선택한 것이 바로 TCA 입니다. TCA는 상태와 로직을 명확히 분리하고, 애플리케이션을 작은 조각으로 나누어 관리할 수 있도록 도와주는 iOS용 아키텍처 프레임워크입니다. 특히 대규모 앱에서 발생하기 쉬운 상태 관리 혼란을 줄이고, 테스트 용이성과 코드 일관성을 높이는 데 효과적입니다. 다음과 같은 특징점을 가집니다.
- 예측 가능성: 모든 상태변화가 정해진 방식(Reducer)을 통해서만 발생하므로 흐름을 추적하기 쉽습니다.
- 테스트 용이성: 의존성 주입이 용이하고, 순수 함수 기반의 구조(Reducer) 덕분에 일관된 테스트 결과를 얻을 수 있습니다.
- 조립 가능성: 작은 단위의 기능을 조합해 더 큰 기능으로 구성할 수 있어 확장성과 재사용성이 뛰어납니다.
물론, TCA는 러닝 커브가 존재하며 처음 접하는 개발자에게는 진입 장벽이 될 수 있습니다. 그러나 Point-Free에서 활발히 유지되는 오픈소스 프로젝트로, Swift 커뮤니티 내에서 이미 널리 사용되고 있어 도입 자체는 큰 어려움 없이 진행할 수 있었습니다.
Clean Architecture
이렇게 해서, SwiftUI + TCA 조합으로 구성된 화면 표현부를 Presentation Layer로 활용하고, 이를 기반으로 한 클린 아키텍처를 도입하게 되었습니다. 클린 아키텍처는 UI, 비즈니스 로직, 데이터 처리를 명확히 분리하여 개발과 유지보수를 더 쉽게 만드는 소프트웨어 설계 원칙입니다. 이를 통해 다음과 같은 장점을 얻을 수 있습니다.
- UI와 비즈니스 로직의 명확한 분리: 화면을 구성하는 코드와 핵심 로직이 서로 영향을 주지 않도록 분리합니다.
- 단방향 흐름: 의존성은 한 방향으로만 흐르도록 구성하여 코드의 안정성을 높입니다.
이러한 구조를 통해 역할이 명확하게 나뉜 유연한 설계가 가능해졌고, 테스트와 유지보수 측면에서도 더 나은 품질을 기대할 수 있게 되었습니다.
구현 방식
저희가 홈 리브랜딩에 도입한 방식 (SwiftUI + TCA 를 PresentationLayer 로 하는 클린 아키텍처) 은 사실, 첫 도입이 아닙니다. 저희 앱개발팀 iOS 파트에서는 2023년 부터 꾸준히 시도를 해왔으며, 앱내 여러 지면 (예를 들어, 카테고리, 기획전 리스트, 설정 화면 등) 을 통해 점진적으로 해당 기능을 팀내에서 체득해오고 있었습니다. 홈 화면 같이 중요한 지면에 도입 하는 것은 제 예상보다 조금 빨랐지만, 저희 팀은 이미 준비가 되어 있었고 다음과 같은 식으로 대처하였습니다.

실제 프로젝트 내 Home 구조
우선 방향을 잡아보았습니다. Home 이라는 디렉토리를 생성하고, 그 안에 Domain, Data, Presentation 라는 명칭의 모듈로 나누어 정의해 보았습니다. 각 모듈 하위에는 다음과 같은 구현이 존재합니다.

클린 아키텍처 개괄
Data
이곳은 외부 세계와의 통신을 전담하는 영역입니다. 서버 API, 데이터베이스 등 구체적인 데이터 소스를 다루며, 앱의 다른 부분은 ‘어떻게’ 데이터를 가져오는지 몰라도 되도록 상세 구현을 캡슐화 합니다.
- Repository : Domain 레이어에 정의된 Repository 프로토콜의 실제 구현체가 위치하는 곳입니다. 예를 들어, ProductRepository 프로토콜을 준수하는 ProductRepositoryImpl 이나 ProductRepositoryMock 같은 클래스는 이곳에서 URLSession이나 Alamofire를 사용해 실제 서버와 통신하는 코드를 포함합니다.
- RequestDTO(Data Transfer Object) : 서버로 데이터를 보낼 때, API 명세에 정확히 일치하는 형태로 만들어진 데이터 모델입니다. 앱 내부에서 사용하는 모델(Entity)을 이 RequestDTO로 변환하여 네트워크 요청을 보냅니다.
- ResponseDTO(Data Transfer Object) : 서버로부터 데이터를 받을 때, API 응답 명세에 정확히 일치하는 형태로 만들어진 데이터 모델입니다. 외부로부터 받은 데이터를 일단 이 ResponseDTO로 파싱한 뒤, 앱의 핵심 비즈니스 모델인 Domain의 Entity로 변환하는 과정을 거칩니다.
- Tests : Data 레이어의 테스트는 주로 Repository 구현체의 정합성을 검증합니다. 가짜(Mock) 네트워크 응답 데이터를 주입했을 때, Repository가 ResponseDTO를 올바르게 디코딩하는지 등을 테스트합니다.
Domain
앱의 심장부입니다. 이곳에는 오직 순수한 비즈니스 로직만이 존재하며, UI 프레임워크, 데이터베이스 기술 등 외부의 기술에 가능한 의존하지 않도록 설계하고 있습니다.
- Entity : 앱의 핵심 비즈니스 데이터 모델입니다. 프로젝트의 주인공이라고 할 수 있죠. Entity는 다른 어떤 레이어에도 의존하지 않는 순수한 Swift 객체여야 합니다.
- Log : 앱의 핵심적인 이벤트나 상태 변화를 기록하기 위한 로깅 인터페이스(프로토콜)를 정의하는 곳입니다. Domain 레이어는 “what” 에 대한 명세를 인터페이스에 정의하고, “how” 에 해당하는 부분은 구현부에서 책임을 맡고 있도록 하고 있습니다. (예: Firebase, Kinesis 등) 이를 통해 의존성 규칙을 지키고자 합니다.
- UseCase : 특정 비즈니스 규칙이나 사용자의 작업 단위를 의미합니다. 예를 들자면, 홈의 헤더 정보를 가져오기 위한 HomeHeaderUseCase 나 홈에서 보여지는 팝업을 위한 HomePopupUseCase 등이 존재하고 있습니다. 이처럼, UseCase는 Repository 프로토콜을 통해 필요한 데이터를 요청하고, 가져온 데이터를 조합하여 의미 있는 비즈니스 로직을 수행합니다.
- Tests : Domain 레이어의 UseCase가 비즈니스 로직을 올바르게 수행하는지 검증합니다. Mock 데이터로 구성된 Repository를 주입하여, 특정 조건에서 UseCase가 예상된 결과를 정확히 반환하는지 순수 단위 테스트(Unit Test) 를 통해 확인합니다.
Presentation
사용자에게 보여지고, 사용자와 상호작용하는 모든 것을 책임지는 영역입니다. SwiftUI와 TCA가 이곳의 주인공입니다.
Sections
- 각 섹션별로 정의된 폴더명을 가집니다. 예를 들어, 배너, 기획전, 헤더, 푸터 등으로 명명되어 있습니다.
- View : SwiftUI로 섹션의 UI를 작성합니다. TCA의 Store가 제공하는 State를 기반으로 화면을 그리는 역할에만 집중합니다. View는 어떠한 로직도 소유하지 않으며, 사용자의 터치 같은 이벤트가 발생하면 이를 Action의 형태로 Store에 전달하기만 하는 수동적인 존재입니다.
- Store : TCA 아키텍처의 핵심 요소들을 담는 컨테이너입니다.
- Reducer : 해당 Section의 두뇌입니다. Action이 들어왔을 때 State를 어떻게 변경할지, 어떤 Side Effect(예: UseCase 호출)를 실행할지를 결정하는 모든 로직이 이곳에 정의됩니다.
- State : 하나의 View를 그리는 데 필요한 모든 데이터의 상태를 담고 있는 타입입니다.
- Action : 화면에서 일어날 수 있는 모든 종류의 이벤트를 enum으로 정의합니다. 사용자의 버튼 터치, 화면 나타남(onAppear), UseCase로부터의 응답 등이 모두 Action이 됩니다.
- Dependency : Reducer가 비즈니스 로직을 수행하기 위해 필요로 하는 외부 의존성을 관리합니다. @Dependency 프로퍼티 래퍼를 사용해 Domain 레이어의 UseCase를 주입받는 통로 역할을 합니다.
Tests
- 스냅샷 테스트. UI의 시각적 변화를 감지하는 테스트입니다. Reducer에 특정 State를 설정하여 View의 모습을 이미지 파일(스냅샷)로 저장해 둡니다. 이후 코드가 변경될 때마다 새로 찍은 스냅샷과 기존 스냅샷을 비교하여, 의도치 않은 UI 변경이 발생했는지 자동으로 확인합니다. 이는 UI의 일관성을 유지하는 데 매우 효과적입니다.
저희 앱의 홈 화면은 정적인 단일 화면이 아닌, 독립적으로 동작하는 여러 섹션들이 유기적으로 결합된 모듈형 구조로 설계되었습니다. 각 섹션은 자체적인 View와 Store를 가지며, 고유한 기능과 생명주기를 갖는 독립된 단위로 구성되어 있습니다.
Store는 해당 섹션의 화면을 그리는 데 필요한 모든 상태(State)와 발생 가능한 이벤트(Action)를 정의합니다. 외부 시스템과의 통신이나 비즈니스 로직 실행에 필요한 의존성은 @Dependency를 통해 명확하게 주입되며, 이는 Reducer를 거쳐 View로 전달됩니다. 이러한 구조는 각 UI 컴포넌트의 재사용성을 극대화하고, 개발 및 유지보수의 단위를 명확하게 분리하는 효과를 가져옵니다.
프레젠테이션 레이어의 역할은 사용자 인터페이스를 그리고, 사용자 상호작용에 응답하여 도메인 레이어의 UseCase를 호출하는 것까지입니다. Reducer는 UseCase를 통해 추상화된 인터페이스를 실행할 뿐, 그 결과가 어떻게 도출되는지에 대한 구체적인 로직은 전혀 알지 못합니다. 그 결과, UI는 비즈니스 로직의 복잡성으로부터 완벽하게 자유로워집니다.
또한, TCA의 핵심 원칙인 단방향 데이터 흐름을 철저히 준수하여 상태의 예측 가능성을 확보했습니다. 특히 각 섹션의 상태(State)를 TCA의 스코핑을 활용해 모듈화하고 격리함으로써, 특정 기능의 상태 변화가 다른 기능에 예기치 않게 영향을 미치는 것을 방지했습니다. 이는 복잡한 화면에서도 상태를 안정적으로 관리할 수 있는 견고한 기반이 되었습니다.
도입 효과
이 아키텍처를 도입한 후 저희가 가장 먼저, 그리고 가장 극적으로 체감한 변화는 개발 생산성의 향상과 시스템 안정성의 확보였습니다.
예를 들어, SwiftUI 와 TCA 를 최초로 배포했던 2024년 11월 9.0.0 버전을 배포 하는 시점과 2025년 4월 9.9.0 버전을 배포하는 시점의 비정상 종료 추이를 비교해 보면 다음과 같습니다.

표에서 보듯 배포 1주 전 부터 배포 이후 2주간 항상 전체 사용자 대비 일정 건수 미만의 비정상 종료 횟수를 지속적으로 유지하고 있고 크래시가 나지 않는 유저의 비율은 99.99%를 유지하는 것을 미루어 보아, 홈 화면이 전체적으로 교체되는 순간에도 안정적인 운영에는 아무런 문제가 없었음을 유추할 수 있습니다.
과거에는 기능 하나를 수정하면 다른 곳에서 발생하는 사이드 이펙트를 걱정해야 했지만, 이제는 각 섹션이 독립된 모듈로 존재하기에 신규 기능의 확장 및 기존 로직의 변경 작업이 다른 영역에 대한 우려 없이 독립적으로 수행될 수 있었습니다. 가령, 새로운 프로모션 섹션을 추가하는 일이 더 이상 다른 추천 상품 로직의 안정성을 위협하지 않게 된 것이죠.

홈 카테고리 섹션에 대한 스크린샷 테스트 예시
또한, TCA를 통해 상태 흐름이 명확하게 관리되면서 고질적인 문제였던 상태 불일치(State Inconsistency)나 UI 업데이트 충돌 현상이 현저히 감소했습니다. 명확하게 분리된 각 모듈은 유닛 테스트의 커버리지를 높이는 데 기여했으며, 스크린샷 테스트의 도입에 결정적인 역할을 해주었습니다. 이는 곧 QA 과정에서의 커뮤니케이션 비용을 절감하고 더 높은 품질의 프로덕트를 만드는 선순환 구조로 이어졌습니다. 결국, 잘 설계된 아키텍처는 단순히 ‘좋은 코드’를 넘어, 팀 전체의 개발 문화를 건강하게 만드는 핵심적인 역할을 하고 있습니다.
회고와 개선점
물론, 이 모든 과정이 순탄하기만 했던 것은 아닙니다. 도입 초기, 팀원들이 TCA의 익숙하지 않은 데이터 흐름과 상태 관리 패러다임에 온전히 적응하기까지는 분명한 학습 곡선이 존재했습니다.

함께 성장하는 팀 멤버 (AI 로 생성한 이미지 입니다)
특히, 자식 Reducer의 Action을 부모 Reducer로 전달하는 방식이나, State에 의해 View가 갱신된다는 원리를 체득하는 데에는 적지 않은 시간과 노력이 필요했습니다. 이러한 노력은 일부 구간의 성능최적화나 TestStore 활용을 위해 활용되었으며, 이렇게 깊이 탐구하며 해결해나가는 과정은 각 팀원이 아키텍처를 수동적으로 사용하는 것을 넘어 그 원리를 이해하고 스스로 성장하는 귀중한 자양분이 되었습니다.
때로는 SwiftUI 자체의 기술적 제약(특정 iOS 버전에서의 호환성 문제나 Navigation 로직 처리의 복잡성 등)이 예상치 못한 기술 부채로 다가오기도 했습니다. 하지만 이러한 도전 과제를 해결하며 쌓인 경험과 노하우는, 역설적으로 아키텍처를 더욱 견고하게 다지고 다른 기능으로 확장 적용하는 데 있어 무엇과도 바꿀 수 없는 자산이 되었습니다.
마무리
홈 화면은 사용자와의 첫 접점이자, 서비스의 핵심 가치가 모이는 전략적인 공간입니다. 이번 구조 개선을 통해 저희는 기술적으로 기능 간의 결합도를 낮추고 테스트 용이성을 확보하는 성과를 거두었습니다.
하지만 이번 도전의 가장 큰 수확은 단순히 코드의 품질을 높인 것을 넘어, 팀의 협업 문화를 한 단계 발전시켰다는 데 있습니다. 잘 설계된 아키텍처는 팀원 각자의 역할을 명확히 하고, 서로의 작업을 신뢰할 수 있는 건강한 개발 환경의 단단한 기반이 되었습니다.
결국, 성공적인 아키텍처 전환의 마지막 퍼즐은 더 나은 제품을 만들고자 하는 팀원들의 적극적인 의지와 열정이었습니다. 기술은 방향을 제시할 뿐, 그 길을 함께 걷는 동료가 있었기에 이 모든 긍정적인 변화가 가능했습니다.

누구나 마음 편히 놀 수 있는 세상을 함께 만들어갈 동료를 찾고 있어요 :)
