트렌드브리프

SwiftUI 성능 개선, 뷰 재평가를 줄여 스크롤 끊김 잡기

게시 · 약 6분 분량

핵심 요약 SwiftUI 스크롤 끊김의 주범인 뷰 본문 재평가를 줄이는 법. 에어비앤비 엔지니어링(2025년 6월)과 애플 WWDC 2025 자료를 근거로 diffing 원리, Self._printChanges 진단, @Equatable, 뷰 분할을 정리했다.

목차
  1. SwiftUI는 왜 같은 화면을 다시 그리나
  2. 뷰가 다시 계산되는 이유는 어떻게 찾나
  3. @Equatable로 불필요한 비교 건너뛰기
  4. 큰 뷰 쪼개기와 복잡도 자동 측정
  5. 어디서부터 손대야 할까

SwiftUI 앱이 스크롤에서 뚝뚝 끊긴다면 원인은 대개 뷰 본문(body)이 필요 이상으로 다시 계산되는 데 있다. 2026년 7월 기준으로 이 문제를 가장 구체적으로 다룬 실무 자료는 에어비앤비 엔지니어링 블로그가 2025년 6월 24일 공개한 'Understanding and improving SwiftUI performance'(작성자 Cal Stephens·Miguel Jimenez)와, 애플이 WWDC 2025에서 소개한 새 SwiftUI 프로파일링 도구다. 두 자료가 공통으로 가리키는 해법은 세 가지다. 뷰 본문이 왜 다시 계산되는지 진단하고, Equatable로 불필요한 비교를 건너뛰고, 큰 뷰를 작게 쪼개 바뀐 부분만 갱신하게 하는 것이다.

이 글은 SwiftUI로 iOS 화면을 만드는 개발자를 대상으로, 뷰 재평가와 diffing에 따른 렌더링 성능에 초점을 맞춘다. 레이아웃 계산 내부나 데이터 계층(Core Data 등) 최적화는 다루지 않는다.

SwiftUI는 왜 같은 화면을 다시 그리나

SwiftUI의 화면 갱신은 세 단계로 흐른다. 상태(@State 등)가 바뀌면 그 상태를 쓰는 뷰의 body를 다시 실행하고, 새로 만든 뷰 트리를 이전 것과 비교(diffing)한 뒤, 실제로 달라진 부분만 화면에 반영한다. 문제는 첫 단계다. 상태 하나만 바뀌어도 그와 연결된 뷰의 본문이 통째로 다시 계산되고, 본문이 크고 복잡할수록 이 재평가 비용이 스크롤 한 프레임 안에 다 들어가지 못한다. 여기서 '끊김(히치, hitch)'은 한 프레임을 제때 그리지 못해 화면이 순간 멈칫하는 현상을 말한다.

에어비앤비 엔지니어링 블로그는 SwiftUI의 diffing이 리플렉션(reflection, 실행 중에 타입의 프로퍼티를 들여다보는 기능)으로 저장 프로퍼티를 하나씩 비교하는 방식이라고 설명한다. 비교 방식은 프로퍼티 종류에 따라 달라진다.

프로퍼티 종류SwiftUI의 비교 방식재평가 위험
Equatable 준수 타입직접 정의한 == 결과로 판단낮음
값 타입(struct)저장 프로퍼티를 재귀적으로 비교중간
참조 타입(class)내용이 아니라 포인터 동일성만 비교높음
클로저대개 비교 불가 → 변경으로 간주높음

표에서 보듯 참조 타입과 클로저가 성능의 함정이다. 뷰가 클로저를 프로퍼티로 들고 있으면 SwiftUI는 그 값을 비교하지 못해 '바뀌었다'고 판단하고, 결국 본문을 매번 다시 계산한다. 값 타입(struct)과 참조 타입(class)의 차이가 낯설다면 스위프트 기본 문법을 예제로 정리한 글이 먼저 도움이 된다.

뷰가 다시 계산되는 이유는 어떻게 찾나

원인을 짐작하지 말고 도구로 확인한다. 손이 가장 덜 가는 방법은 본문에 Self._printChanges()를 한 줄 넣는 것이다. 이 정적 메서드는 SwiftUI가 왜 이 뷰의 본문을 다시 요청했는지 콘솔에 출력한다.

struct FlightRow: View {
  let flightNumber: String

  var body: some View {
    let _ = Self._printChanges()
    Text(flightNumber)
  }
}

출력에서 @self는 뷰 값 자체가 바뀌었다는 뜻이고, @identity는 뷰의 정체성(identity)이 바뀌어 SwiftUI가 새 인스턴스로 취급했다는 뜻이며, 프로퍼티 이름이 찍히면 그 프로퍼티 때문에 재평가됐다는 신호다. 다만 이 기능은 디버그 전용이고 최선의 추정치를 보여줄 뿐이라, 릴리스 빌드의 판단 근거로 쓰지 않는다.

