← 모든 글

@State 매크로: Xcode 27이 더 이상 컴파일하지 않는 것

Apple의 iOS 27 릴리스 노트는 @State 항목을 SwiftUI가 iOS 13부터 지고 온 버그 설명으로 시작합니다.3 “초기값으로 표현식을 지정한 @State는 뷰 구조체가 다시 인스턴스화될 때마다 그 표현식을 매번 평가했습니다. @State private var model = Model()의 경우, 뷰의 생애 전체에 걸쳐 Model.init()이 여러 번 호출된다는 뜻입니다.”1

해결책은 재작성입니다. “Xcode 27은 이 반복 평가를 피하는 새로운 @State 구현을 도입합니다. 이 새로운 동작은 iOS 17 계열 OS까지 소급 적용됩니다. 새 @State는 Swift 매크로로 구현되었습니다. 프로퍼티 래퍼 버전과 소스 호환성이 대체로 유지되지만, 몇 가지 예외가 있습니다.”1

이 문장에서 방아쇠가 무엇인지 읽어내야 합니다. Apple이 지목한 것은 Xcode이고, 배포 타깃이 아닙니다.

요약

  • Xcode 27은 @State를 Swift 매크로로 다시 구현하며, Apple의 심볼 페이지는 그 전환을 양방향으로 못 박습니다. State 구조체 페이지는 “Xcode 27 이상으로 빌드하면 시스템이 State() 매크로를 대신 사용합니다”라고 적고, State() 매크로 페이지는 “Xcode 26 이하로 빌드하면 시스템이 State 프로퍼티 래퍼를 대신 사용합니다”라고 적습니다.23
  • 배포 타깃으로는 매크로를 빠져나갈 수 없습니다. 매크로는 자신이 대체한 프로퍼티 래퍼와 동일한 iOS 13.0 가용성을 갖고 있고, 전환의 기준은 타깃이 아니라 Xcode 버전입니다. 런타임 쪽 절반은 “iOS 17 계열 OS”까지만 소급 적용되므로, iOS 15나 16을 지원하는 프로젝트는 런타임 동작 없이 컴파일 시점 변경만 받습니다.
  • 값을 조용히 버리는 동작은 예전부터 있었고 바뀌지 않았습니다. Apple은 그 동작이 “매크로 때문에 바뀐 것은 아니지만, 그런 사례 일부는 더 이상 컴파일되지 않습니다”라고 적습니다.1 매크로는 이니셜라이저의 값을 삼켜 버리던 버그를 빌드 실패로 바꿔 놓습니다.
  • 컴파일이 깨지는 지점은 두 가지 패턴입니다. 선언부에 초기값이 있는 @State 프로퍼티에 이니셜라이저에서 값을 대입하는 경우, 그리고 모든 멤버가 private인 구조체에 대해 컴파일러가 자동 생성해 주는 멤버와이즈 이니셜라이저를 익스텐션에서 호출하는 경우입니다.1 Apple이 쓴 표현은 “그런 사례 일부”이고 전부가 아니며, 어느 경우인지는 끝까지 열거하지 않습니다.
  • 이 글을 쓰기 전에 실제 배포 중인 앱 네 개를 감사했습니다. @State 선언 267개, 그중 선언부 초기값을 가진 것 201개, 두 패턴에 해당하는 사례 0개, 아슬아슬하게 비껴간 사례 1개, 그리고 macOS에서 거짓 “이상 없음”을 만들어 내는 grep 관용구 하나가 나왔습니다.6

이 항목은 Xcode 27 릴리스 노트가 아니라 iOS 및 iPadOS 27 릴리스 노트의 SwiftUI 섹션에 실려 있고, Xcode 27 릴리스 노트에는 대응하는 항목이 없습니다.7 Apple 스스로 Xcode에 귀속시킨 변화치고는 이상한 자리이며, 이 소식이 사람들에게 늦게 도착할 이유로도 충분합니다.

Apple이 고친 성능 버그

예전 동작에 대한 Apple의 설명은 릴리스 노트치고는 이례적으로 직설적입니다. Model.init()이 “뷰의 생애 전체에 걸쳐 여러 번 호출된다”는 것입니다.1 SwiftUI는 뷰 구조체를 끊임없이 다시 인스턴스화하고, 그때마다 등호 오른쪽의 표현식이 다시 평가되었습니다. 이미 존재하는 상태가 우선하므로 SwiftUI는 그 결과를 버렸지만, 계산은 그대로 수행됐습니다.

