코드네임 :
🍎 3주차 워크북 - List와 Navigation 본문
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로 만들면, 어디서든 화면 이동을 트리거할 수 있습니다.
'👥Club > 🍀UMC🍀' 카테고리의 다른 글
🍏 on시리즈 모디피어 (0) | 2025.04.01 |
---|---|
🍎 3주차 실습 (0) | 2025.04.01 |
🍎 3주차 문법 - 딕셔너리와 세트(Set), 열거형(enum) (0) | 2025.03.26 |
🍎 2주차 문법 - 연산자, 조건문과 반복문, 배열 그리고 옵셔널 (0) | 2025.03.26 |
🍎 2주차 실습 (0) | 2025.03.25 |