[리팩토링] 08. 외부 의존성(LinkNavigator) 제거

2026. 2. 25. 03:55·탭탭 - TapTap/리팩토링
728x90

와우... 약 2주 동안 진행했던 외부 의존성 제거 리팩토링 작업이 드디어 끝났습니다..!

현재 시간 새벽 3시.. PR까지 올리고 이제 자려고 했는데, 그동안 고민했던 내용들이 머리속에서 휘발될 것 같아 잠들기 전에 정리해보려고 합니다.

 

아무튼.. 이번 리팩토링은 정말 정말 쉽지 않은 작업이었습니다.

사실 코드 짜는 것은 그렇게 어렵지 않았어요. 아래에서 자세히 이야기하겠지만, StackState 기반 네비게이션은 이전에도 한 번 경험해본 적이 있었기 때문입니다.

 

진짜 어려웠던 건 구조 설계였습니다. 이미 Tuist로 피쳐가 멀티 모듈화가 되어 있는 상태에서

- 추상화와 캡슐화를 깨뜨리지 않으면서 네비게이션을 구현할 수 있을까?

- 피쳐 모듈 간 결합도를 높이지 않고 네비게이션을 처리할 수 있을까?

에 대한 고민 때문에 여러 자료를 찾아보고, TCA 공식 문서와 Point-Free 강의 영상도 찾아보면서 여러 가지 구조를 고민했습니다. 아마 지금까지 했던 리팩토링 중에서도 아키텍처를 가장 오래 고민했던 작업이었던 것 같습니다.

 

그만큼 배운 것도 많았고요! 자 이제 서론은 이쯤에서 마무리하고, 본격적으로 이야기 해보려고 합니다.

 

밤샘 작업을 하느랴 육체적으로는 꽤 힘들었지만, 정신적으로는 힘들지 않았던?

고민하는 고통이 생각보다 재밌었던.. 리팩토링에 대해 이야기 해보려고 합니다.


외부 의존성 제거?

리팩토링에도 이유가 있어야겠죠? 저번 글에서도 잠깐 언급했지만, 현재 탭탭의 네비게이션은 LinkNavigator라는 라이브러리를 사용하고 있었습니다.

 

 

GitHub - forXifLess/LinkNavigator: 🌊 Easy & Powerful navigation library in SwiftUI

🌊 Easy & Powerful navigation library in SwiftUI. Contribute to forXifLess/LinkNavigator development by creating an account on GitHub.

github.com

 

LinkNavigator는 TCA 환경에서 사용하기 편한 라이브러리이지만, 상대적으로 마이너한 외부 의존성이기 때문에 탭탭 프로젝트를 장기적으로 바라봤을 때 몇 가지 고민이 생겼습니다.

 

1. LinkNavigator는 마이너한 외부 의존성이고, TCA나 SwiftUI 업데이트 시 호환성 충돌 가능성이 존재합니다. 

2. 네비게이션은 앱의 코어이므로 외부 라이브러리의 유지보수나 대응 속도 및 호환성에 의존하게 되는 구조는, 장기적으로 리스크가 될 수 있다고 판단했습니다.

 

또 하나의 이유는 네비게이션 방식이 프로젝트 내부에서 통일되어 있지 않았습니다. 일부 화면은 StackState 기반으로 구현되어 있었고, 또 다른 일부는 LinkNavigator를 사용하고 있었습니다.

 

이러한 LinkNavigator에 대한 장기적인 우려와, 프로젝트 내부에서 네비게이션 방식이 통일되어 있지 않다는 점 때문에 이번 리팩토링을 진행하게 되었습니다.

 

