코드네임 :

🍎 3주차 워크북 - List와 Navigation 본문

👥Club/🍀UMC🍀

🍎 3주차 워크북 - List와 Navigation

비엔 Vien 2025. 3. 31. 23:36

List

  • 여러 개의 View를 포함할 수 있는 컨테이너 역할
  • 기본적으로 스크롤 가능
  • ForEach와 함께 사용하여 동적으로 리스트를 구성할 수 있음
  • 각 행별로 자동으로 간격 및 구분선이 적용됨

 


 

ForEach

  • 반복 가능한 데이터를 기반으로 여러 개의 View를 생성
    • 배열(Array), 범위(Range), Identifiable 프로토콜을 따르는 컬렉션을 기반으로 View를 반복 생성할 수 있습니다.
  • List와 함께 사용하면 동적으로 리스트를 구성 가능
    • ForEach를 List 내부에서 사용하면 UITableView(UIkit)와 같은 기능을 구현할 수 있습니다.
    • List가 아닌 VStack, HStack, ScrollView 등과 함께 사용하여 다양한 UI 요소를 만들 수 있습니다.
  • 각 항목을 구별할 수 있도록 id 값을 제공해야 함
    • ForEach는 각 항목을 고유하게 식별해야 하므로 id 값을 요구합니다.
    • String이나 Int 같은 기본 타입을 사용할 때는 id: \.self를 이용할 수 있습니다.
    • 구조체를 사용할 경우 Identifiable 프로토콜을 채택하는 것이 좋습니다.


 

Identifiable 프로토콜을 활용한 데이터 바인딩

SwiftUI에서는 Identifiable 프로토콜을 사용하여 리스트 데이터를 쉽게 관리할 수 있습니다. @State, @Binding, @ObservedObject 등의 프로퍼티 래퍼와 함께 사용하면 동적인 데이터 바인딩을 구현할 수 있습니다

 

1. @State를 활용한 데이터 바인딩

 

2. @Binding을 활용한 데이터 바인딩

import Foundation

struct User: Identifiable {
    let id = UUID()
    var name: String
    var age: Int
}

 


Scroll View

: 내부의 콘텐츠 크기가 화면을 초과하면 자동으로 스크롤 기능을 활성화

Identifiable 프로토콜을 활용하면 SwiftUI의 **List**나 **ForEach**에서 개별 데이터를 고유하게 관리하고, 데이터가 변경될 때도 자동으로 UI가 업데이트되는 장점을 제공합니다. 하지만 List는 기본적으로 내부적으로 스크롤이 적용되어 있으며, 특정한 커스텀 레이아웃을 적용하기 어려울 수도 있습니다.
만약 리스트 아이템을 가로로 배치하거나 더 유연한 스크롤이 필요하다면, **ScrollView**를 활용하는 것이 좋은 선택이 될 수 있습니다.

 

 

수직 스크롤 코드 (기본값)

ScrollView(.vertical) {
    VStack(spacing: 10) {
        ForEach(1..<30) { index in
            Text("Row \(index)")
                .frame(maxWidth: .infinity)
                .background(Color.green.opacity(0.3))
        }
    }
}

 

수평 스크롤 코드 (꼭 HStack을 사용해야함)

ScrollView(.horizontal) {
    HStack(spacing: 15) {
        ForEach(1..<10) { index in
            Text("Item \(index)")
                .frame(width: 100, height: 100)
                .background(Color.purple.opacity(0.3)
        }
    }
}

 

 


스크롤 특정 위치 이동

[ ScrollViewReader를 활용한 특정 위치 이동 ]

여기 버튼 누르면 스크롤 됨

 

[ .scrollPosition()으로 특정 이동 ]
올 2초뒤에 자동으로 25번이 화면 중앙으로 오게끔 그 위치로 넘어감


 

LazyHStack & LazyVStack 

SwiftUI에서는 HStack, VStack을 사용하면 모든 자식 뷰를 한 번에 생성 합니다.
많은 아이템을 포함할 경우, 불필요한 뷰까지 한꺼번에 렌더링하면서 성능 문제가 발생할 수 있습니다.
해결하기 위해 SwiftUI는 LazyHStack, LazyVStack을 제공합니다.  필요할 때만 뷰를 생성하여 성능을 최적화합니다.

 

[LazyHStack]

HStack과 비슷하지만, 보이는 요소만 렌더링하여 성능을 최적화합니다. 가로 스크롤이 필요한 경우에 유용합니다.

import SwiftUI

struct LazyHStackExample: View {

    var body: some View {
        ScrollView(.horizontal, content: {
            LazyHStack(spacing: 15, content: {
                ForEach(1...50, id: \.self) { index in
                    Text("아이템: \(index)")
                        .background(Color.green)
                        .frame(width: 100, height: 100)
                }
            })
        })
    }
}

#Preview {
    LazyHStackExample()
}

 

