티스토리 뷰

 

NavigationStack에서 화면 전환을 구현할 때 일반적으로 NavigationLink를 많이 사용하는데, 이 컴포넌트는 전환 대상인 뷰를 미리 생성하는 방식이다. 아직 화면 전환을 하지 않았더라도 다음 뷰가 이미 메모리에 로드된 상태가 된다.

 

내가 구현한 화면에서는 전환 대상 뷰 내부에 무거운 연산과 네트워크 요청이 포함되어 있었고, 이로 인해 초기 진입 시 뷰 빌드에 시간이 오래 걸리며 성능 저하가 발생하는 문제가 있었다. 실제로 화면 전환을 하지 않아도 해당 뷰가 생성되기 때문에 불필요한 자원 낭비가 발생했던 것이다.

 

이러한 문제를 해결하기 위해, 해당 뷰가 실제로 필요할 때에만 생성되도록 LazyView를 적용하게 되었다.

NavigationLink의 동작 방식에 대해 이해하고 LazyView를 적용하는 방법에 대해 알아보자.

📝 목차

1. NavigationLink의 동작 방식
2. LazyView 적용
3. Instruments로 메모리 성능 테스트
4. LazyView 사용 기준 정리

1. NavigationLink의 동작 방식

 

 

화면 1, 2, 3, 4 있다고 생각해 보자. 

1화면인 경우 NavigationLink를 클릭하지 않아도 2는 생성되어 있는 상태가 된다.

그럼 2화면으로 넘어가면 3화면도 생성되는 방식일까?

 

=> 맞다! 즉, NavigationLink의 destination View를 미리 생성한다고 이해하면 된다.

 

 

메인화면인 ContentView를 만들어보자.

struct ContentView: View {

    var body: some View {
        NavigationStack{
            
            Text("This is the Main View")
                .font(.largeTitle)
            
            NavigationLink(destination: FirstView()){
                Text("Next View")
            }
        }
    }
}

 

그리고 이동할 여러 뷰도 만들자.

struct FirstView: View {
    init() {
        print("FirstView initialized")
    }
    
    var body: some View {
       Text("This is the First View")
            .font(.largeTitle)
        
        NavigationLink(destination: SecondView()){
            Text("Next View")
        }
    }
}

struct SecondView: View {
    init() {
        print("SecondView initialized")
    }
    
    var body: some View {
       Text("This is the Second View")
            .font(.largeTitle)
        
        NavigationLink(destination: ThirdView()){
            Text("Next View")
        }
    }
}

struct ThirdView: View {
    init() {
        print("ThirdView initialized")
    }
    
    var body: some View {
        Text("This is the Third View")
            .font(.largeTitle)
    }
}

 

코드를 테스트해 보자.

처음 Main View에서 Navigation Link를 클릭하지 않아도 First View가 생성된 걸 확인할 수 있다.

 

 

 

다음으로 First View로 이동한 경우 Second View가 생성되는 걸 확인할 수 있다.

 

 

여기까지 잘 이해했다면 Navigation Link가 destination의 뷰를 미리 생성한다는 거는 알 수 있을 것이다!

 

그럼 여기서 드는 의문점!!! 미리 뷰가 생성되는 것이 뭐가 어때서...??

 

🧐 Navigation Link의 미리 뷰를 생성하는 방식이 문제가 되는가?

 

간단한 프로젝트나 가벼운 뷰라면 NavigationLink가 다음 화면을 미리 생성하는 방식이 크게 문제 되지 않을 수 있다.

하지만 다음 화면의 뷰가 네트워크 요청을 포함하거나 무거운 연산을 수행하는 경우, 현재 뷰에서 그 화면까지 미리 생성하는 것은 불필요한 자원 낭비로 이어질 수 있다.

 

예를 들어, 현재 보여지는 뷰 자체도 생성 비용이 높은 경우, 아직 사용자가 이동하지도 않은 화면까지 함께 초기화된다면 전반적인 렌더링 성능이 저하될 수 있다.
이는 사용자의 체감 속도에 영향을 주고, 특히 여러 NavigationLink가 있는 복잡한 뷰에서는 성능 저하가 더욱 두드러질 수 있다.

정리하면 다음과 같은 문제가 있다.

 

  1. 뷰 생성 비용이 큰 경우
    • 예: 네트워크 요청, 무거운 초기 연산이 뷰의 init이나 onAppear에 들어있는 경우
    • → 아직 클릭하지도 않았는데 불필요한 리소스 소모 발생
  2. 의도하지 않은 사이드 이펙트
    • 예: 뷰가 생성되면서 API 호출이 일어나거나 상태가 바뀌는 경우
    • → 사용자가 화면 전환을 하지 않아도 내부적으로 동작이 실행됨
  3. 메모리 낭비
    • 여러 개의 NavigationLink가 존재하고, 각각 복잡한 destination 뷰를 가진다면?
    • → 화면에 보이지도 않는 뷰들이 계속 메모리에 올라와 있는 셈

 


