PROJECT

SERENADE

사이드 팀 프로젝트 · 웨딩 정보 서비스 · iOS 2인 · App Store 출시

2025.02 – 2025.07

Tuist · ReactorKit + RxSwift · RxDataSources · FlexLayout·PinLayout

결혼 준비의 체크리스트, 예산, 마이페이지를 맡아 ReactorKit 단방향으로 구현했습니다.

설계 포인트

무엇을 결정했나

Case 01 · 단방향

토글은 낙관적으로, 예산은 비관적으로

문제

체크리스트는 항목을 자주 켜고 끄는데, 서버 응답을 기다렸다 반영하면 누를 때마다 한 박자씩 늦게 반응합니다. 반대로 예산은 틀린 금액이 잠깐이라도 보이면 안 됩니다.

접근

같은 ReactorKit 단방향 안에서 흐름별로 전략을 나눴습니다. 토글은 UI를 먼저 반영하는 낙관적 업데이트, 예산은 로딩→서버→재조회의 비관적 업데이트.

핵심 결정

Action→Mutation→State 규칙은 그대로 두고 mutate의 방출 순서만 바꿉니다. 화면마다 다른 요구를 하나의 아키텍처 규칙 안에서 소화했습니다.

FullCheckListReactor.swift
case let .toggleCheckListItem(id, isCompleted):
    // 1) UI 먼저: 목록이 즉시 반응
    let uiUpdate = Observable.just(
        Mutation.updateItem(id, isCompleted))
    // 2) 서버 반영은 뒤에
    let domainUpdate = useCase
        .updateCompletion(id, isCompleted)
    return .concat([uiUpdate, domainUpdate])

// 예산 수정은 반대: 로딩 → 서버 → 재조회
return .concat([setLoading(true), update, refetch()])

Case 02 · 렌더링

섹션 diff로 바뀐 행만 다시 그린다

문제

카테고리 접기와 펼치기, 체크 토글마다 reloadData를 부르면 화면 전체가 깜빡이고 스크롤 위치가 흔들립니다.

접근

RxDataSources AnimatableSectionModel · 셀 모델이 identity와 Equatable을 나눠 구현해, 바뀐 행만 fade로 삽입, 삭제, 갱신됩니다.

핵심 결정

State의 sections를 distinctUntilChanged로 바인딩해 같은 상태가 다시 방출되면 렌더링 자체를 건너뜁니다. 펼친 카테고리는 UserDefaults에 저장해 다음 실행에서 복원됩니다.

CategoryCheckListCellModel.swift
// identity + Equatable: 둘 다 있어야 diff가 정확
struct CellModel: IdentifiableType, Equatable {
    var identity: String { item.id }
    static func == (l: Self, r: Self) -> Bool {
        l.item == r.item
        && l.isChecked == r.isChecked && l.isLast == r.isLast
    }
}

reactor.state.map(\.sections)
    .distinctUntilChanged()
    .bind(to: tableView.rx.items(dataSource: dataSource))