-
SwiftUI TCA: Alert (경고창) 만드는 방법공부/Swift(프로그래밍 언어) 2026. 2. 5. 01:50반응형

먼저, SwiftUI 와 TCA가 결합된 프로젝트에서는 경고창을 일반적인 방법으로 만들지 않고, AlertState라는 경고창을 위한 특수 상태를 활용합니다.
AlertState를 사용하는 이유
일반 Alert 대신 AlertState를 사용하는 이유는 아키텍처 일관성, 테스트 가능성, 상태 기반 UI 원칙과 직접적으로 관련되어 있습니다.
1. Alert를 “상태”로 다루기 위함
TCA의 기본 철학은 UI는 상태의 함수라는 점입니다. 일반 SwiftUI Alert는 뷰 레벨에서 비교적 즉흥적으로 선언되는 반면, AlertState는 명확히 도메인 상태의 일부로 존재합니다. 경고창과 관련된 모든 사항이 State에 포함됩니다.
- Alert가 떠 있는지 여부
- Alert의 제목, 메시지
- 어떤 버튼이 있고, 각 버튼이 어떤 액션을 발생시키는지
struct State: Equatable { var alert: AlertState<Action>? }이렇게 되면 Alert 역시 다른 화면 요소들과 동일하게 상태 변화의 결과로 나타나게 됩니다.
2. View와 비즈니스 로직의 분리가 명확
일반 Alert를 사용하면 다음과 같은 문제가 생기기 쉽습니다.
- 버튼 탭 시 클로저 내부에서 직접 로직 처리
- View에서 조건 분기 및 사이드 이펙트 발생
AlertState를 사용하면 Alert의 버튼은 액션만 방출하고, 실제 로직은 Reducer에서 처리됩니다. View는 단순 표현이며, Reducer가 모든 판단과 처리를 담당하는 구조가 유지됩니다.
state.alert = AlertState { TextState("정말 삭제하시겠습니까?") } actions: { ButtonState(role: .destructive, action: .confirmDelete) { TextState("삭제") } ButtonState(role: .cancel) { TextState("취소") } }3. 단방향 데이터 흐름을 유지할 수 있습니다.
TCA는 모든 이벤트가 User Action → Action → Reducer → State → View 의 흐름을 따르기를 요구합니다. 일반 Alert의 버튼 클로저는 이 흐름을 우회하기 쉽지만, AlertState는 버튼 탭조차도 Action으로만 표현됩니다.
이는 숨겨진 사이드 이펙트를 방지하고, 액션 흐름 추적이 용이해지며, 디버깅 및 로그 분석이 용이해집니다.
그 외에
4. 테스트에 알맞음
5. 복잡한 Alert 관리에 유리
등의 이유로 AlertState를 사용하는 것입니다.
얼럿 구현
처음에는 물건 구매 또는 취소를 선택할 수 있는 버튼이 있는 1차 얼럿이 뜨고, [취소]를 누르면 그대로 종료, [처리]를 누르면 구매 완료되었다는 창이 뜹니다.