매크로 페이지는 새 계약을 한 문장으로 정리합니다. “State() 프로퍼티는 SwiftUI가 뷰를 처음 인스턴스화할 때 기본값을 인스턴스화합니다.”2

Apple의 SwiftUI 업데이트 페이지에는 릴리스 노트가 빠뜨린 한정 조건이 붙어 있는데, 계획을 세울 때 정작 중요한 것이 그 한정 조건입니다. “App, Scene, View에서 @State 속성이 State() 매크로로 상태 값을 만들도록 하려면 프로젝트를 Xcode 27 이상에서 빌드하세요. 이 변경은 프로퍼티가 클래스일 때만 초기화와 저장을 한 번으로 만듭니다.”4

“클래스일 때”라는 조건은 이득의 범위를 상당히 좁히지만, 동시에 요즘 SwiftUI 코드베이스에 넘쳐 나는 패턴을 정확히 가리킵니다. @Observable 객체를 @State에 담는 방식은 Apple이 문서로 권장하는 접근이고, 매크로 페이지의 예제 자체가 @Observable class Library를 바로 그렇게 보관합니다.2 구조체 이니셜라이저는 대개 비용이 크지 않습니다. 저장소를 열고, 쿼리를 시작하고, 옵저버를 등록하는 클래스 이니셜라이저는 그렇지 않은데, Apple은 그것이 “여러 번” 실행된다고 설명합니다.1

실제로 바뀐 것과 바뀌지 않은 것

Apple은 의미론과 컴파일 동작을 한 문장에 담았고, 그 두 절은 서로 반대 방향을 가리킵니다. “@State 선언에 초기값을 지정하고 이니셜라이저에서 같은 프로퍼티에 값을 대입하려 하면, 이니셜라이저의 값은 버려집니다. 이 동작은 매크로 때문에 바뀐 것은 아니지만, 그런 사례 일부는 더 이상 컴파일되지 않습니다.”1

코드의 의미는 하나도 바뀌지 않았습니다. 프로퍼티 래퍼 시절에도 선언부에 값이 있는 @State 프로퍼티에 대입하는 이니셜라이저는 아무 일도 하지 않았고, 아무 말 없이, 깨끗하게 컴파일됐습니다. 매크로는 규칙은 그대로 두고 그 침묵만 걷어냅니다.

사라진 실패 양상은 값비싼 쪽이었습니다. 개발자가 이니셜라이저를 작성하고, 그리로 제목을 넘기고, 화면에 엉뚱한 제목이 그려지는 것을 보고, 뷰 body를 뒤지기 시작합니다. 컴파일러는 처음부터 알고 있었지만 그 사실을 말할 방법이 없었습니다.

Apple의 예제에는 그 요점을 담은 주석 두 줄이 들어 있습니다.

struct StickerPageView: View {
    @State private var page = StickerPage()
    let title: String

    init(title: String) {
        // `title` won't have any effect
        // this also won't compile with @State macro
        self.page = StickerPage(title: title)
        self.title = title
    }
}

“아무 효과도 없다”와 “컴파일되지 않는다”가 나란한 두 줄에 놓여 있습니다. 앞 줄은 Xcode 26을, 뒤 줄은 Xcode 27을 설명합니다. 같은 코드, 같은 의미, 다른 판정입니다.

고치는 방법은 선언부의 초기값을 지우는 것입니다.

struct StickerPageView: View {
    @State private var page: StickerPage // no initial value expression
    let title: String

    init(title: String) {
        self.page = StickerPage(title: title) // works!
        self.title = title
    }
}

Apple은 이 규칙을 지시문 하나로 줄입니다. “이니셜라이저로 초기값을 대입할 때는 @State 선언에 초기값을 지정하지 마세요.”1

그러니 정확한 요약은 매크로가 이니셜라이저 대입을 깨뜨렸다는 것이 아닙니다. 이니셜라이저 대입은 이미 깨져 있었고, 매크로는 그 사실을 처음으로 소리 내어 말하는 존재입니다. 이 변화를 퇴행으로 규정하는 사람은 방향을 반대로 읽은 것입니다. 다만 릴리스 브랜치가 감당해야 하는 실무적 결과는 똑같습니다. 어제까지 통과했던 빌드가 오늘 실패합니다.