+ 물론 "조금은 과한 리팩토링이지 않을까?", "불필요한 공수가 들어가는 리팩토링이 아닐까?" 에 대한 개발팀의 우려도 있었습니다. 하지만 네비게이션은 앱의 코어이기 때문에, 예측할 수 없는 리스크를 줄이는 것이 더 중요하다고 판단했습니다. 또한 팀원들이 이미 익숙하게 사용하고 있는 TCA의 StackState 기반 네비게이션으로 통일하는 것이 유지보수 측면에서도 더 유리하다고 판단했습니다. 


이번 리팩토링을 통해 기대하는 점?

1. 외부 라이브러리 의존성 제거

이번 리팩토링의 가장 큰 목표는 외부 의존성을 제거하는 것이었습니다. 네비게이션은 앱의 코어 영역이기 때문에, 특정 라이브러리의 유지보수 상황이나 업데이트 대응 속도에 영향을 받지 않는 구조로 만드는 것이 중요하다고 생각했습니다. 그만큼 특정 외부 라이브러리에 깊게 의존하기보다는, 프레임워크와 아키텍처에서 제공하는 방식으로 구성하는 것이 좋다고 생각했습니다.

 

2. 네비게이션 로직 일관성 증가

네비게이션을 TCA의 StackState 기반 구조로 통일하면서, 프로젝트 전반의 네비게이션 흐름을 보다 명확하게 정리할 수 있을 것으로 기대하고 있습니다. 또한 네비게이션 방식이 하나로 정리되면 코드를 읽는 입장에서도 구조를 이해하기 쉬워지고, 새로운 화면을 추가할 때도 동일한 패턴을 따라 구현할 수 있기 때문에 유지보수나 기능 개발에 있어서 도움이 될 것이라고 기대합니다. 


네비게이션 구조에 대한 고민

사실 탭탭 팀은 LinkNavigator를 도입하기 전, 이미 StackState 기반으로 네비게이션을 구현해 본 경험이 있습니다. 

 

하지만 당시에는 몇 가지 아쉬운 점이 있었습니다. 단순히 delegate를 통해 상위로 네비게이션 이벤트를 전달하는 방식이 생각보다 가독성이 떨어진다고 느꼈고, 네비게이션을 좀 더 중앙에서 관리하고 싶다는 이유도 있었습니다.

 

게다가 그 시점에는 TCA를 처음 도입하던 단계였기 때문에, 프레임워크나 아키텍처에 대한 이해도 역시 지금보다 낮은 상태였습니다.

 

이런 상황에서 LinkNavigator는 꽤 매력적인(?) 선택지였습니다.

네비게이션을 한 곳에서 관리할 수 있었고, 라우팅 방식도 비교적 직관적으로 느껴졌기 때문입니다. 이러한 이유들로 탭탭 팀은 LinkNavigator를 도입하여 네비게이션을 구현하였습니다.. 만! 

 

지금은 그런 LinkNavigator를 제거해야 하는 상황이 되었습니다.

본격적인 리팩토링을 진행하기 앞서, 꽤나 많은 고민이 있었습니다.

 

단순히 라이브러리를 제거하는 것이 아니라, LinkNavigator가 가지고 있던 장점은 최대한 유지하면서 리팩토링하고 싶었기 때문입니다. (조금은 개인적인 욕심일 수도 있지만..)

 

게다가 이전과 지금의 프로젝트 환경도 달라졌습니다. 예전에는 각 피쳐가 멀티 모듈로 분리되어 있지 않았지만, 지금은 각 피쳐가 독립된 모듈로 구성된 구조입니다.

 

이런 멀티 모듈 환경에서는 단순히 네비게이션하는 것 이상의 고민이 필요했습니다.

- 피쳐 간 의존성은 어떻게 관리할 것인지?

- 모듈 간 결합도는 어떻게 최소화할 것인지?

- 추상화와 캡슐화를 깨뜨리지 않으면서 네비게이션을 구성할 수 있을지?

 

이러한 것들을 모두 고려해야 했기 때문에, 리팩토링을 시작하기 전 구조 설계에만 거의 일주일 정도의 시간을 투자하게 되었습니다.


