내비게이션이란?
상태 기반 내비게이션의 두 가지 주요 방식인 트리 기반 내비게이션과 스택 기반 내비게이션, 그리고 각 방식의 장단점을 알아봅니다.
개요
상태 기반 내비게이션은 크게 두 가지 범주로 나뉩니다. 하나는 Optional과 enum으로 내비게이션을 모델링하는 트리 기반 방식이고, 다른 하나는 평면 컬렉션으로 내비게이션을 모델링하는 스택 기반 방식입니다. 실제 앱의 내비게이션 대부분은 두 방식을 함께 사용하지만, 도메인을 모델링할 때는 각각의 장단점을 알아두는 것이 중요해요.
내비게이션 정의하기
내비게이션이라는 단어는 사람마다 여러 의미로 사용합니다. 예를 들어 많은 사람은 SwiftUI의 NavigationStack이나 UIKit의 UINavigationController가 제공하는 drill-down 스타일을 내비게이션의 예로 봐요. 하지만 drill-down을 내비게이션으로 본다면 시트와 전체 화면 cover도 내비게이션으로 봐야 합니다.
시트와 cover는 오른쪽에서 왼쪽으로 이동하는 대신 아래에서 위로 애니메이션될 뿐입니다. 이 차이가 본질적인 차이일까요?
시트와 cover가 내비게이션이라면 popover도 당연히 내비게이션입니다. 더 나아가 alert, 확인 대화상자, Apple이 제공하지 않는 사용자 정의 내비게이션까지 포함할 수 있어요.
이 문서에서는 내비게이션을 다음처럼 느슨하게 정의합니다.
정의: 내비게이션은 애플리케이션의 모드가 바뀌는 일입니다.
drill-down, 시트, popover, cover, alert, 대화상자 등 앞에서 살펴본 모든 예는 애플리케이션의 모드 변화입니다.
다만 여기까지는 정의하지 않은 모드 변화라는 말로 내비게이션을 정의했을 뿐입니다. 그래서 다음처럼 더 정의합니다.
정의: 모드 변화는 어떤 상태 조각이 존재하지 않던 상태에서 존재하게 되거나, 반대로 존재하던 상태에서 존재하지 않게 되는 일입니다.
상태 조각이 존재하지 않던 상태에서 존재하게 바뀌면 애플리케이션의 내비게이션과 모드 변화가 일어난 것입니다. 상태가 다시 존재하지 않는 상태로 바뀌면 내비게이션을 되돌리고 이전 모드로 돌아간 것을 뜻합니다.
이는 상태 기반 내비게이션을 아주 추상적으로 설명한 것입니다. 다음 두 섹션에서 상태 기반 내비게이션의 두 주요 형태인 트리 기반 내비게이션과 스택 기반 내비게이션으로 이 개념을 구체화합니다.
트리 기반 내비게이션
앞에서 상태 기반 내비게이션은 상태의 존재 여부로 제어한다고 정의했습니다. 여기서 존재는 아직 정의하지 않았고, 여러 방식으로 정의할 수 있어요. Swift의 Optional 타입으로 상태의 존재와 부재를 나타내기로 하면 이를 트리 기반 내비게이션이라고 부릅니다. 여러 내비게이션 상태를 중첩하면 트리와 같은 구조가 만들어지기 때문입니다.
예를 들어 항목 목록이 있는 인벤토리 기능에서 한 항목을 탭하면 해당 항목의 상세 화면으로 drill-down한다고 해 보겠습니다. 어떤 Optional 상태를 가리키는 Presents() 매크로로 모델링할 수 있어요.
세부 화면 안에는 항목을 시트에서 편집하는 버튼이 있을 수 있습니다. 이 역시 Optional 상태를 가리키는 Presents() 매크로로 모델링할 수 있어요.
또한 edit item 기능 안에는 alert가 표시되는지 나타내는 Optional 상태가 있을 수 있습니다.
이 구조는 앱에 존재하는 내비게이션 계층만큼 계속 이어갈 수 있습니다.
이렇게 모델링하면 앱의 특정 상태로 딥 링크하는 일은 깊게 중첩된 상태 조각을 만드는 일이 됩니다. 예를 들어 특정 항목의 세부 화면까지 drill-down하고, 편집 시트와 alert까지 연 상태로 인벤토리 화면을 시작하려면 그 내비게이션을 나타내는 상태를 만들기만 하면 됩니다.
여기서 이 도메인 모델링 방식의 트리 구조를 확인할 수 있습니다. 앱의 각 기능은 트리의 노드가 되고, 이동할 수 있는 각 목적지는 노드에서 뻗는 가지가 됩니다. 새 기능으로 이동하는 일은 또 하나의 중첩된 상태 조각을 만드는 일에 해당합니다.
이것이 트리 기반 내비게이션의 기본입니다. 앱에서 트리 기반 내비게이션을 구현할 때 TCA가 제공하는 도구는 트리 기반 내비게이션 문서에서 알아보세요.
스택 기반 내비게이션
앞 섹션에서는 Optional 상태로 자식 기능의 표시 여부를 모델링하는 과정을 트리 기반 내비게이션으로 정의했습니다. 이 방식에서는 깊이 중첩된 기능을 깊이 중첩된 상태 조각으로 나타냅니다.
내비게이션을 구동할 상태의 존재와 부재를 모델링하는 또 다른 강력한 도구는 컬렉션입니다. 이 방식은 기능 전체 스택을 데이터 컬렉션으로 나타내는 SwiftUI의 NavigationStack 뷰에서 가장 많이 사용됩니다. 컬렉션에 항목을 추가하면 새 기능을 스택에 push하는 것이고, 컬렉션에서 항목을 제거하면 기능을 스택에서 pop하는 것입니다.
일반적으로는 스택에서 이동할 수 있는 모든 기능을 담는 enum을 정의합니다. 앞 섹션의 예를 이어서, 인벤토리 목록에서 항목의 세부 기능으로 이동한 뒤 편집 화면으로 이동할 수 있다면 다음처럼 나타낼 수 있어요.
그다음 이 상태의 컬렉션이 스택에 표시되는 기능을 나타냅니다.
이 Path 요소 컬렉션은 필요한 길이만큼 만들 수 있습니다. 여러 계층 깊이까지 drill-down한 상태를 나타낼 만큼 길게 만들 수도 있고, 스택의 루트에 있음을 나타내도록 비워둘 수도 있어요.
이것이 스택 기반 내비게이션의 기본입니다. 앱에서 스택 기반 내비게이션을 구현할 때 TCA가 제공하는 도구는 스택 기반 내비게이션 문서에서 알아보세요.
트리 기반과 스택 기반 내비게이션 비교
실제 앱 대부분은 트리 기반 내비게이션과 스택 기반 내비게이션을 섞어 사용합니다. 예를 들어 앱의 루트에서는 NavigationStack으로 스택 기반 내비게이션을 사용하고, 스택 안의 각 기능에서는 시트, popover, alert 등을 표시하기 위해 트리 기반 내비게이션을 사용할 수 있어요. 다만 두 방식에는 각각 장단점이 있으므로 도메인을 모델링할 때 그 차이를 알아두는 것이 중요합니다.
트리 기반 내비게이션의 장점
-
트리 기반 내비게이션은 내비게이션을 매우 간결하게 모델링하는 방식입니다. 앱에서 유효한 모든 내비게이션 경로를 정적으로 설명할 수 있으므로, 앱에서 유효하지 않은 내비게이션을 복원하는 일은 불가능합니다. 예를 들어
detail화면 다음에만edit화면으로 이동하는 것이 타당하다면 세부 기능은 Optional 편집 상태만 보관하면 됩니다.이 구조는 세부 화면에서만 편집 화면으로 이동할 수 있다는 관계를 정적으로 강제합니다.
-
앞 장점과 관련해 트리 기반 내비게이션은 앱이 지원하는 유한한 내비게이션 경로의 수를 표현할 수 있습니다.
-
앱 기능을 모듈화하면 트리 기반 내비게이션 도구로 만든 기능 모듈은 더 독립적인 단위가 됩니다. 따라서 기능용 Xcode preview와 preview 앱이 완전히 동작합니다. 예를 들어 세부 기능의 모든 로직과 뷰를 담은
DetailFeature모듈이 있다면 편집 기능의 도메인이 세부 기능에 직접 포함되므로 preview에서도 편집 기능으로 이동할 수 있어요. -
앞 장점과 관련해 기능이 긴밀하게 통합되므로 기능 통합을 검증하는 단위 테스트를 매우 쉽게 작성할 수 있습니다. 세부 기능과 편집 기능이 어떻게 통합되는지 검증하는 깊이 있고 세밀한 테스트를 작성해, 두 기능이 올바르게 상호작용함을 증명할 수 있어요.
-
트리 기반 내비게이션은 drill-down, 시트, popover, cover, alert, 대화상자 등을 포함한 모든 내비게이션 형태를 하나의 간결한 API 스타일로 통합합니다. 자세한 내용은 트리 기반 내비게이션의 API 통합을 참고하세요.
트리 기반 내비게이션의 단점
-
복잡하거나 재귀적인 내비게이션 경로를 트리 기반 내비게이션으로 표현하는 일은 번거로울 수 있습니다. 예를 들어 영화 앱에서 영화로 이동하고, 영화에 출연한 배우 목록으로 이동하고, 특정 배우로 이동한 뒤 처음 시작한 영화로 다시 이동할 수 있습니다. 이런 구조는 기능 사이에 재귀 의존성을 만들며 Swift 데이터 타입으로 모델링하기 어려울 수 있어요.
-
설계상 트리 기반 내비게이션은 기능을 결합합니다. 세부 기능에서 편집 기능으로 이동할 수 있다면 세부 기능을 컴파일하려면 전체 편집 기능을 컴파일할 수 있어야 합니다. 특히 앱 루트에 가까운 기능은 모든 목적지 기능을 빌드해야 하므로 결국 컴파일 시간이 느려질 수 있어요.
-
과거에는 특히 drill-down 내비게이션에서 트리 기반 내비게이션이 SwiftUI 내비게이션 버그에 더 취약했습니다. 다만 이러한 버그는 iOS 16.4에서 많이 수정됐으므로 요즘은 우려할 정도가 줄었습니다.
스택 기반 내비게이션의 장점
-
스택 기반 내비게이션은 복잡하고 재귀적인 내비게이션 경로를 쉽게 처리할 수 있습니다. 앞에서 살펴본 영화와 배우를 오가는 예도 기능 상태 배열로 간단히 구현할 수 있어요.
영화 기능에서 시작해 영화 기능으로 끝나지만, 이는 단지 평면 배열이므로 실제 재귀는 아닙니다.
-
스택에 들어가는 각 기능은 일반적으로 스택의 다른 모든 화면과 완전히 분리할 수 있습니다. 따라서 기능을 서로 의존하지 않는 별도 모듈에 넣을 수 있고, 다른 기능을 컴파일하지 않고도 컴파일할 수 있어요.
-
SwiftUI의
NavigationStackAPI는 트리 기반 내비게이션에서 사용하는NavigationLink(isActive:),navigationDestination(isPresented:)보다 일반적으로 버그가 적습니다.NavigationStack에도 몇 가지 버그는 남아 있지만, 평균적으로 훨씬 안정적입니다.
스택 기반 내비게이션의 단점
-
스택 기반 내비게이션은 간결한 도구가 아닙니다. 완전히 의미 없는 내비게이션 경로도 표현할 수 있습니다. 예를 들어 세부 화면에서만 편집 화면으로 이동하는 것이 타당하더라도 스택에서는 기능을 역순으로 표시할 수 있어요.
이는 전혀 의미가 없습니다. 편집 화면으로 drill-down한 다음 세부 화면으로 이동한다는 것은 무엇을 뜻할까요? 편집 화면 여러 개를 연달아 push하는 것처럼 다른 의미 없는 경로도 만들 수 있습니다.
앱에서 유한하고 잘 정의된 내비게이션 경로가 필요할 때 이는 스택 기반 방식의 단점입니다.
-
앱을 모듈화하고 각 기능을 별도 모듈에 넣는다면 Xcode preview에서 개별적으로 실행한 기능은 대부분 동작하지 않습니다. 예를 들어 세부 기능에서 편집 기능으로 drill-down하는 버튼은 세부 기능과 편집 기능이 완전히 분리돼 있으므로 Xcode preview에서 동작할 수 없습니다. 따라서 세부 기능의 모든 동작을 Xcode preview에서 검증할 수 없고, 전체 앱을 컴파일하고 실행해야 모든 것을 preview할 수 있습니다.
-
앞 내용과 관련해 여러 기능이 어떻게 통합되는지 단위 테스트하기도 더 어렵습니다. 기능이 완전히 분리돼 있으므로 세부 기능과 편집 기능이 어떻게 상호작용하는지 쉽게 검증할 수 없습니다. 이 테스트를 작성하는 유일한 방법은 전체 앱을 컴파일하고 실행하는 것입니다.
-
마지막으로 스택 기반 내비게이션과
NavigationStack은 drill-down에만 적용되며, 시트, popover, alert 같은 다른 내비게이션 형태는 전혀 다루지 않습니다. 이런 내비게이션을 분리하는 작업은 여전히 직접 해야 합니다.
이제 내비게이션의 기본 용어, 특히 상태 기반 내비게이션을 정의하고, 내비게이션을 트리 기반과 스택 기반이라는 두 범주로 나눴습니다. TCA가 내비게이션을 위해 도메인을 모델링하고 기능을 통합할 때 제공하는 도구는 트리 기반 내비게이션과 스택 기반 내비게이션 문서에서 계속 알아보세요.