한 가지 유보는 계속 안고 가야 합니다. Apple이 쓴 표현은 “그런 사례 일부”이고 전부가 아니며, 어느 경우인지는 한 번도 열거하지 않습니다. Xcode 27에서 빌드가 깨끗하게 통과했다는 사실은 규칙에 대한 증명이 아니라 여러분 코드에 대한 증거일 뿐입니다.

자동 생성 이니셜라이저가 사라진다

두 번째 예외는 초기값과 아무 관련이 없고, 호출 지점에 @State가 한 번도 등장하지 않는 코드를 걸고 넘어집니다.

“구조체의 모든 저장 멤버가 private이면, 컴파일러는 같은 타입의 익스텐션에서 사용할 수 있는 private init을 자동 생성합니다.”1

struct StickerPageView: View {
    @State private var page: StickerPage
    private let title: String
    ...
}

extension StickerPageView {
    init(title: String, _ page: StickerPage) {
        self.init(page: page, title: title) // using the synthesized init
    }
}

“state 매크로는 이 자동 생성 이니셜라이저를 비활성화합니다. 그래서 위 코드는 더 이상 컴파일되지 않습니다. 이를 완화하려면 멤버에 값을 명시적으로 대입하세요.”1

extension StickerPageView {
    init(title: String, _ page: StickerPage) {
        self.title = title
        self.page = page
    }
}

이 패턴은 첫 번째보다 찾아내기가 훨씬 고약합니다. 깨지는 호출 지점이, 원인이 되는 @State와는 다른 선언에 들어 있기 때문입니다. @State를 grep해도 드러나지 않습니다.

Apple은 결과만 적고 거기서 멈춥니다. 공개된 매크로 선언은 그 증상과 앞뒤가 맞습니다.

@attached(accessor, names: named(init), named(get), named(set))
@attached(peer, names: prefixed(`_`), prefixed(`__`), prefixed(`$`))
macro State()

getset을 제공하는 접근자 매크로는 컴파일러가 보기에 그 프로퍼티의 정체를 바꿔 놓고, 멤버와이즈 이니셜라이저 자동 생성은 저장 프로퍼티를 근거로 삼습니다.2 이 선언을 원인으로 읽는 것은 Apple의 주장이 아니라 저의 추론이며, 완화 방법은 메커니즘과 무관합니다. 대입문을 손으로 적어 주면 됩니다.

제네릭 추론과 프로퍼티 래퍼 조합

남은 예외 둘에 Apple이 할당한 분량은 각각 한 문장씩입니다. 많은 프로젝트를 건드릴 사안은 아니지만, 마이그레이션 점검 목록에는 각각 한 줄을 남겨 둘 만합니다.

제네릭 추론은 이 항목 안에서 가장 모호하게 처리됩니다. “드문 상황에서는 매크로 구현이 @State의 제네릭 인자를 자동 추론하는 데 있어 유연성이 떨어집니다. 타입을 더 구체적으로 적으세요.”1 Apple은 어떤 경우인지 이름을 대지 않고, 찾아볼 진단 메시지도 알려 주지 않습니다. 완화 방법은 선언에 타입을 명시적으로 붙이는 것이고, 컴파일러가 불평하는 곳마다 적용하면 됩니다.

조합에 대해서는 Apple이 파손이 아니라 한계로 서술합니다. “@State를 다른 프로퍼티 래퍼나 매크로와 조합하는 것은 지원되지 않습니다.”1 직접 만든 프로퍼티 래퍼 안에 @State를 감싼 적이 있다면 확인해 볼 일이고, Apple이 이것을 예전에는 되던 기능이 아니라 애초에 지원 범위 밖이라고 규정했다는 점도 기억해 둘 만합니다.

어떤 배포 타깃으로도 빠져나갈 수 없다

Apple의 세 페이지에서 뽑은 세 문장이 모든 탈출로를 막습니다.

State() 매크로 심볼의 가용성은 iOS 13.0, iPadOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, visionOS 1.0부터이며, 이는 그것이 대체한 프로퍼티 래퍼와 정확히 일치합니다. 그러니 최소 지원 버전을 내려도 매크로를 피할 수 없습니다.23 그리고 전환의 기준은 타깃이 아니라 컴파일러입니다. “Xcode 27 이상으로 빌드하면 시스템이 State() 매크로를 대신 사용합니다.”3

