왜 이 내용을 정리하나
Swift에서 클로저는 바깥 변수를 자연스럽게 참조한다. 콜백, 지연 실행, 여러 번 호출되는 핸들러를 만들다 보면 클로저가 실행되는 시점의 값을 읽을지, 클로저를 만든 시점의 값을 가질지 헷갈리는 경우가 많다. 특히 값 타입인 struct는 참조 타입인 class와 다르게 동작하기 때문에, 같은 코드라도 결과가 달라질 수 있다. 버튼을 여러 개 만들면서 루프 안에서 클로저를 등록하거나, 설정값을 담은 struct를 비동기 작업에 넘기는 상황에서 이 차이가 실제 버그로 이어진다.
이 노트에서는 캡처 리스트 [x]를 썼을 때 struct 값이 클로저 생성 시점에 고정되는지를 print로 직접 확인해 본다. 그리고 캡처 리스트가 없을 때와 비교해 차이를 정리해 본다.
개념 설명
클로저는 자신이 사용하는 외부 변수를 캡처한다. 기본 동작은 변수 자체를 참조하는 것이다. 변수가 var이면 클로저는 그 변수의 현재 값을 나중에 읽는다. 즉 클로저를 만든 뒤 변수를 바꾸면, 클로저가 실행될 때 바뀐 값을 보게 된다.
캡처 리스트는 클로저 앞의 대괄호 안에 변수를 적어서 캡처 방식을 명시하는 문법이다. [x]처럼 이름만 적으면 그 순간 x의 값을 복사해서 클로저 내부의 새 상수로 잡는다. 이후 바깥 x가 바뀌어도 클로저 안의 값은 바뀌지 않는다.
struct는 값 타입이다. 대입하거나 함수 인자로 넘길 때 복사본이 만들어진다. 그래서 캡처 리스트로 struct를 잡으면 그 복사본이 고정된다. 반대로 캡처 리스트 없이 var struct를 참조하면 클로저는 바깥 변수를 가리키므로, 나중에 변경된 값을 본다.
정리하면 다음과 같다. 캡처 리스트 없는 클로저는 변수를 참조하고, [x]가 있는 클로저는 생성 시점의 값을 복사한다. struct는 이 복사가 값 전체에 대해 일어나므로, 내부 프로퍼티까지 포함해 생성 시점의 상태가 고정된다.
코드 예제
아래 코드는 struct 하나를 만들고, 캡처 리스트가 없는 클로저와 있는 클로저를 각각 만든 뒤 값을 바꿔서 출력한다.
struct Counter {
var value: Int
var label: String
}
var counter = Counter(value: 1, label: 'first')
// 캡처 리스트 없음: 변수 자체를 참조한다
let plain = {
print('plain 클로저 실행 시점 값: \(counter.value), \(counter.label)')
}
// 캡처 리스트 [x]: 생성 시점의 복사본을 잡는다
let captured = { [counter] in
print('captured 클로저 실행 시점 값: \(counter.value), \(counter.label)')
}
print('--- 클로저 생성 직후 ---')
plain()
captured()
counter.value = 99
counter.label = 'second'
print('--- counter 변경 후 ---')
print('바깥 counter: \(counter.value), \(counter.label)')
plain()
captured()
실행하면 생성 직후에는 두 클로저 모두 value 1, label first를 출력한다. counter를 바꾼 뒤에는 plain 클로저가 value 99, label second를 출력하고, captured 클로저는 여전히 value 1, label first를 출력한다. 즉 [counter]로 캡처한 struct는 생성 시점의 상태에 고정되고, 캡처 리스트가 없는 클로저는 변경된 현재 값을 읽는다.
헷갈리는 점
첫 번째로 헷갈리는 점은 캡처 리스트가 struct 내부의 참조 타입 프로퍼티까지 복사해 주는 것은 아니라는 점이다. struct 안에 class 인스턴스를 담고 있다면, 복사되는 것은 참조값이므로 class 내부 상태는 여전히 공유된다. 값 고정은 struct가 직접 가진 값에 대해서만 성립한다.
두 번째는 let으로 선언한 변수는 캡처 리스트 여부와 상관없이 값이 바뀌지 않으므로, 이 차이가 드러나지 않는다는 점이다. 이 노트의 실험은 var로 선언한 변수에서 의미가 있다.
세 번째는 캡처 리스트에 적은 이름이 클로저 안에서 새 상수처럼 동작한다는 점이다. 클로저 안에서 counter.value = 5처럼 수정하려고 하면 컴파일 에러가 난다. 수정 가능한 복사본이 필요하면 [var counter]처럼 명시해야 한다.
네 번째는 루프 안에서 클로저를 여러 개 만들 때 특히 중요하다는 점이다. 루프 변수를 캡처 리스트로 고정하지 않으면, 모든 클로저가 같은 변수를 보게 되어 의도와 다른 결과가 나올 수 있다.
정리
캡처 리스트 없는 클로저는 바깥 변수를 참조하므로 실행 시점의 값을 읽는다. [x]를 쓰면 생성 시점의 값을 복사해 고정한다. struct는 값 타입이라 이 복사가 struct 전체 상태에 적용되며, 실험에서 확인했듯이 counter를 바꿔도 captured 클로저는 생성 시점의 값을 출력한다. 다만 struct 안의 참조 타입 프로퍼티는 여전히 공유되므로 이 점을 함께 확인해야 한다. 이 노트에서는 print 실험으로 캡처 리스트의 값 고정 동작을 확인하고, 참조 방식과의 차이를 정리해 보았다.
#Swift #클로저 #캡처리스트 #struct #값타입
'Swift' 카테고리의 다른 글
| Swift 공부 노트: mutating 요구사항 프로토콜을 class가 채택할 때의 컴파일 동작 (0) | 2026.10.30 |
|---|---|
| Swift 딕셔너리 subscript로 꺼낸 struct 값 수정과 원본 반영 과정 검증 (0) | 2026.10.30 |
| Swift enum switch에서 default 없이 모든 case를 다루지 않을 때 컴파일 에러 메시지 직접 확인하기 (0) | 2026.10.28 |
| Swift throws 함수를 do-catch로 감싸 에러 케이스별 분기 처리 확인하기 (0) | 2026.10.27 |
| Swift 제네릭 where 절 Equatable 제약 위반 시 컴파일 에러 확인 (0) | 2026.10.26 |