[SwiftUI] View modifier(onChange, onReceive...

2026. 2. 4. 18:15🍏/Swift

View modifier

계열 대표 API 트리거 기준
값 변화 onChange SwiftUI 상태 비교
이벤트 onReceive Publisher emit
생명주기 onAppear View 등장
비동기 task(id:) id 변경
앱 상태 scenePhase 시스템 상태
입력 onSubmit 유저 입력
레이아웃 Geometry / Preference 레이아웃 변화
제스처 onTap / gesture 사용자 액션

뷰 모디파이어는 위와 같은 API들이 있으며, 이번 포스팅에서는 onChange, onReceive에 대해서 알아보려고 합니다. 


 

onChange의 개념 (SwiftUI 값-기반 변화 감지)

  • 트리거 기준: “이 뷰가 업데이트되는 과정에서” 관찰 중인 value를 다시 읽었을 때, 직전 값과 !=(Equatable 비교) 이면 실행.
  • “값(value)의 변화”에 반응하는 훅

무엇을 감지하나?

  • of:로 넘긴 값 자체의 “이전 값 vs 현재 값”을 비교해서,
  • 달라졌을 때만 action을 실행합니다. (Equatable 기반)

언제 실행되나? (핵심)

  • 값이 바뀌는 “순간”에 즉시 실행되는 게 아니라,
  • SwiftUI가 뷰를 업데이트(=body 재평가)하는 흐름 안에서
    1. 새 값을 읽고
    2. 이전 값과 비교한 뒤
    3. 다르면 실행합니다.

그래서 더 정확한 개념은:

onChange는 “값이 바뀌면”이 아니라 “값이 바뀐 상태로 SwiftUI가 그 변화를 관측할 기회가 생겼을 때” 실행됩니다.

왜 이렇게 설계됐나?

SwiftUI는 UI를 “명령형으로 이벤트 처리”하는 방식이 아니라,

  1. 상태(state)가 바뀌면
  2. 그 결과로 View가 다시 계산되고
  3. UI가 그려지는 구조입니다.

onChange는 이 흐름에 맞춰서 “상태 변화 → 사이드이펙트”를 연결하는 도구입니다.

초기 실행은?

  • iOS 17+의 onChange(of:initial:)에서
    • initial: false(기본) → 처음 나타날 때는 실행 안 함
    • initial: true → 처음 나타날 때도 1회 실행

장점

  • 같은 값 반복이면 실행 안 함 (중복 억제)
  • 값 기반이라 의도가 명확함 (“이 값이 바뀌면 해야 하는 일”)

주의점 (개념적으로 꼭 알아야 하는 2가지)

  1. 이 뷰가 그 값을 “관찰하는 구조”여야 안정적
    • @State, @Binding, @ObservedObject/@StateObject, @Environment
    • → 값 변경이 뷰 업데이트로 이어지는 연결고리가 있어야 함
  2. 값이 큰 타입이면 비교 비용이 커질 수 있음
    • 큰 배열/큰 struct를 그대로 넣으면 ==가 무거워질 수 있음

더 정확한 문장

value 자체가 바뀌는 순간에 바로 실행되는 게 아니라, 그 값이 바뀐 상태로 SwiftUI가 뷰 업데이트를 수행하면서 이전 값과 비교해 “바뀌었네?”가 확인되는 시점에 실행된다.

onChangeSwiftUI의 업데이트 사이클에 종속돼요. (뷰가 업데이트를 안 하면 비교도 안 하고 실행도 안 함)

  • 중복 호출: 같은 값이면 실행 안 함(Equatable 비교니까).
  • 초기 실행: 기본은 처음 등장 때는 실행 안 함. iOS 17+의 initial: true를 켜면 1회 실행.



onReceive의 개념 (Combine 이벤트-기반 수신)

  • 트리거 기준: publisher가 emit(방출) 할 때마다 실행.
  • SwiftUI 업데이트/Equatable 비교 같은 건 상관없고, 이벤트가 오면 무조건 실행(값이 같아도 실행).
  • “이벤트(event stream)”에 반응하는 훅

무엇을 감지하나?

  • Combine Publisher가 emit하는 이벤트를 감지합니다.
  • 이벤트가 오면 매번 action 실행 (값이 같아도 실행)

언제 실행되나?

  • 뷰가 화면에 존재하면서 publisher를 구독하고 있는 동안
  • publisher가 emit하면 실행됩니다.
  • SwiftUI의 body 업데이트와는 독립적입니다.

즉:

onReceive는 “뷰 상태 변화”가 아니라 “외부에서 날아오는 이벤트 스트림”을 받아서 처리하는 도구다.

“처음 등록될 때 1회는 무조건 실행”은 맞나?

반은 맞고 반은 틀려요.

  • onReceive 자체가 그런 규칙이 있는 게 아니라,
  • “publisher 종류”에 따라 다릅니다.

구독하자마자 값 1회 주는 애들

  • CurrentValueSubject
  • @Published의 publisher ($value)
  • → “현재 값”을 들고 있어서 구독 즉시 한 번 보내는 성향이 흔함

구독해도 아무것도 안 주는 애들

  • PassthroughSubject
  • send()가 오기 전까지는 아무 이벤트도 없음

타이머

  • Timer.publish(...).autoconnect()
  • → 즉시 1회가 아니라 “틱 주기”에 따라 들어옴

장점

  • 외부 이벤트를 UI에 연결하기 좋음
  • 뷰 업데이트에 의존하지 않음 (스트림이면 스트림대로 받음)

주의점 (여기가 퍼포먼스/버그의 핵심)

  1. 이벤트 빈도 = 호출 빈도
    • 고빈도 스트림이면 action이 폭주할 수 있음
    • removeDuplicates, throttle, debounce 같은 제어가 필요할 때가 많음
  2. 스레드/액터 컨텍스트
    • publisher가 백그라운드에서 emit하면 action도 그쪽에서 불릴 수 있음
    • UI 상태 변경은 main에서 해야 안전
    • → 필요하면 receive(on:)로 메인에 올림

선택 기준을 한 줄로 정리하면

  • 내가 감지하려는 게 “값의 변화”면 → onChange = “렌더링 중 값이 바뀐 걸 발견하면 반응”
  • 내가 감지하려는 게 “이벤트 스트림”이면 → onReceive = “이벤트 스트림이 오면 즉시 반응”
“원천이 Combine Publisher라면 onReceive가 자연스럽고, SwiftUI 상태/바인딩/환경값이라면 onChange가 자연스럽다.”

 

짧은 예시

✅ onChange가 딱 맞는 예

 

@State var isOn: Bool

Toggle("Auto Upload", isOn: $isOn)
  .onChange(of: isOn) {
     // 토글이 바뀌면 설정 저장
  }

 

✅ onReceive가 딱 맞는 예

.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
    // 앱이 active 될 때마다 새로고침
}