Tuist 모듈화의 의미

엥 갑자기 Tuist 이야기..? 네! 이 부분이 가장 고민이 많았던 지점이었기 때문입니다.

 

각 피쳐들이 독립적으로 모듈화 되어있는 상황에서, 어떻게 하면 좋은 구조를 설계할 수 있을까에 대해 고민을 정말 많이 했었습니다.

사실 모듈이 독립되어 있지 않았다면 네비게이션 구조를 설계하는 것은 그렇게 어렵지 않았을 거라고 생각합니다. 그냥 NavigationStackStore와 StackState를 활용해서 연결해 주기만 하면 되기 때문입니다. 접근 제어자나 의존성, 결합도 같은 것을 크게 고민하지 않아도 되었을 거라고 생각합니다. (그렇다고 아예 안하면 안되겠죠?)

 

그래서 일단 가장 원초적인 질문을 했습니다.

"도대체 모듈화는 왜 하는 걸까?"

"아 도대체 모듈화 모듈화.. 뭔데? 대체 뭐 때문에 이렇게까지 모듈을 나누는데?"

 

이 질문에서 시작해서 꼬리 질문을 계속 던졌습니다.

 

"모듈화에서 말하는 의존성이 대체 뭔데?"

"그래서 모듈화로 얻는 이점이 뭐고, 그 이점을 얻기 위해 내가 어떤 구조를 설계해야하는데?"

 

이 부분에 대한 자세한 이야기는 모듈화를 주제로 한 글에서 따로 정리할 예정입니다. 그래서 여기서는 모든 내용을 깊게 다루기보다는, 핵심만 간단히 이야기 하고 넘어가 보겠습니다.


도대체 모듈화는 왜 하는 걸까?

가장 원초적인 질문입니다. 대체 왜 모듈화를 하는 것이고, 뭐 때문에 이렇게까지 모듈을 나누는 것일까요?

 

모듈화의 가장 큰 목적은 관심사의 분리입니다. 즉, 서로 다른 역할을 하는 코드들을 적절하게 나누어 각 영역이 자신의 책임만 가지도록 만드는 것입니다. 예를 들어 네트워크 로직, UI, 비즈니스 로직, 데이터 모델 등이 하나의 모듈에 섞여 있다면 구조를 이해하기 어렵고 수정할 때 영향을 예측하기 힘들어집니다. 하지만 기능이나 역할에 따라 모듈을 나누면 코드의 구조가 훨씬 명확해집니다.

 

그렇다면 모듈화로 얻는 이점이 무엇일까요?

 

1. 의존성 관리가 쉬워집니다.

모듈이 분리되어 있으면 어떤 코드가 어떤 코드에 의존하고 있는지 훨씬 명확해집니다. 또한 잘 설계된 모듈 구조에서는 의존성의 방향을 의도적으로 설계할 수 있습니다. 예를 들어 UI 모듈이 네트워크 모듈을 직접 참조하지 않도록 하는 것들이 있겠죠. 

 

2. 코드 변경의 영향 범위를 줄일 수 있다.

모듈화가 되어 있지 않은 프로젝트에서는 작은 수정 하나가 예상하지 못한 영역까지 영향을 줄 수 있습니다. 하지만 모듈화가 되어있으면 특정 영역을 수정하더라도 그 영향이 해당 모듈 내부로 제한되는 경우가 많습니다.

 

즉, 코드 변경에 대한 리스크를 줄일 수 있습니다.

 

3. 빌드 속도 개선

모듈이 분리되어 있으면 변경된 모듈만 다시 빌드하면 되기 때문에 프로젝트 전체를 다시 컴파일하는 상황이 줄어들 수 있습니다.

 

결국 모듈화의 핵심은 단순히 폴더를 나누는 작업이 아니라, 코드의 경계를 정의하고 의존성을 설계하는 것이라고 생각합니다. 즉, 어떤 코드는 어디까지 책임을 가지고, 어떤 모듈이 어떤 모듈을 알아야 하는지를 결정하는 과정이라고 할 수 있겠습니다!

 