이 변화의 런타임 절반에는 컴파일 시점 절반에 없는 하한선이 있습니다. Apple은 새 동작이 “iOS 17 계열 OS까지 소급 적용된다”고 적는데, 그 아래로는 빈틈이 남습니다. iOS 15나 16을 배포 타깃으로 삼은 프로젝트는 컴파일 시점에 매크로를 받고 그와 함께 소스 호환성 예외까지 받지만, 소급 적용된 런타임 동작은 받지 못합니다.1 그 타깃들이 대신 무엇을 받는지는 Apple이 말하지 않습니다. iOS 17보다 오래된 OS를 지원한다면, 빌드가 깨지는 것은 확정으로, 반복 초기화 문제의 해결은 가장 오래된 기기에서 미확인으로 놓고 계획하세요.

Xcode 27로 프로젝트를 열고 빌드하면 그것이 곧 새 @State입니다. 이 결정에는 Info.plist 키도, 빌드 설정도, 가용성 검사도 개입하지 않습니다.

27 주기에는 개발자들이 계속 한 항목으로 묶어 정리하는 파괴적 변화가 셋 있는데, 터지는 시점은 서로 다릅니다. 런치 스크린 요구사항은 “27.0 SDK 이상으로 빌드한 앱”을 묶고, 대가는 App Store 심사 반려입니다. 씬 생명주기 의무화는 “최신 SDK로 빌드한” 앱을 묶고, 대가는 실행되지 않는 앱입니다. @State 매크로는 Xcode 버전을 묶고, 대가는 빌드입니다. Apple은 앞의 둘을 SDK 기준으로, 세 번째를 툴체인 기준으로 표현했고, 그래서 순서상 @State가 가장 먼저입니다. plist 키나 배포 타깃을 건드리기도 전에, 첫 빌드에서 만나게 됩니다.

그 툴체인을 열어야 하는 압력은 공개된 일정표를 따라 도착하지만, iOS 27용 일정은 아직 없습니다. Apple의 요구사항 페이지는 현재 이렇게 적혀 있습니다. “2026년 4월 28일부터 App Store Connect에 업로드하는 앱은 iOS 26, iPadOS 26, tvOS 26, visionOS 26, watchOS 26용 SDK를 사용해 Xcode 26 이상으로 빌드해야 합니다.”5 iOS 27 SDK에 대응하는 날짜는 Apple이 아직 발표하지 않았습니다. Apple은 최근 몇 해 봄마다 SDK 최소 버전을 올려 왔으므로, 27 주기의 기한도 사실이 아니라 합리적인 예상으로만 두는 것이 맞습니다. 다른 곳에서 읽은 구체적인 날짜는 모두 추측입니다.

실제 배포 중인 앱 네 개에는 무엇이 있었나

남의 코드를 이야기하기 전에 제 코드부터 감사했습니다. App Store에 올라가 있는 앱 네 개, 전부 SwiftUI, 전부 현재 Xcode 26.6으로 빌드하고 있습니다.6

Swift 파일 @State 선언 선언부 초기값 있음 @State를 선언한 타입
Reps 77 120 83 31
Return 57 57 42 14
Ace Citizenship 26 63 54 11
Banana List 55 27 22 8
합계 215 267 201 64

명령 두 개로 작업 범위가 좁혀집니다. 첫 번째는 초기값을 가진 @State 선언을 찾고, @State 뒤의 문자 클래스는 제 몫을 합니다. [^A-Za-z0-9_]@StateObject를 결과에서 걸러 주는데, @State만 그대로 검색하면 그게 안 됩니다.

grep -rn --include="*.swift" \
  --exclude-dir=.build --exclude-dir=DerivedData --exclude-dir=build \
  -E '@State[^A-Za-z0-9_][^=]*[^!<>=]=[^=]' .

이 명령은 속성과 선언이 같은 줄에 있다고 가정합니다. private var page = StickerPage() 위에 @State를 따로 한 줄로 적은 프로퍼티는 아무것도 걸리지 않는데, 이는 아래에서 설명할 거짓 “이상 없음”과 옷만 다른 같은 실패입니다. 걸리지 않은 것은 “깨끗하다”가 아니라 “아직 확인하지 않았다”로 읽으세요.

두 번째는 이니셜라이저까지 함께 선언한 파일로 범위를 좁힙니다. 첫 번째 예외가 물 수 있는 곳은 거기뿐입니다.