더 넓게 보려면 Instruments를 쓴다. 애플은 WWDC 2025 세션 'Optimize SwiftUI performance with Instruments'에서 Instruments 26에 새로 들어간 SwiftUI 전용 계측기를 소개했다. 이 도구는 SwiftUI가 실제로 일한 구간을 'Update Groups' 레인으로 보여주고, 오래 걸린 본문 갱신(Long View Body Updates)을 히치 유발 가능성에 따라 주황·빨강으로 구분해 표시한다. 함께 제공되는 'Cause & Effect Graph'는 어떤 Observable이나 Environment 값이 그 갱신을 촉발했는지 의존 관계를 시각화한다. 이 계측기가 현재 SwiftUI 병목을 짚는 가장 구체적인 공식 도구다.

@Equatable로 불필요한 비교 건너뛰기

재평가가 잦은 뷰를 찾았다면, SwiftUI에 "이 프로퍼티들이 그대로면 본문을 다시 만들지 마라"고 직접 알려줄 수 있다. 뷰를 Equatable에 맞추고 ==를 구현한 뒤 부모에서 .equatable() 수식어를 붙이면 된다. 애플 개발자 문서는 equatable()이 새 값이 이전 값과 같을 때 자식 뷰의 갱신을 막는다고 설명한다.

struct FlightRow: View, Equatable {
  let flightNumber: String
  let onTap: () -> Void  // 클로저는 == 비교에서 제외

  static func == (lhs: FlightRow, rhs: FlightRow) -> Bool {
    lhs.flightNumber == rhs.flightNumber
  }

  var body: some View {
    Button(flightNumber) { onTap() }
  }
}

// 부모 뷰에서 .equatable()로 커스텀 비교를 강제한다
FlightRow(flightNumber: "KE001", onTap: handleTap)
  .equatable()

핵심은 비교에서 뺄 것을 고르는 일이다. 위 예제처럼 클로저(onTap)를 == 비교에서 제외해야, 부모가 매번 새 클로저를 넘겨도 본문이 다시 계산되지 않는다. 에어비앤비는 이 작업을 손으로 반복하지 않으려고 @Equatable 매크로를 만들었다. 이 매크로는 뷰의 저장 프로퍼티를 비교하는 ==를 자동 생성하되 @State·@Environment처럼 SwiftUI가 관리하는 프로퍼티는 빼고, 비교에 넣으면 안 되는 프로퍼티에는 @SkipEquatable을 붙여 제외한다고 밝혔다.

주의할 점이 있다. ==를 실행하는 비교 자체도 공짜가 아니다. 비교 비용이 본문 재계산 비용보다 커질 수 있으므로, 자주 다시 그려지면서도 비교할 프로퍼티가 단순한 뷰에만 골라서 적용한다.

큰 뷰 쪼개기와 복잡도 자동 측정

Equatable로도 안 되는 경우가 있다. 본문 하나가 너무 많은 것을 그리면, 그중 무엇 하나만 바뀌어도 전체가 다시 계산된다. 해법은 큰 본문을 여러 작은 뷰로 나누는 것이다. 그러면 SwiftUI가 바뀐 하위 뷰만 골라 갱신하므로, 기능이 커져도 재평가 범위가 화면 전체로 번지지 않는다.

문제는 뷰가 언제 '너무 커졌는지' 사람이 눈으로 놓치기 쉽다는 점이다. 에어비앤비는 이를 자동화하려고 SwiftSyntax(스위프트 소스 코드를 추상 구문 트리로 파싱하는 도구)를 이용한 커스텀 SwiftLint 규칙을 만들었다고 설명한다. 이 규칙은 본문이 계산 프로퍼티·함수·클로저로 뷰를 조합할 때마다 복잡도 점수를 올리고, 점수가 임계값(에어비앤비는 최대 10으로 설정)을 넘으면 Xcode에서 경고를 띄운다.

에어비앤비는 자주 재평가되는 뷰에 @Equatable을 적용하고 큰 본문을 쪼갠 결과, 메인 검색 화면의 스크롤 끊김이 15% 줄었다고 밝혔다. 특정 화면 한 곳의 수치이지만, 어떤 개선이 어느 정도 효과로 이어지는지 가늠하는 참고점이 된다.

어디서부터 손대야 할까

순서는 진단이 먼저다. 무작정 모든 뷰에 .equatable()을 붙이면 비교 비용만 늘 수 있다. 먼저 Self._printChanges()나 Instruments의 SwiftUI 계측기로 '자주, 오래' 재평가되는 뷰를 특정한다. 그다음 그 뷰가 큰 본문 탓이면 작은 뷰로 쪼개고, 불필요한 프로퍼티 비교(특히 클로저·참조 타입) 탓이면 Equatable로 비교 대상을 좁힌다. 원문의 세 가지 기법과 코드 예제는 에어비앤비 엔지니어링 블로그 글WWDC 2025 세션에서 확인할 수 있다.

더 많은 글 보기 RSS 구독