그렇기 때문에 모듈화의 이점을 제대로 살리려면, 모듈 간 의존성을 가능한 최소화 해야합니다. 


그래서 문제가 뭔데?

네비게이션은 "A 화면에서 B 화면으로 이동한다"라는 특성상, 결국 어떤 화면이 어떤 화면으로 이동하는지를 알아야 합니다. 하지만 피쳐 단위가 독립적인 모듈로 존재하는 상황에서는 이 부분이 단순하지 않았습니다.

 

import SearchFeature // SearchFeature 직접 참조

@Reducer
public struct HomeFeature { ... }

피쳐 간 네비게이션을 구현하려면 피쳐끼리 참조하는 것은 거의 필연적입니다. 예를 들어 HomeFeature에서 SearchFeature로 이동하려면, HomeFeature가 SearchFeature를 참조해야합니다. 

 

단순하게 생각하면 HomeFeature에서 SearchFeature를 import하면 끝 입니다. 하지만 이런 방식이 계속 반복되면 점점 많은 피쳐들이 서로를 직접 참조하게 되고, 결국 모듈 간 결합도가 빠르게 증가하게 됩니다. 멀티 모듈을 구성한 이유가 의존성을 줄이기 위해서인데, 네비게이션 때문에 다시 서로 얽히는 구조가 되어버리는 것이었습니다.

 

그래서 고민이 시작됐습니다.  "어떻게 하면 모듈 간 참조를 최소한으로 하면서 네비게이션 구조를 설계할 수 있을까?"

 

즉, 각 피쳐가 다른 피쳐를 직접 참조하지 않고도 네비게이션을 표현할 수 있는 구조가 필요했습니다. 이 고민을 하다 보니 자연스럽게 생각난 것이 Coordinator였습니다. 피쳐는 단지 "어디로 이동하고 싶다"는 의도만 전달하고, 실제 네비게이션 결정과 화면 연결은 상위 레벨에서 담당하도록 하는 구조입니다. 

 

이렇게 하면 피쳐는 다른 피쳐를 직접 알 필요가 없고, 피쳐 모듈 간 의존성을 줄일 수 있으며, 네비게이션 로직을 한 곳에서 관리할 수 있게 됩니다. 


추상화와 캡슐화 그리고 접근 제어자

코디네이터 구조로 설계를 한다고 해도, 결국 상위 모듈에서는 각 피쳐를 알아야 했고, 외부 모듈에서 다른 모듈의 타입을 사용하려면 접근 제어자를 public으로 선언해야 했습니다.

 

여기서 또 고민이 시작됐습니다.

 

"이렇게 접근 제어자를 public으로 열어버리면, 캡슐화와 추상화가 깨지는 것이 아닐까?"

처음에 제가 생각하고 있던 추상화는 OOP에서 이야기하는 전통적인 추상화였습니다.

 

예를 들어,

- 내부 구현을 숨긴다.

- 인터페이스만 노출한다.

- 변경의 영향을 최소화하는 것

과 같은 개념입니다.

 

즉, 제가 생각하고 있던 추상화는 클래스 / 타입 레벨의 추상화였고, 이 문제를 OOP 관점의 추상화로 해결하려고 했습니다. 그래서 또 이런 꼬리질문을 던지게 됐습니다.

- "Feature를 public으로 열어버리면 내부 구현이 외부로 노출되는 것 아닌가?" 

- "State나 Action을 외부에서 접근할 수 있으면 캡슐화가 깨지는 것 아닌가?"

 

그래서 직접 확인해보기로 했습니다. "각 Feature를 public으로 열면 정말 내부 구현이 외부로 다 보일까?"

엥?