2. LazyView 적용

 

LazyView를 적용하면 NavigationLink는 더이상 destination의 뷰를 미리 생성하지 않는다고 한다.

진짜로 생성하지 않는지 확인해 보자.

 

struct LazyView<Content: View>: View {
    let build: () -> Content
    init(_ build: @escaping () -> Content) {
        self.build = build
    }
    var body: Content {
        build()
    }
}

 

위에서 만든 LazyView로 destination의 뷰를 감싸주면 끝이다.

NavigationLink(destination: LazyView{FirstView()}){
    Text("Next View")
}

 

 

그럼 MainView에서도 더이상 다음 뷰인 FirstView를 생성하지 않는다 ㅎㅎ

 

 

그리고 FirstView에서도 다음 화면인 SecondView를 생성하지 않는다.

 

 

 


3. Instruments로 메모리 성능 테스트

 

LazyView가 있는지 여부에 따라서 메모리에서도 변화가 감지되고 있는지 확인하고 싶어서 Instruments로 테스트해 봤다.

 

cmd + i로 실행하면 Instruments가 나오는데 Time Profiler를 선택하면 메모리 테스트를 할 수 있다.

차이를 좀 명확하게 확인하고 싶어서 무거운 HeavyView를 추가했다.

 

struct HeavyView: View {
    init() {
        print("HeavyView init at: \(Date())")
        heavyComputation()
    }

    func heavyComputation() {
        // 일부러 CPU 점유
        for _ in 0...1_000_000 {
            _ = UUID().uuidString
        }
        // 렌더링 느리게 만들기
        Thread.sleep(forTimeInterval: 1.0)
    }

    var body: some View {
        Text("Heavy View Loaded")
            .padding()
    }
}

 

그리고 FirstView에서 LazyView 있을 때와 없을 때를 나눠서 NavigationLink를 사용했다.

 

struct FirstView: View {
    init() {
        print("FirstView initialized")
    }
    
    var body: some View {
       Text("This is the First View")
            .font(.largeTitle)
        
        NavigationLink(destination: HeavyView()){
            Text("미리 생성됨")
        }
        
        NavigationLink(destination: LazyView{HeavyView()}){
            Text("Lazy로 생성")
        }
    }
}

 

아래는 Time Profiler로 테스트한 결과다!

 

 

1. LazyView를 사용하지 않은 경우

 

LazyView를 사용하지 않은 경우, FirstView에서 SecondView로 넘어갈 때 이미 HeavyView가 미리 생성되어 있기 때문에, FirstView가 로드되는 시점에 HeavyView의 생성 비용까지 함께 발생한다. 이로 인해 Time Profiler에서 Hangs 구간이 FirstView 진입 시점에 나타나게 된다.

 

=> FirstView 초기 진입할 때 Hangs 시간은 약 0.1초에서 1.2초로 증가, 대신 SecondView로 이동할 때 0.1초의 Hangs가 발생하는 구조로 바뀌었다.

 

2.  LazyView를 사용한 경우

 

반면, LazyView를 사용한 경우, SecondView로 이동하는 NavigationLink를 실제로 클릭했을 때에만 HeavyView가 생성된다. 따라서 Hangs 시간은 버튼 클릭 시점에만 발생하고, 초기 진입 시에는 별다른 성능 저하 없이 부드럽게 화면이 로드된다.

 

=> FirstView 초기 진입할 때  Hangs 시간은 약 1.2초 → 0.1초로 감소, 대신 HeavyView로 이동할 때 1.2초의 Hangs가 발생하는 구조로 바뀌었다.

 

 

🧐 Hangs가 뭐지??

 

"Hangs"는 앱이 사용자 입력에 응답하지 못할 정도로 멈춰 있는 시간을 의미한다.

즉, 메인 스레드가 일정 시간 이상 바쁘게 돌아가면서 사용자 이벤트(터치, 스크롤 등)를 처리하지 못하는 상태를 말한다.

 


4. LazyView 사용 기준 정리

 

 

내가 생각하는 LazyView를 사용할지 말지에 대한 판단 기준은 다음과 같다.

 

✅ LazyView 사용 판단 기준

 

이렇게 2가지를 충족한다면 LazyView를 사용하는 것을 추천한다.

 

1. 뷰가 무겁다 (생성 비용이 높다)

2. 뷰를 당장 보여줄 필요가 없다 (사용자가 직접 진입해야 하는 뷰다)

 

해당 기준은 내가 프로젝트에서 LazyView를 적용할지 말지에 대한 기준으로 틀릴 수 있다!

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함