find . -name "*.swift" -not -path "*/build/*" -not -path "*/DerivedData/*" -print0 |
while IFS= read -r -d '' f; do
  grep -qE '@State[^A-Za-z0-9_][^=]*[^!<>=]=[^=]' "$f" || continue
  grep -qE '^[[:space:]]*(private |public |internal |fileprivate )?init[[:space:]]*[(<]' "$f" || continue
  echo "$f"
done

이 루프는 xargs로 파이프하는 것보다 무거워 보이지만, macOS에서는 그 무게만큼의 값을 합니다. BSD grep-Z에 대해 GNU grep처럼 NUL 구분자를 내보내지 않으므로, grep -rlZ ... | xargs -0 grep -l은 두 번째 grep에 줄바꿈으로 이어 붙인 덩어리 하나를 넘깁니다. 경로에 공백이 들어간 프로젝트, 예컨대 Banana List 같은 경우, 표준 오류로는 “No such file or directory”가 화면을 채우고 표준 출력에는 아무것도 나오지 않는데, 이건 감사가 깨끗하게 끝난 것과 똑같이 읽힙니다.6 빈 출력을 “일치 없음”으로 읽으면, 정작 검사가 가장 필요한 프로젝트에서 답을 거꾸로 얻게 됩니다.

Swift 파일 215개 가운데 후보로 남은 것은 20개였습니다. Reps 9개, Return 6개, Ace Citizenship 3개, Banana List 2개입니다. 20개를 모두 손으로 읽었고, 네 프로젝트 모두 결과가 같았습니다.

  • Apple이 제시한 첫 번째 형태, 즉 선언부 초기값과 같은 타입의 이니셜라이저 안에서 같은 프로퍼티에 대입하는 조합: 0건.
  • 두 번째 예외, 즉 @State를 선언한 구조체의 자동 생성 멤버와이즈 이니셜라이저를 익스텐션에서 호출하는 경우: 0건.
  • 조합 예외, 즉 한 선언에 @State와 다른 프로퍼티 래퍼나 매크로를 함께 붙인 경우: 0건.

아슬아슬하게 비껴간 사례 하나는 따로 설명할 만합니다. Apple이 지우라고 말한 형태에서 한 줄 거리에 있기 때문입니다. Reps의 프로필 설정 뷰는 @State private var healthWriteStatus: HealthAuthorizationState = .notRequested를 선언하고, 그 이니셜라이저 안에서 self._healthWriteStatus = State(initialValue: ...)를 대입합니다. 두 곳 모두 값을 갖고 있으니 Apple의 완화 문장이 글자 그대로 적용됩니다. @State 선언에 초기값을 지정하지 마세요.1 다만 이 대입은 Apple 예제의 래핑된 값 형태가 아니라 밑줄 형태를 쓰고 있고, Apple의 릴리스 노트는 밑줄 형태를 한 번도 언급하지 않습니다.

그래서 제 감사로는 답할 수 없는 질문이 남습니다. _x = State(initialValue:) 관용구는 네 앱 중 세 앱에 걸쳐 15번 등장하고, 이니셜라이저 파라미터로 상태의 초기값을 심는 표준적인 방법으로 여전히 쓰입니다. Apple의 항목은 이 형태를 다루지 않습니다. 매크로 선언이 밑줄로 시작하는 피어를 생성하기는 하지만,2 그건 시사일 뿐 보장은 아닙니다. 저는 Xcode 26.6(빌드 17F113)에서 빌드하므로 이 관용구도, 그 결과로 나올 컴파일 오류의 문구도 확인할 수 없었습니다.6 Xcode 27 베타를 가진 사람이라면 둘 다 오후 한나절에 정리할 수 있습니다. 누군가 그걸 해내기 전까지는, 오류 문자열이 아니라 선언 형태를 기준으로 코드를 검색하세요.

선언 267개에서 나온 정직한 결론은, SwiftUI 코드 대부분이 손댈 것 없이 통과한다는 것이고, 이는 Apple의 “소스 호환성이 대체로 유지된다”는 표현과 맞아떨어집니다.1 위험한 뷰는 손으로 쓴 이니셜라이저를 가진 쪽이며, 이들은 아주 좁게 뭉쳐 있습니다. 네 코드베이스를 합쳐 Swift 파일 10개 중 1개도 안 되는 수만이 손으로 쓴 이니셜라이저와 자기 초기값을 가진 @State를 동시에 갖고 있었습니다.