백문이 불여일견! 실제로 각 Feature를 public으로 열고, 외부 모듈에서 접근해보았습니다. 결과는 예상과 조금 달랐습니다. 외부 모듈에서 Feature의 타입 자체에는 접근할 수 있었지만, 그 안에 있는 State나 Action을 마음대로 수정하거나 내부 로직을 건드릴 수 있는 것은 아니었습니다.

 

Feature를 public으로 선언하더라도, 상태 변경은 Reducer를 통해서만 이루어지고, 외부에서는 Action을 보내는 것만 가능하며, 실제 비즈니스 로직은 Feature 내부에 그대로 캡슐화되어 있었습니다. 즉, 아키텍처적으로는 여전히 Feature 내부 로직이 잘 감춰져 있는 구조였습니다.


그래도 불안해

Feature를 public으로 선언해도 내부 로직이 잘 감춰져있다는 것을 확인했지만, 그래도 접근제어자를 public으로 열어버리는 것에 대한 불안함은 감출 수 없었습니다. 사실 개발에 정답이란 없지만, 어떤 결정을 내렸을 때 그것에 대한 충분한 근거는 있어야한다고 생각하기 때문입니다. 

 

https://www.pointfree.co/episodes/ep171-modularization-part-1?utm_source=chatgpt.com

 

Video #171: Modularization: Part 1

We’ve talked about modularity a lot in the past, but we’ve never devoted full episodes to show how we approach the subject. We will define and explore various kinds of modularity, and we’ll show how to modularize a complex application from scratch us

www.pointfree.co

마침 TCA를 개발한 Point-Free에 모듈화에 대한 레퍼런스가 있어 참고해보았습니다. 핵심 내용은 꽤 단순했습니다. 멀티 모듈 환경에서는 다른 모듈에서 사용할 타입은 반드시 public으로 선언해야 한다는 것입니다. Swift의 기본 접근 제어자는 internal이고, 이는 같은 모듈 내부에서만 접근 가능합니다.

 

즉, 모듈을 나누는 순간 아래와 같은 상황이 생깁니다.

- A 모듈에서 정의한 Feature를

- B 모듈에서 사용하려면

- 그 타입은 반드시 public이어야 한다.

 

결국 TCA + 멀티 모듈 환경에서 public은 단순히 캡슐화를 깨는 키워드가 아니라 모듈 간에 사용할 수 있는 계약을 명시하는 도구에 더 가깝다는 이야기였습니다. (계약이라는 표현이 맞나 모르겠네요ㅋㅋㅋㅋㅋ TCA에서 contact라고 설명했길래 저도 계약이라고 표현했습니다!)


조금 다른 관점으로 생각해봅시다

그래서 이번에는 OOP 관점의 추상화가 아니라, 모듈화 관점에서 추상화로 접근해보기로 했습니다. 

 

OOP에서의 추상화는 보통 아래와 같은 질문으로 시작합니다.

- "내부 구현이 외부에 노출되는가?"

- "인터페이스만 공개하고 구현은 숨겼는가?"

 

하지만 모듈화 관점에서는 조금 달랐습니다.

"누가 누구를 알아도 되는가?" 즉, 모듈 간 의존성의 방향입니다. 

 

예를 들어,

HomeFeature -> Core
HomeFeature -> SettingFeature

이런 의존성은 괜찮은가? 

 

이처럼 모듈화에서의 추상화는 단순히 public을 줄이는 문제가 아니라, 의존성 그래프 자체를 설계하는 문제에 가까웠습니다. 생각해보면 Feature를 public으로 열더라도 Reducer 내부 로직이나 상태 변경 방식은 여전히 해당 Feature 내부에 머무르게 됩니다. 즉 구조를 결정하는 것은 접근 제어자보다 어떤 모듈이 어떤 모듈을 의존하는지였습니다.

 

이렇게 정말 많은 고민을 했었고..! 제가 내린 결론은!