도메인(피처) 작성
얼럿 창 '내부 버튼'에 대한 액션 추가
먼저, 경고창에서 필요한 액션을 추가합니다. 버튼을 누르는 것은 액션으로 취급합니다. 위의 케이스에서는 OK 버튼에 대한 액션만 필요합니다.
@Reducer struct AlertExampleDomain { @ObservableState struct State: Equatable { // ... // } enum Action { @CasePathable enum Alert { case dismissAlert // 얼럿 [취소] 버튼 누름 case confirmAlert // 얼럿 [처리/확인/제출] 버튼 누름 } }@CasePathable은 열거형(enum)의 case를 “경로(path)”처럼 안전하게 접근·변환할 수 있게 해주는 매크로입니다.
주로 TCA에서 switch 없이도 특정 case만 골라서 다루기 위해 사용됩니다.[iOS/TCA] @CasePathable
TCA Reducer 내부에서 열거형을 일치시킬때 자주 사용된다. enum 타입에서 케이스 매칭을 보다 직관적으로 할 수 있게 도와준다. 또한 상태를 스코프로 다룰 수 있어 오류를 방지할 수 있다. 예시
100percent-me.tistory.com
'얼럿 그 자체'에 대한 상태 및 액션 추가
얼럿에 대한 상태 변수를 추가합니다. 앞서 언급했듯 AlertState라는 특수 상태를 사용합니다.
@ObservableState struct State: Equatable { @Presents var alert: AlertState<Action.Alert>? }@Presents 는 TCA 1.3+에서 alert, sheet, fullScreenCover 같은 뷰 전환 상태를 표현하기 위한 속성 래퍼입니다.
위의 상태를 다루기 위한 액션도 추가합니다.
enum Action { case alert(PresentationAction<Alert>) }PresentationAction은 알림(alert) 등 일시적 상태를 처리할 때 쓰는 구조입니다.
얼럿 창을 열기 위한 버튼 액션 추가
여기서는 버튼을 누르면 얼럿창이 뜨도록 하겠습니다. 이를 위해 액션을 추가합니다.
case alertButtonTapped액션에 대한 리듀서 구현
먼저 다음 3가지 케이스의 리듀서가 필요합니다.
- case .alertButtonTapped: 얼럿 띄우기 버튼이 눌림
- case .alert(.presented(let alertAction)): 얼럿 안의 버튼을 눌렀을 때의 액션을 구현
- case .alert: 얼럿 창에서 그 외의 경우
var body: some Reducer<State, Action> { Reduce { state, action in switch action { case .alertButtonTapped: state.alert = AlertState(title: { TextState("얼럿 제목") }, actions: { ButtonState(role: .cancel, action: .dismissAlert) { TextState("취소") } ButtonState(role: .destructive, action: .confirmAlert) { TextState("처리") } }, message: { TextState("물건을 구매하시겠습니까?") }) return .none case .alert(.presented(let alertAction)): switch alertAction { case .dismissAlert: state.alert = nil return .none case .confirmAlert: state.alert = AlertState(title: { TextState("구매 완료되었습니다.") }) return .none } case .alert: return .none } } .ifLet(\.alert, action: \.alert) .ifLet(\.alert, action: \.alert)- 전체 흐름 요약
- Alert는 UI 이벤트가 아니라 상태 변화의 결과로 표시됩니다.
- Alert 버튼 입력은 다시 액션으로 환원되어 리듀서에서 처리됩니다.
- case .alertButtonTapped:
- 사용자가 “Alert 표시” 버튼을 눌렀을 때 호출되는 액션입니다.
- 이 시점에서 Alert를 띄우기 위해 state.alert에 AlertState를 할당합니다.
- state.alert = AlertState(...)
- Alert의 제목, 버튼, 메시지를 선언적으로 정의합니다.
- state.alert가 nil이 아니게 되는 순간, View에서 Alert가 자동으로 표시됩니다.
- TextState("얼럿 제목")
- Alert 상단에 표시되는 제목 텍스트입니다.
- Text가 아닌 TextState를 사용하는 이유는 TCA의 상태 기반 UI 설계를 따르기 위함입니다.
- ButtonState(role: .cancel, action: .dismissAlert)
- 취소 버튼을 정의합니다.
- 버튼을 누르면 .dismissAlert 액션이 발생합니다.
- ButtonState(role: .destructive, action: .confirmAlert)
- 파괴적(destructive; 삭제, 리셋 등 비가역적 액션) 버튼을 정의합니다.
- 이 예제에서는 실제 역할과 무관하게 단순히 역할 강조를 위해 destructive 역할을 사용했을 뿐입니다.
- 버튼을 누르면 .confirmAlert 액션이 발생합니다.
- 파괴적(destructive; 삭제, 리셋 등 비가역적 액션) 버튼을 정의합니다.
- message: { TextState("물건을 구매하시겠습니까?") }
- Alert 본문 메시지를 정의합니다.
- case .alert(.presented(let alertAction)):
- Alert가 화면에 표시된 상태에서, 사용자가 버튼을 눌렀을 때 들어오는 액션을 처리합니다.
- PresentationAction.presented는 “실제로 Alert가 떠 있는 동안 발생한 액션”임을 의미합니다.
- switch alertAction
- Alert 내부 버튼 액션을 다시 분기 처리합니다.
- 어떤 버튼이 눌렸는지에 따라 서로 다른 후속 동작을 수행합니다.
- case .dismissAlert:
- 취소 버튼을 눌렀을 때 실행됩니다.
- state.alert = nil로 설정하여 Alert를 닫습니다.
- case .confirmAlert:
- 확인(처리) 버튼을 눌렀을 때 실행됩니다.
- 새로운 AlertState를 다시 할당하여 “구매 완료” Alert로 상태를 전환합니다.
- Alert 역시 상태이기 때문에, 다른 Alert로 자연스럽게 교체됩니다.
- case .alert:
- Alert와 관련된 기타 액션(예: dismiss 등)을 포괄적으로 처리합니다.
- 특별한 처리가 필요 없는 경우 .none을 반환합니다.
- .ifLet(\.alert, action: \.alert)
- alert 상태가 존재할 때만 Alert 관련 액션을 리듀서에 연결합니다.
- Alert의 생명주기(표시·해제)와 액션 처리를 안전하게 묶어줍니다.
- @Presents를 사용하는 경우, 실무에서는 거의 필수로 붙이는 패턴입니다.
[참고] 이 코드에서 .ifLet(\.$alert, action: \.alert) 를 안붙여도 얼럿창이 뜨는 이유? ifLet 코드는 어떤 경우 필요?
(김생성씨의 답변 - 더보기를 눌러주세요)더보기이 코드는 겉보기에는 ifLet이 없어도 Alert가 정상 동작하기 때문에 헷갈리기 쉬운 케이스입니다.
결론부터 말씀드리면, “Alert를 띄우는 것”과 “Alert 액션을 안전하게 리듀서로 전달하는 것”은 다른 문제입니다.
왜 .ifLet(\.$alert, action: \.alert) 없이도 Alert가 뜨는가
핵심 이유는 이것입니다.
Alert 표시 자체는 View 레벨에서 State만 보고 결정되기 때문입니다.
현재 코드 구조
@Presents var alert: AlertState<Action.Alert>?그리고 View 쪽에서는 보통 이렇게 연결되어 있습니다.
.alert($store.scope(state: \.alert, action: \.alert))이 구조에서:
- state.alert != nil → SwiftUI가 Alert를 띄움
- 이 과정에는 Reducer의 .ifLet이 전혀 관여하지 않음
즉,
ifLet은 “Alert를 보여주기 위한 조건”이 아니라
“Alert에서 발생한 액션을 어떻게 처리할지”에 관한 것입니다.
그래서 Alert는 뜹니다.
그럼 .ifLet은 도대체 언제 필요한가
ifLet의 진짜 역할
.ifLet(\.$alert, action: \.alert)이 코드는 다음을 보장합니다.
- Alert가 존재할 때만
- Alert에서 발생한 Action을
- 해당 하위 리듀서(또는 PresentationAction 처리 영역)로 전달
즉,
“Alert 수명주기와 액션 처리를 연결하는 장치”입니다.
현재 코드에서는 왜 없어도 문제가 안 생기나
지금 코드를 보면:
case .alert(.presented(let alertAction)): switch alertAction { case .dismissAlert: state.alert = nil case .confirmAlert: state.alert = AlertState(title: { TextState(verbatim: "완료") }) }여기서 중요한 점:
- alert 액션을 상위 Reduce 블록에서 직접 처리
- 별도의 하위 리듀서가 없음
- PresentationAction을 수동으로 switch 처리
즉, 구조적으로:
.ifLet이 해주는 일을 직접 수작업으로 처리하고 있는 상태
그래서 없어도 “동작은” 합니다.
.ifLet이 반드시 필요한 경우
아래 중 하나라도 해당되면 반드시 필요합니다.
1. Alert 전용 하위 리듀서가 있는 경우
@Reducer struct AlertDomain { ... }.ifLet(\.$alert, action: \.alert) { AlertDomain() }→ 없으면 Alert 액션이 어디로도 전달되지 않음
2. Navigation / Sheet / Popover / FullScreenCover
@Presents var destination: Destination.State?.ifLet(\.$destination, action: \.destination)→ 이건 필수 중의 필수
3. Alert 액션을 자동 dismiss 흐름에 맡기고 싶을 때
ifLet을 쓰면:
- Alert가 dismiss되면
- TCA가 자동으로 상태 정리
- case .alert(.dismiss) 같은 boilerplate 감소
4. 테스트 안정성
.ifLet이 없으면:
- Alert가 nil인데도 .alert(...) 액션이 들어오는 구조가 가능
- 테스트에서 불완전한 상태 전이 발생 가능
현재 코드의 구조적 평가
실제 프로젝트 기준으로는:
- Alert가 늘어나면 switch 폭발
- Presentation 로직이 상위 도메인에 섞임
- Navigation 확장 시 구조 변경 불가피
실무 권장 구조
var body: some Reducer<State, Action> { Reduce { ... } .ifLet(\.$alert, action: \.alert) }그리고 Alert 로직은 최대한 분리.
한 줄 요약
- Alert가 뜨는 이유 → View는 state.alert != nil만 본다
- ifLet의 역할 → Alert의 “존재 여부”와 “액션 처리”를 안전하게 연결
- 지금 코드에서 없어도 되는 이유 → Alert 액션을 상위 Reduce에서 직접 처리하고 있기 때문
- 실무에서는? → @Presents가 있으면 거의 항상 .ifLet을 붙인다
SwiftUI 뷰 작성
뷰에서 사용할 주요 스토어(store)를 추가합니다.
import SwiftUI import ComposableArchitecture struct AlertExampleView: View { @Bindable var store: StoreOf<AlertExampleDomain>body에 버튼을 추가합니다. 이 버튼을 누르면 얼럿이 뜹니다.
Button(action: { store.send(.alertButtonTapped) // <- 액션 전송 }) { Label("얼럿창 띄우기", systemImage: "globe") .imageScale(.large) .foregroundStyle(.tint) }주요 View 밑에 Alert modifier(.alert)를 추가해야 합니다. .alert은 다음과 같은 구조로 되어 있습니다.
@MainActor @preconcurrency func alert<ButtonAction>( store: Store<PresentationState<AlertState<ButtonAction>>, PresentationAction<ButtonAction>> ) -> some Viewstore에 얼럿과 관련된 scope를 추가하도록 되어 있으므로, 다음과 같이 추가합니다.
VStack { // ... // } .alert(store: store.scope(state: \.$alert, action: \.alert)) // <- 스코프 추가앞에는 $가 붙는 이유?
앞에 붙은 $는 “단순 값”이 아니라 @Presents가 만들어 준 바인딩용 Projection을 넘기기 때문입니다.
\.alert 은 실제 alert 상태 값, \.$alert 은Alert를 표시·해제하기 위한 Presentation 전용 상태 입니다.
액션에는 이미 Action 자체가 PresentationAction으로 감싸져 있기 때문에 추가적인 Projection이 필요 없습니다.프리뷰 또는 상위 뷰에서 추가할 경우 AlertExampleView에서 Store를 지정하면 됩니다.
#Preview { AlertExampleView( store: Store( initialState: AlertExampleDomain.State(), reducer: { AlertExampleDomain() }) ) }반응형'공부 > Swift(프로그래밍 언어)' 카테고리의 다른 글
Swift iOS: 이미지를 불러오는 Action Extension(액션 확장)에서 스크린샷 이미지를 인식하지 못하는 문제 해결 방법 (0) 2026.02.08 SwiftUI: 탭바가 있는 뷰(TabView) 만들기 (구버전, 신버전) (0) 2026.02.04 SwiftUI TCA: 내비게이션 페이지 이동 (패스, 스택) (0) 2026.01.31 SwiftUI: ‘appendInterpolation’ is deprecated 문제 해결하기 (0) 2026.01.28 Xcode Swift UIKit 프로젝트 스토리보드 없이 만드는 법 (0) 2026.01.24