자주 묻는 질문

_page = State(initialValue:) 호출을 바꿔야 하나요?

Apple의 항목은 말하지 않습니다. 밑줄 형태는 래핑된 값이 아니라 투영된 저장소에 직접 대입하는데, 릴리스 노트에는 이 형태가 어디에도 없습니다. initialValue도, 밑줄 예제도, 어느 방향의 언급도 없습니다.1 공개된 매크로 선언이 _로 시작하는 피어를 만들어 내기는 하지만, 그건 시사일 뿐 보장은 아닙니다.2 제가 감사한 네 앱 중 셋이 이 관용구를 쓰고 총 15번 등장하니, 문서로 명시된 두 예외보다 더 많은 코드베이스에 걸린 문제입니다.6 Apple이 이 부분을 정리해 주기 전까지는, 추측으로 리팩터링하지 말고 프로젝트를 Xcode 27에서 빌드해 컴파일러가 답하게 하세요.

이니셜라이저에서 대입한 값의 운명이 매크로 때문에 달라졌나요?

아닙니다. Apple은 값을 버리는 동작이 “매크로 때문에 바뀐 것은 아니지만, 그런 사례 일부는 더 이상 컴파일되지 않습니다”라고 분명히 적습니다.1 Xcode 26에서도 선언부에 이미 값이 있는 @State 프로퍼티에 대입하는 이니셜라이저는 아무 일도 하지 않고 컴파일됐습니다. Xcode 27에서는 그중 일부가 빌드에 실패합니다. 의미론은 제자리에 있고 진단만 새로 도착한 것이니, 여기서 빌드가 깨진다면 그건 이미 갖고 있던 버그를 컴파일러가 알려 주는 것입니다.

내 프로젝트에서 위험한 코드를 어떻게 찾나요?

초기값을 가진 @State 선언을 검색하고, 결과를 이니셜라이저까지 선언한 파일로 좁힌 뒤, 그 후보 목록을 손으로 읽으세요. @StateObject는 문자 클래스(@State[^A-Za-z0-9_])로 걸러 내고, macOS에서는 grep -rlZ | xargs -0보다 find -print0 루프를 쓰세요. BSD grep은 NUL 구분자를 내보내지 않고, 공백이 든 경로는 통과처럼 보이는 빈 출력을 만들어 냅니다.6 앱 네 개와 Swift 파일 215개에서 후보는 20개 파일로 좁혀졌습니다. 뷰 구조체에 self.init(...)을 호출하는 익스텐션은 따로 검색하세요. 두 번째 예외는 그 원인이 되는 @State 근처에 아무 흔적도 남기지 않습니다.

앱이 실제로 빨라지나요?

특정한 조건에서만 그렇고, Apple은 그 조건을 릴리스 노트보다 더 좁게 잡습니다. 릴리스 노트는 반복 평가를 피한다고 일반적으로 서술하지만,1 SwiftUI 업데이트 항목은 이 변경이 “프로퍼티가 클래스일 때만 초기화와 저장을 한 번으로 만든다”고 적습니다.4 이득이 생기는 자리는 @State private var model = SomeObservableClass()이고, 예전 구현은 뷰가 다시 인스턴스화될 때마다 그 클래스 이니셜라이저를 실행하고 결과를 버렸습니다. @State private var isPresented = false는 애초에 문제가 아니었습니다.

핵심 정리

iOS 개발자에게: - 찾아야 할 형태는 하나가 아니라 둘입니다. 첫 번째는 이니셜라이저 옆의 @State 선언에 있고, 두 번째는 저장 멤버가 모두 private인 뷰 구조체에 self.init(...)을 호출하는 익스텐션에 있습니다.1 - 첫 번째는 이니셜라이저의 대입을 지우지 말고 선언부의 초기값을 지워서 고치세요. Apple의 지시는 이니셜라이저를 유일한 출처로 남기고 선언부는 비워 두라는 것입니다.1