"모듈화에서의 추상화는 접근제어보다 "의존성 방향"이 핵심이다" 라는 결론이 나오게 되었습니다.


Coordinator!

그동안 고민했던 네비게이션 구조에 대한 결론이 어느 정도 정리되었고, 이제 본격적으로 구조 설계에 들어가게 되었습니다.

 

1. 네비게이션 로직을 중앙화 하고 싶다.

2. 모듈 간 결합도를 낮추고 싶다.

3. 네비게이션 책임을 위임하고 싶다.

라는 니즈가 있습니다. 이 니즈를 바탕으로 Coordinator를 설계하기로 했습니다. 예전에 UIKit 기반 프로젝트를 진행할 때 한 번 학습하고 적용해 본 기억이 있었는데, SwiftUI + TCA 환경에서는 UIKit에서의 Coordinator와는 구현 방식에 약간 차이가 있었습니다.

 

구현 방식은 다양하지만, 보통 UIKit에서 이야기하는 Coordinator는 다음과 같은 형태를 가집니다.

import UIKit

final class AuthCoordinator: Coordinator {
  weak var parentCoordinator: Coordinator?
    
  var children: [Coordinator] = []
    
  var navigationController: UINavigationController
  ...
  func start() {
      goToSignUpViewController()
  }
}

extension AuthCoordinator {
    func goToSignUpViewController() {
        let signUpViewModel = SignUpViewModel(coordinator: self)
        let signUpViewController = SignUpViewController(viewModel: signUpViewModel)
        navigationController.pushViewController(signUpViewController, animated: true)
    }
    ...
}

Coordinator는 navigationController를 보유하고 있으며, ViewController를 생성하고, push/pop과 같은 네비게이션을 직접 수행합니다.

 

구조적으로 보면 아래와 같은 흐름이 됩니다.

VC -> Coordinator -> 목적지 VC

 

즉, ViewController가 다른 ViewController를 직접 생성하거나 이동하지 않고, 중간에 Coordinator가 개입하여 화면 전환을 담당하게 됩니다.


SwiftUI + TCA에서는?

SwiftUI와 TCA 환경에서는 UIKit처럼 navigationController를 직접 다루지 않기 때문에 동일한 방식으로 구현할 수는 없습니다. 그래서 Coordinator의 형태가 아니라 역할에 집중해서 구조를 설계했습니다.

 

UIKit에서 말하는 Coordinator의 역할은

- 화면 이동 관리

- Flow 관리

- 화면 간 연결을 담당

- VC의 책임 감소

이 본질을 기준으로 SwiftUI + TCA 환경에 맞게 설계하고자 했습니다.


UIKit의 Coordinator에서 가져온(?) 핵심 Flow

UIKit에서의 코디네이터는 보통

- UINavigationController를 소유하고

- 목적지 VC를 직접 생성하고

- push/pop을 실행하며

- 화면 흐름까지 조립합니다.

final class AuthCoordinator: Coordinator {
  var navigationController: UINavigationController

  func start() {
    goToSignUpViewController()
  }

  func goToSignUpViewController() {
    let vc = SignUpViewController(...)
    navigationController.pushViewController(vc, animated: true)
  }
}

즉, "화면 이동의 실행 주체"가 코디네이터이고, VC는 '이동하고 싶어유'라는 의도만 코디네이터에게 위임하는 방향입니다. 


그럼 SwiftUI + TCA에서는 "실행 주체"가 뭐가 될까요?

SwiftUI는 UIKit처럼 navigationController.pushViewController를 직접 호출하지 않습니다. 대신 NavigationStack의 상태가 곧 네비게이션입니다.

 

TCA에서는 그 상태가 바로!

StackState<Path.State> path

이고, 이동은 곧

- push : path.append(...)

- pop : path.removeLast()

로 표현됩니다.

 

즉, UIKit의 navigationController가 하던 역할을, SwiftUI + TCA에서는 path(StackState)가 한다고 보면 됩니다.