[LazyVStack]

VStack과 비슷하지만, 보이는 요소만 렌더링하여 성능을 최적화합니다. 스크롤 가능한 컨텐츠에서 효율적인 리스트를 만들 때 사용합니다.

import SwiftUI

struct LazyVstackExample: View {

    var body: some View {
        ScrollView(.vertical, content: {
            LazyVStack(spacing: 15, content: {
                ForEach(1...50, id: \.self) { index in
                    Text("아이템: \(index)")
                        .background(Color.green)
                        .frame(width: 100, height: 100)
                }
            })
        })
    }
}

#Preview {
    LazyHStackExample()
}

 

\

[LazyVStack 과 LazyHStack 함께 사용]

import SwiftUI

struct CombineLazyStack: View {

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 20, content: {
                ForEach(1...10, id: \.self) { rowIndex in
                    VStack(alignment: .leading, content: {
                        Text("섹션 \(rowIndex)")
                            .font(.headline)
                        
                        ScrollView(.horizontal, content: {
                            LazyHStack(spacing: 10, content: {
                                ForEach(1...10, id: \.self) { columnIndex in
                                    Text("아이템 :\(columnIndex)")
                                        .frame(width: 80, height: 80)
                                        .background(Color.blue.opacity(0.3))
                                        .clipShape(RoundedRectangle(cornerRadius: 20))
                                }
                            })
                            .padding(.bottom, 10)
                        })
                    })
                }
            })
        }
    }
}

#Preview {
    CombineLazyStack()
}

 LazyVStack & LazyHStack 사용 시 주의할 점

❗️ LazyVStack과 LazyHStack은 항상 ScrollView와 함께 사용해야 합니다.

LazyVStack과 LazyHStack은 기본적으로 크기를 자동으로 조절하지 않으므로, ScrollView 내에서 사용해야 정상적으로 동작합니다.

❗️ 뷰가 많을 때는 성능이 크게 향상 되지만, 적은 경우에는 큰 차이가 없습니다.

소량의 데이터(예: 5~10개 정도)에서는 일반 VStack과 HStack을 사용해도 큰 차이가 없습니다.

 뷰 크기를 명확하게 설정해야 합니다.

LazyHStack과 LazyVStack은 크기가 자동으로 설정되지 않기 때문에 내부 요소들의 frame(width: height: )를 명확하게 지정하는 것이 중요합니다.


LazyVGrid & LazyHGrid

SwiftUI의 LazyVGrid와 LazyHGrid는 많은 양의 데이터를 효율적으로 표시할 수 있도록 설계된 레이아웃 컨테이너입니다. 기존의 VStack, HStack과 달리 지연 로딩(lazy loading) 방식을 사용하여 화면에 보이는 항목만 렌더링하고, 스크롤 시 필요한 항목을 동적으로 로드합니다. 이를 통해 성능을 최적화할 수 있습니다.

 

 

- 둘의 차이를 보면 columns, rows로 다르게 Grid를 지정합니다.

Grid는 화면에 아이템을 격자 형태로 정렬해서 보여주는 레이아웃

 

[ LazyVGrid ]

**LazyVGrid**는 수직(Vertical) 방향으로 아이템을 배치하므로, 열(columns) 을 정의해야 합니다.

즉, 각 열이 어떻게 배치될지를 결정하고, 각 열 안에서 아이템이 위에서 아래로 쌓이는 구조입니다.

LazyVGrid(columns: [GridItem], content: () -> Content)

 

[ LazyHGrid ]

**LazyHGrid**는 수평(Horizontal) 방향으로 아이템을 배치하므로, 행(rows) 을 정의해야 합니다.

즉, 각 행이 어떻게 배치될지를 결정하고, 각 행 안에서 아이템이 왼쪽에서 오른쪽으로 배치됩니다.

LazyHGrid(rows: [GridItem], content: () -> Content)

 

 

**LazyVGrid**와 **LazyHGrid**의 columns 또는 rows 속성에는 [GridItem] 배열이 필요합니다.

이 배열을 직접 생성하는 것뿐만 아니라 Array(repeating:count:)를 사용하여 동일한 속성의 GridItem을 여러 개 쉽게 생성할 수 있습니다.

GridItem은 SwiftUI에서 그리드 레이아웃(특히 LazyVGrid, LazyHGrid)을 만들 때 각 열(column)이나 행(row)의 크기와 정렬 방식을 정의하는 객체


 

Array(repeating:count:) 

동일한 속성의 GridItem을 반복 생성할 수 있습니다.

가변적인 개수를 지정할 때 유용

Array(repeating: GridItem(크기 설정), count: 개수)

 

 

[LazyVGrid에서]