오래된 SwiftUI 코드를 유지 보수하는 팀에게: - 검토 부담은 코드베이스 전체가 아니라 후보 목록에 맞춰 배분하세요. 초기값을 가진 @State와 손으로 쓴 이니셜라이저를 함께 가진 파일은 제 네 앱에서 215개 중 20개였고, 첫 번째 패턴의 모든 사례는 반드시 그 안에 있습니다. 두 번째 패턴은 익스텐션에 숨으니 따로 검색하세요.6 - 깨진 빌드는 발견된 버그로 받아들이세요. 컴파일러가 이제 거부하는 그 이니셜라이저 값을 SwiftUI는 이미 버리고 있었으니, 이 변화가 무는 자리마다 어떤 동작은 이미 틀려 있었습니다.1

릴리스 담당자에게: - @State 감사는 같은 주기의 SDK 기반 작업보다 앞에 배치하세요. 런치 스크린 키씬 생명주기는 둘 다 빌드에 사용한 SDK를 기준으로 묶지만, @State는 빌드에 사용한 Xcode를 기준으로 묶으므로 가장 먼저 도착합니다.13 - iOS 27 SDK 기한을 전제로 계획하지 마세요. Apple이 공개한 최소 요건은 여전히 iOS 26 SDK이고, 2026년 4월 28일부터 적용 중이며, 27에 대해 Apple이 발표한 것은 아무것도 없습니다.5


한 주기에 강제 지점 셋이 함께 들어오고, 실패하는 자리는 셋 다 다릅니다. 런치 스크린 키는 제출을 막고, 씬 의무화는 실행을 막고, @State 매크로는 빌드를 막습니다. 같은 SDK에 함께 들어오는 나머지 변화는 iOS 27 SwiftUI의 새로운 기능에서 확인하세요. 시리즈 전체는 Apple 생태계 시리즈에 모여 있습니다.

