PROJECT

SERENADE

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

2025.02 – 2025.07

Tuist · ReactorKit + RxSwift · RxDataSources · FlexLayout·PinLayout

결혼 준비의 체크리스트·예산·마이페이지를 맡아 ReactorKit 단방향으로 구현했습니다. Domain부터 Presentation까지 직접 설계했습니다.

설계 포인트

무엇을 결정했나

설계 01 · 단방향

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

문제

체크리스트는 토글이 잦아 서버 응답을 기다리면 조작감이 죽습니다. 반대로 예산 수정은 틀린 값이 잠깐이라도 보이면 안 됩니다.

접근

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

핵심 결정

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()])

설계 02 · 렌더링

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

문제

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

접근

RxDataSources AnimatableSectionModel · 셀 모델이 행 정체성(identity)과 내용 비교(Equatable)를 나눠 구현해, 바뀐 행만 fade로 삽입·삭제·갱신됩니다.

핵심 결정

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

CategoryCheckListCellModel.swift · diff의 두 기준
// 행 정체성 + 내용 비교: 둘 다 있어야 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))

회고 · 성장 기록

같은 프로젝트 안에서 고쳐 나갔습니다

그때

초기 체크리스트 코드에는 낙관적 업데이트의 서버 실패 롤백이 없었고, Reactor가 State 밖 보조 딕셔너리를 들고 있었고, 화면 간 데이터 전달에 UserDefaults를 썼습니다.

지금 보이는 것

후반에 맡은 마이페이지에서는 모든 상태를 State 안으로 모으고 에러를 타입으로 나눠 처리하는 구조로 정리했습니다. 남은 한계는 그대로 기록해 두었습니다.

배운 것

초기 코드의 한계를 후반 마이페이지에서 스스로 정리한 것처럼, 짧은 참여 기간에도 구조를 남기려 했습니다.

그때의 한계 · 코드가 말해 주는 것
// 1. 낙관적 업데이트에 롤백 경로 없음
//    → 서버 실패 시 화면·저장소 불일치
// 2. Reactor의 State 밖 보조 딕셔너리
//    → 단일 진실 원천이 흔들림
// 3. UserDefaults를 화면 간 전달 버스로 사용
// 후반 마이페이지: 상태는 State 안에, 에러는 타입으로