count를 3으로 지정했으니, 3개의 열로 지정이 되면서 20개의 아이템이 생성됩니다. 또한 .flexible()을 사용했기 때문에 남은 공간을 균등하게 나눠서 열이 배치됩니다.

 

 

[LazyHGrid에서]

rows를 Array(repeating: GridItem(.fixed(80)), count: 2)로 설정하여 고정 높이(80)인 행을 2개 생성합니다.

15개의 아이템이 2행으로 정렬되며, 왼쪽에서 오른쪽으로 가로 스크롤됩니다.

 


[ NavigationStack ]

루트 뷰를 표시하고 루트 뷰 위에 추가 뷰를 표시할 수 있는 뷰.
여러 개의 NavigationLink를 통해 여러 개의 화면을 순차적으로 쌓을 수 있습니다.

  • NavigationStack 내에서 여러 개의 화면을 Push & Pop 방식으로 전환 가능합니다.
  • 상태를 유지하여 사용자가 특정 화면으로 돌아와도 데이터가 사라지지 않습니다.
  • **NavigationPath**를 활용하면 동적인 화면 전환을 구현할 수 있습니다.

>> NavigationLink

탐색 프레젠테이션을 제어하는 뷰

SwiftUI의 NavigationLink는 사용자가 탭하거나 클릭하면 새로운 화면(뷰)으로 이동할 수 있도록 하는 UI 컴포넌트입니다.

 

>> Navigation Path

이 기능의 가장 큰 장점은 navigationDestination(for: )을 사용하여 특정 타입의 데이터를 기준으로 화면을 정의할 수 있다는 점입니다. navigationDestination(for: String.self)을 선언하고 path.append("Profile")을 실행하면, "Profile"이라는 문자열을 인식하여 해당 화면으로 자동으로 전환된다. 이를 활용하면 하나의 코드로 다양한 화면을 연결할 수 있으며, 특히 리스트에서 선택한 항목에 따라 다른 화면을 띄워야 하는 경우 매우 유용합니다.
NavigationPath를 사용하면 뒤로 가기 및 초기화 기능도 쉽게 구현할 수 있습니다. path.removeLast()를 실행하면 가장 최근에 추가된 화면이 제거되면서 이전 화면으로 돌아갑니다.

 

>> NavigationStack을 사용한 화면 닫기(Pop)

화면 전환 후, 특정 버튼을 눌러 이전 화면으로 돌아가려면 dismiss를 사용하면 됩니다.

 


네비게이션 라우터를 활용한 고급 네비게이션 관리

NavigationPath를 사용하면 동적인 화면 전환을 쉽게 관리할 수 있지만, 이를 한 단계 더 발전시키면 네비게이션 라우터 패턴을 적용하여 더욱 체계적으로 화면 이동을 관리할 수 있습니다. 네비게이션 라우터는 각 화면을 데이터 기반으로 관리하고, 특정 경로에 따라 적절한 화면을 연결하는 방식을 의미합니다.


[ 네비게이션 라우터 ]

기본적으로 NavigationPath를 사용하면 사용자가 버튼을 클릭할 때마다 path.append(값)을 통해 특정 화면을 푸시할 수 있습니다. 하지만 프로젝트가 커질수록 네비게이션 경로를 일관되게 관리하기 어려워지고 특정 화면으로 이동해야 하는 상황이 많아질 수 있습니다. 이런 문제를 해결하기 위해 라우터 패턴을 활용하면 네비게이션 경로를 한 곳에서 중앙 집중적으로 관리하면서 코드의 가독성과 유지보수성을 높일 수 있습니다.

  • 네비게이션 경로를 Enum으로 정의하여 보다 직관적으로 화면 이동 가능(Enum 문법은 3주차 문법 워크북을 보고 오시면 이해할 수 있어요!)
  • 특정 로직에서 하나의 경로로 여러 개의 화면을 구성할 수 있습니다.
  • 네비게이션 상태를 ObservableObject로 관리하여 전역적으로 화면 전환이 가능합니다.

 

네비게이션 라우터 활용의 장점

✅ 1. 코드의 가독성이 높아짐

  • Route Enum을 사용하면 이동할 수 있는 화면 목록이 한눈에 정리됩니다.
  • 각 화면 이동 로직을 **NavigationRoute**에서 관리하여 화면 코드가 깔끔해져요!

✅ 2. 화면 이동을 데이터 기반으로 처리 가능

  • 기존의 **NavigationLink**는 뷰가 렌더링될 때마다 실행되지만, **NavigationRouter**는 필요할 때만 동적으로 화면을 추가할 수 있습니다.
  • Route.detail(title: "SwiftUI")와 같이 필요한 데이터를 함께 전달 가능합니다.

✅ 3. 전역 네비게이션 상태 관리 가능

  • NavigationRouter를 ObservableObject로 만들면, 어디서든 화면 이동을 트리거할 수 있습니다.