TCA Coordinator는 무엇을 할까요?

제가 설계한 Coordinator는 UIKit의 Coordinator의 역할을 거의 동일하게 수행하지만, 방식만 SwiftUI + TCA스럽게(?) 바뀌었습니다.

 

1. 화면 간 연결

UIKit : Coordinator가 직접 VC를 생성해 연결

TCA: Coordinator가 Feature State를 생성해 path에 넣음

case .home(.delegate(.route(.setting))):
  state.path.append(.setting(.init()))
  return .none

여기서 SettingFeature.State()를 만들고 path에 push하는 순간, NavigationStackStore가 자동으로 SettingView를 목적지로 렌더링합니다. UIKit의 pushViewController를 path.append가 대체한 셈입니다.

 

2. Flow 관리

UIKit: start()에서 첫 화면을 띄우고, 이후 단계별 분리 처리

TCA: root(feature) + path(destinationn)을 분리해 흐름을 구성

public struct State {
  var path = StackState<Path.State>()
  var home = HomeFeature.State()
}

home은 root화면이고, path는 루트 화면에서 파생되는 모든 흐름입니다. 이것이 UIKit의 코디네이터가 navigation stack을 관리하는 것과 동일한 역할을 합니다.

 

3. 화면 이동 관리

UIKit: navigationController.push/pop 직접 호출

TCA: route 이벤트를 받아 path를 조작

case .home(.delegate(.route(.linkDetail(let article)))):
  state.path.append(.linkDetail(.init(article: article)))
  return .none

HomeFeature는 LinkDetail로 가고 싶다라는 의도를 delegate로 버블 업해서 올리고, AppCoordinator가 최종적으로 push를 실행합니다. UIKit과 동일하게 이동은 Coordinator가, Feature(=VC)는 의도만 전달합니다.


Route?

코디네이터 구조를 통해 네비게이션 책임을 코디네이터로 위임하고, 네비게이션 로직을 중앙화 했습니다. 다음으로 모듈 간 결합도와 의존성을 낮춰야합니다.

 

피쳐와 피쳐 간 이동은 의존성을 만듭니다. 이는 의존성 그래프를 복잡하게 하고 순환 사이클 위험이 있고 변경 영향 범위가 증가합니다. 즉, 멀티 모듈의 의미가 약해집니다.

 

해결 방법은 간단하게 생각했습니다. 피쳐는 목적지를 몰라도 되게 만든다. 대신 어디로 가고 싶은지만 표현한다.

이를 위해 Route를 도입했습니다. Route는 쉽게 말해 앱 전체에서 사용하는 네비게이션 언어(?) 입니다.

 

아래와 같은 형태입니다.

public enum AppRoute {
  case search
  case setting
}

Feature는 특정 Feature를 직접 호출하지 않습니다. 대신 이동 의도만 전달합니다.

return .send(.delegate(.route(.search)))

작동 방식

작동방식

작동 방식은 아래와 같습니다

1. 사용자 액션 발생: FeatureView에서 사용자 이벤트가 발생하면 FeatureReducer로 전달 됩니다.

2. 이동 의도 표현: Reducer는 직접 화면을 이동하지 않고, delegate(.rotue(AppRoute)) 형태로 이동 의도만 전달합니다.

3. Route 전달: 이 라우트는 Shared 모듈의 AppRoute를 통해 상위로 전달됩니다.

4. Coordinator가 네비게이션 처리: App 모듈의 CoordinatorReducer가 Route를 받아 PathState에 append/remove를 수행합니다.

5. NavigationStack 업데이트: PathState 변경을 감지한 NavigationStackStore가 실제 목적지 View를 렌더링합니다.

 


마무리

최종적으로 네비게이션 의존성 방향은 위와 같이 정리되었습니다. 

Feature -> Shared(AppRoute)
Coordinator -> 모든 Feature

 