참고 문헌


  1. Apple, iOS & iPadOS 27 Release Notes, SwiftUI 섹션, New Features(radar 105893279). 예전 동작 설명(“초기값으로 표현식을 지정한 @State는 뷰 구조체가 다시 인스턴스화될 때마다 그 표현식을 매번 평가했습니다. @State private var model = Model()의 경우, 뷰의 생애 전체에 걸쳐 Model.init()이 여러 번 호출된다는 뜻입니다”), 새 구현(“Xcode 27은 이 반복 평가를 피하는 새로운 @State 구현을 도입합니다. 이 새로운 동작은 iOS 17 계열 OS까지 소급 적용됩니다. 새 @State는 Swift 매크로로 구현되었습니다. 프로퍼티 래퍼 버전과 소스 호환성이 대체로 유지되지만, 몇 가지 예외가 있습니다”), 첫 번째 예외와 그 지시문(“@State 선언에 초기값을 지정하고 이니셜라이저에서 같은 프로퍼티에 값을 대입하려 하면, 이니셜라이저의 값은 버려집니다. 이 동작은 매크로 때문에 바뀐 것은 아니지만, 그런 사례 일부는 더 이상 컴파일되지 않습니다”, “이니셜라이저로 초기값을 대입할 때는 @State 선언에 초기값을 지정하지 마세요”), 첫 번째 예외에 딸린 StickerPageView 코드 예제 두 개, 두 번째 예외(“구조체의 모든 저장 멤버가 private이면, 컴파일러는 같은 타입의 익스텐션에서 사용할 수 있는 private init을 자동 생성합니다”, “state 매크로는 이 자동 생성 이니셜라이저를 비활성화합니다. 그래서 위 코드는 더 이상 컴파일되지 않습니다. 이를 완화하려면 멤버에 값을 명시적으로 대입하세요”)와 그에 딸린 코드 예제 두 개, 제네릭 추론 노트(“드문 상황에서는 매크로 구현이 @State의 제네릭 인자를 자동 추론하는 데 있어 유연성이 떨어집니다. 타입을 더 구체적으로 적으세요”), 조합 노트(“@State를 다른 프로퍼티 래퍼나 매크로와 조합하는 것은 지원되지 않습니다”)의 출처입니다. 모든 코드 예제는 원문 그대로 옮겼습니다. HTML 페이지는 JavaScript를 통해 내용을 렌더링하므로, 2026년 7월 25일 Apple 문서 JSON으로 확인했습니다. 

  2. Apple, State() macro, SwiftUI 매크로 레퍼런스. 공개된 선언(@attached(accessor, names: named(init), named(get), named(set)), @attached(peer, names: prefixed(_), prefixed(__), prefixed($)), macro State()), 가용성 목록(iOS 13.0, iPadOS 13.0, Mac Catalyst 13.0, macOS 10.15, tvOS 13.0, visionOS 1.0, watchOS 6.0), 툴체인 관련 부기(“Xcode 26 이하로 빌드하면 시스템이 State 프로퍼티 래퍼를 대신 사용합니다”), 새 초기화 계약(“State() 프로퍼티는 SwiftUI가 뷰를 처음 인스턴스화할 때 기본값을 인스턴스화합니다”), 그리고 @Observable class Library@State에 담는 “Store observable objects” 예제의 출처입니다. 

  3. Apple, State, SwiftUI 구조체 레퍼런스. 여전히 @frozen @propertyWrapper struct State<Value>로 선언되어 있고, 가용성은 iOS 13.0, iPadOS 13.0, Mac Catalyst 13.0, macOS 10.15, tvOS 13.0, visionOS 1.0, watchOS 6.0부터입니다. 반대 방향을 가리키는 툴체인 부기의 출처입니다. “Xcode 27 이상으로 빌드하면 시스템이 State() 매크로를 대신 사용합니다.” 

  4. Apple, SwiftUI Updates, 2026년 6월, General. 클래스 한정 조건의 출처입니다. “App, Scene, View에서 @State 속성이 State() 매크로로 상태 값을 만들도록 하려면 프로젝트를 Xcode 27 이상에서 빌드하세요. 이 변경은 프로퍼티가 클래스일 때만 초기화와 저장을 한 번으로 만듭니다.” 

  5. Apple, Upcoming requirements, Apple Developer News. 현재 SDK 최소 요건의 출처입니다. “2026년 4월 28일부터 App Store Connect에 업로드하는 앱은 iOS 26, iPadOS 26, tvOS 26, visionOS 26, watchOS 26용 SDK를 사용해 Xcode 26 이상으로 빌드해야 합니다.” 2026년 7월 25일 확인했으며, 이 페이지에는 iOS 27 SDK를 언급하는 요건이 하나도 없습니다. 

  6. 2026년 7월 25일, macOS 26.5.2와 Xcode 26.6(빌드 17F113) 환경에서 실제 배포 중인 SwiftUI 앱 네 개(Reps, Return, Ace Citizenship, Banana List)를 대상으로 저자가 직접 감사했습니다. 집계는 각 타입 선언을 중괄호 깊이로 파싱하고 그 안에서 이니셜라이저 본문의 범위를 잡는 스크립트에서 나왔으며, 위에 실은 grep 명령과 교차 검증한 결과 네 프로젝트의 선언부 초기값 개수(83, 42, 54, 22)를 정확히 재현했습니다. BSD grep 동작은 직접 확인했습니다. macOS에서 grep -rlZ는 NUL 대신 줄바꿈으로 구분된 출력을 내보내므로 xargs -0은 이어 붙은 인자 하나를 받게 되고, 공백이 든 경로에서는 파이프라인이 “No such file or directory”로 실패합니다. _x = State(initialValue:) 관용구 개수(Reps, Return, Banana List에 걸쳐 15회)도 같은 검사에서 나왔습니다. Xcode 27에서의 동작은 테스트하지 않았고, 사용한 머신에 Xcode 27이 설치되어 있지 않았으므로 컴파일 오류 문구도 여기 적지 않았습니다. 

  7. Apple, Xcode 27 Release Notes. 2026년 7월 25일에 radar 105893279와 @State 매크로를 설명하는 항목을 모두 검색했지만 어느 것도 나오지 않았습니다. 유일한 @State 언급은 무관한 MusicKit 수정(radar 176947544)입니다. 

관련 게시물

Xcode 27, ld64 제거와 고유 모듈 이름 요구

Xcode 27은 ld64 링커를 제거하고 Clang 모듈 이름이 고유할 것을 요구합니다. 두 변경 모두 툴체인 업그레이드 시점에 터집니다. 프로젝트 7개 감사 결과와 직접 검증한 명령어를 정리했습니다.

17 분 소요

iOS 27 런치 스크린 규칙: 네 개의 키냐, 리젝이냐

iOS 27 SDK로 빌드한 앱은 런치 스크린을 선언해야 하며, 그렇지 않으면 App Store에서 리젝됩니다. 네 개의 키, 그리고 생성된 plist를 쓰는 타깃을 점검하는 방법입니다.

10 분 소요