클로저는 SwiftUI랑 붙어다니는 개념이라 확실히 이해하면 뷰 짜는 게 훨씬 편해진다.
1. 클로저 기본 문법
let greet = { (name: String) -> String in return "Hello, \(name)" } print(greet("James")) - { (매개변수) -> 반환타입 in 코드 }
- 함수랑 똑같지만 이름이 없음 -> 변수에 담아서 쓰거나, 다른 함수에 인자로 전달하는 게 보통
2. 고차함수 예시(map, filter, reduce)
클로저는 컬렉션 다룰 때 특히 많이 쓴다.
- 컬렉션: 데이터를 여러 개 모아둔 자료 구조를 말한다. (Array, Set, Dictionary)
- 고차함수: 함수를 인자로 받거나, 함수를 반환하는 함수
- 함수를 값처럼 다루기에 이렇게 부름
- Swift에서 대표적인 고차함수가 map / filter / reduce
- map: 모든 원소에 같은 동작을 적용한다.
let numbers = [1, 2, 3, 4, 5] let doubled = numbers.map { $0 * 2 } print(doubled) // [2, 4, 6, 8, 10] map은 각 원소를 바꿔서 새로운 배열을 반환
{ $0 * 2 } 이 부분이 클로저 -> 원소 하나하나에 적용된다.
$0, $1의 의미: Swift에서는 클로저를 더 짧게 쓰라고 매개변수 이름을 자동으로 붙여준다. $0은 첫번째 매개변수, $1은 두번째 ...
예를 들어 배열의 map:
let numbers = [1, 2, 3] let doubled = numbers.map { $0 * 2 } - 여기서 { $0 * 2 } 는 사실 이렇게 긴 형태다.
{ (num: Int) -> Int in return num * 2 } - map은 배열을 순서대로 돌면서 각 원소에 클로저를 적용한다.
- $0은 지금 map이 처리중인 현재 원소를 뜻한다.
- filter: 조건에 맞는 원소만 남긴다
let even = numbers.filter { $0 % 2 == 0 } print(even) - 조건을 만족하는 값만 추려내서 새로운 배열 반환
- reduce: 모든 원소를 합쳐 하나의 값으로 줄임
let sum = numbers.reduce(0) { $0 + $1 } print(sum) // 15 - reduce(0) -> 초기값 0
- { $0 + $1 } -> 첫 번째 원소와 더하고, 그 다음 원소와 또 더하고를 반복
- 결국 하나의 값(합계)가 나옴
3. SwiftUI
사실 SwiftUI는 클로저로 뷰와 동작을 연결하는 프레임워크라고 봐도 무방하다.
- Button(action:)
Button("Tap Me") { print("Button tapped!") } - 여기 { print("Button tapped") } 부분이 클로저다.
- 버튼이 눌렸을 때 실행할 동작을 클로저로 전달한 것이다.
- UIKit에서는 target-action 패턴을 썼는데, SwiftUI는 전부 클로저로 해결한다.
- onTapGesture
Text("Hello") .onTapGesture { print("Text tapped!") } - .onTapGesture { ... }도 클로저다.
- 지금 이 순간 "이 뷰가 탭되면 실행할 동작"을 함수로 따로 빼지 않고, 클로저 안에 바로 적는 것이다.
- List + map
let names = ["James", "Alice", "Bob"] List { ForEach(names, id: \.self) { name in Text("Hello, \(name)") } } - ForEach의 마지막 인자가 클로저 { name in Text("") }
- 배열의 각 요소(name)마다 실행돼서 뷰를 반환
- 여기서도 $0으로 축약 가능
ForEach(names, id: \.self) { Text("Hello, \($0)") } [!note] 잘 이해하다가 [[ForEach]]에서 막혔다. 그래서 공부하고 돌아왔다.
- 애니메이션
@State private var isOn = false var body: some View { VStack { Circle() .animation(.easInOut, value: isOn) Button("Toggle") { isOn.toggle } } } - 버튼 안의 { isOn.toggle() } 클로저 -> 상태 변경
- animation은 IsOn 값 변화를 감지하면 뷰 업데이트를 애니메이션으로 감싼다
- 비동기 네트워크 예시
func fetchData(completion: @escaping (String) -> Void) { DispatchQueue.global().async { // 서버 통신했다고 가정 let result = "Data from server" DispatchQueue.main.async { completion(result) // 클로저 실행 } } } struct ContentView: View { @State private var text = "Loading..." var body: some View { Text(text) .onAppear { fetchData { result in text = result } } } } - fetchData는 네트워크 끝났을 때 실행할 동작을 클로저로 전달받음
- .onAppear { ... } 도 클로저
- 여기서 클로저가 없으면 비동기 동작을 표현하기가 거의 불가능
정리
- SwiftUI에서 뷰와 동작을 연결하는 핵심이 크로저
- Button { }, .onTapGesture { }, .onAppear { }, ForEach { } 전부 클로저 기반
- 덕분에 코드가 선언형으로 깔끔해짐
연습
숫자 배열 [1, 2, 3, 4, 5]를 ForEach로 돌려서 버튼 5개 만들고, 버튼 누르면 눌린 숫자를 print하는 뷰
let nums = [1, 2, 3, 4, 5] ForEach(nums, id = \.self) { num in Button("\(num)") { print(num) } } - 고쳐야 할 점이 두 개 있었다.
struct ContentView: View { let nums = [1, 2, 3, 4, 5] var body: some View { ForEach(nums, id: \.self) { num in Button("\(num)") { print(num) } } } } - SwiftUI 뷰니까 View 안에 넣어야 하고, ForEach는 항상 뷰 빌더 안에서 써야 한다.
- id = 이 아니라 id: .self
👉 여기서 연습 더 나아가서, 버튼 누른 숫자를 print 말고 Text로 화면에 표시하는 버전 만들어볼래? (즉, @State 써서 상태 업데이트)
struct ContentView: View { @State var number: Int let nums = [1, 2, 3, 4, 5] var body: some View { ForEach(nums, id: \.self) { num in Button("\(num)") { $number = num } } Text(number) } } - 조금만 더 고쳐보자.
- @State var number -> @State 프로퍼티는 초기값이 필수다.
- 버튼 안에서 numbers를 바꿔야 한다.
- Text는 String을 인자로 받아야 하니까 Text("()") 형태로 감싸야 함.
struct ContentView: View { @State private var number = 0 let nums = [1, 2, 3, 4, 5] var body: some View { ForEach(nums, id: \.self) { num in Button("\(num)") { number = num } } Text("Selected: \(number)") .padding() } } - private 붙이는 이유: Swift 접근 제어자 중 하나인데, "이 속성은 이 파일(struct ContentView)에서만 접근 가능하다"라는 뜻을 가진다. 즉, 다른 파일이나 외부에서 이 프로퍼티를 직접 못 건드리게 막는 것.
- 왜 @State에는 보통 private를 붙일까?
- 뷰 외부에서 바꾸면 안 됨: @State는 SwiftUI가 뷰 내부 상태 관리를 위해 쓰는 거라서 뷰 외부에서 건드리면 SwiftUI의 데이터 흐름 규칙이 깨져버림
- 안전성 + 코드 의도 표현
- private 붙이면 "이건 내부 전용 상태야"라는 신호를 주는 것
- 다른 개발자가 봐도 그렇게 이해하게 됨
- 공식 스타일 가이드 권장
연습문제
숫자 배열 [1, 2, 3, 4, 5]가 있어. 이 배열을 map과 클로저를 사용해서 아래처럼 바꿔라:
["1점수", "2점수", "3점수", "4점수", "5점수"]
let nums = [1, 2, 3, 4, 5] let result = nums.map { "\($0)점수" } print(result) - 와!!! 맞췄따...