피쳐는 네비게이션을 위해 서로를 알 필요가 없어지고, AppRoute를 통해 이동 의도만 전달합니다. 그리고 실제 화면 전환과 Flow 관리는 Coordinator가 중앙에서 담당합니다.

 

이 구조를 통해

- Feature간 직접 의존성 제거

- 네비게이션 로직 중앙화

- Feature 모듈 간 결합도 감소

라는 효과를 얻을 수 있었습니다.

 

특히 멀티 모듈 환경에서 Feature가 서로를 import하지 않아도 된다는 점이 큰 장점이었습니다. 덕분에 모듈 간 결합도와 의존성도 줄어들고 구조도 훨씬 명확해졌습니다.

 

약 2주 동안 진행했던 네비게이션 리팩토링이 마무리되었습니다. 생각보다 고려해야 할 것도 많았고, 중간에 구조를 몇 번이나 다시 고민하기도 했습니다. 단순히 "동작하게 만드는 것"보다 구조적으로 맞는 방향인지 계속 고민했던 시간이었던 것 같습니다.

 

멀티 모듈 구조에서 의존성을 어떻게 관리할지, Feature간 결합도를 어떻게 낮출지, 그리고 SwiftUI + TCA 환경에서 Coordinator를 어떻게 구성할지 직접 고민하고 구현해볼 수 있었던 경험이었습니다.

 

조금 힘들기도 했지만 그만큼 배운 것도 많았고, 개인적으로는 꽤 재미있었던 리팩토링이었습니다. 감사합니다!

 

 

'탭탭 - TapTap > 리팩토링' 카테고리의 다른 글

[리팩토링] 09. DesignSystem 플랫폼 호환성 문제 해결  (0) 2026.03.31
[리팩토링] 07. SwiftDataClient 리팩토링 - SwiftDataClient의 비대함 완화 및 명확한 구분  (0) 2026.02.12
[리팩토링] 06. SwiftDataClient 리팩토링 - 중복 코드 제거  (0) 2026.02.12
[리팩토링] 05. Tuist 모듈화 개선  (0) 2026.02.03
[리팩토링] 04. 온보딩 Callback 지옥을 해결해보자 (온보딩 리팩토링).  (0) 2026.01.19
'탭탭 - TapTap/리팩토링' 카테고리의 다른 글
  • [리팩토링] 09. DesignSystem 플랫폼 호환성 문제 해결
  • [리팩토링] 07. SwiftDataClient 리팩토링 - SwiftDataClient의 비대함 완화 및 명확한 구분
  • [리팩토링] 06. SwiftDataClient 리팩토링 - 중복 코드 제거
  • [리팩토링] 05. Tuist 모듈화 개선
여성일
여성일
  • 여성일
    성일노트
    여성일
  • 전체
    오늘
    어제
    • 분류 전체보기
      • Flyleaf - 독서를 여행처럼
        • 리팩토링
        • 트러블슈팅
        • 개발일지
      • 탭탭 - TapTap
        • 리팩토링
        • 트러블슈팅
        • 개발일지
      • 애플 디벨로퍼 아카데미
        • 챌린지 회고
        • 하루의 날씨
      • Swift Student Challenge 202..
      • AI를 잘쓰는 개발자가 될래요
      • 우리 같이 협업하자
      • 사카마카 (살까말까 고민 될 때는 사카마카)
      • Book2OnNon (모바일 서재)
      • 바꿔조 (환율 계산기)
      • iOS
        • iOS
        • Vapor
        • Design Pattern
        • CoreData
        • Tuist
        • RxSwift
        • ReactorKit
        • TCA
      • Swift
        • Swift 기본기
        • UIkit
        • SwiftUI
      • UX, 사용성
      • 원티드 프리온보딩 챌린지 iOS 과정
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.6
    여성일
    [리팩토링] 08. 외부 의존성(LinkNavigator) 제거
    상단으로

    티스토리툴바