Skip to content

Reimos7/EyeMonDoctor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

27 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

EyeMonDoctor App Icon

EyeMonDoctor

์•ˆ๊ณผ ์˜๋ฃŒ์ง„ ์ „์šฉ iPad ์•ฑ โ€” ํ™˜์ž์™€์˜ ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…, ์ด๋ฏธ์ง€ยทPDF ํŒŒ์ผ ๊ณต์œ , VOD ์ŠคํŠธ๋ฆฌ๋ฐ์ด ๊ฐ€๋Šฅํ•œ ์˜๋ฃŒ์ง„ ์ „์šฉ ๋น„๋Œ€๋ฉด ์ง„๋ฃŒ ์†”๋ฃจ์…˜


Screenshots

๋กœ๊ทธ์ธ (๊ฐ€๋กœ) ๋กœ๊ทธ์ธ (์„ธ๋กœ)
Simulator Screenshot - iPad Pro 11-inch (M5) - 2026-02-08 at 22 07 31 Simulator Screenshot - iPad Pro 11-inch (M5) - 2026-02-08 at 22 08 27
ํšŒ์›๊ฐ€์ž… (๊ฐ€๋กœ) ํšŒ์›๊ฐ€์ž… (์„ธ๋กœ)
Simulator Screenshot - iPad Pro 11-inch (M5) - 2026-02-08 at 22 07 45 Simulator Screenshot - iPad Pro 11-inch (M5) - 2026-02-08 at 22 08 12
์ฑ„ํŒ… (๊ฐ€๋กœ) ์ฑ„ํŒ… (์„ธ๋กœ)
Simulator Screenshot - iPad Pro 11-inch (M5) - 2026-02-08 at 23 06 27 Simulator Screenshot - iPad Pro 11-inch (M5) - 2026-02-08 at 23 06 19
ํ”„๋กœํ•„ (๊ฐ€๋กœ) ํ”„๋กœํ•„ (์„ธ๋กœ)
Simulator Screenshot - iPad Pro 11-inch (M5) - 2026-02-08 at 22 10 29 Simulator Screenshot - iPad Pro 11-inch (M5) - 2026-02-08 at 22 10 16

Tech Stack

๋ถ„๋ฅ˜ ๊ธฐ์ˆ  ์„ ํƒ ์ด์œ 
UI SwiftUI (iPad ์ „์šฉ) NavigationSplitView ๊ธฐ๋ฐ˜ 2-Column ๋ ˆ์ด์•„์›ƒ, ์ขŒ์ธก ์ฑ„ํŒ… ๋ชฉ๋ก + ์šฐ์ธก ์ฑ„ํŒ… ์ƒ์„ธ ๋™์‹œ ํ‘œ์‹œ
Architecture TCA (The Composable Architecture) @Reducer ๋งคํฌ๋กœ ๊ธฐ๋ฐ˜ ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„, @Dependency๋กœ ์™ธ๋ถ€ ์˜์กด์„ฑ ์ฃผ์ž… ๋ฐ Mock ๊ต์ฒด
Network URLSession + async/await Alamofire ์—†์ด ์ˆœ์ˆ˜ URLSession, APIRouter enum ์—”๋“œํฌ์ธํŠธ ์ •์˜ + APIClient ์š”์ฒญยทํ† ํฐ ๊ฐฑ์‹ 
Realtime Socket.IO Namespace ๊ธฐ๋ฐ˜(/chats-{roomId}) ์ฑ„ํŒ…๋ฐฉ๋ณ„ ๋…๋ฆฝ ์—ฐ๊ฒฐ, AsyncStream ๋ž˜ํ•‘์œผ๋กœ TCA Effect ํ†ตํ•ฉ
Auth ์ด๋ฉ”์ผ ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ JWT ํ† ํฐ Keychain ์ €์žฅ, actor ๊ธฐ๋ฐ˜ AuthInterceptor๋กœ ๋™์‹œ ๊ฐฑ์‹  ์š”์ฒญ ์ง๋ ฌํ™”
Media AVPlayer (HLS) m3u8 ๊ธฐ๋ฐ˜ ์ŠคํŠธ๋ฆฌ๋ฐ, ํ”„๋กœํ•„ ํ™”๋ฉด ์ธ๋ผ์ธ VOD ์žฌ์ƒ
Storage Realm + Keychain ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ๋กœ์ปฌ ์บ์‹ฑ(Realm), ํ† ํฐ ๋ณด์•ˆ ์ €์žฅ(Keychain), 3-Stage Loading ์ „๋žต
Image Kingfisher ImageDownloadRequestModifier ๊ธฐ๋ฐ˜ ์ธ์ฆ ํ—ค๋” Extension ํ†ต์ผ, ๋‹ค์šด์ƒ˜ํ”Œ๋ง ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™”
Testing Swift Testing + TCA TestStore @Test ๋งคํฌ๋กœ ๊ธฐ๋ฐ˜ 10๊ฐœ Unit Test, withDependencies Mock ์ฃผ์ž… + LockIsolated Concurrency ์•ˆ์ „ ๊ฒ€์ฆ

Architecture

1. TCA ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„

%%{init: {'flowchart': {'htmlLabels': false, 'useMaxWidth': false}} }%%
flowchart TD
    View["SwiftUI View"]
    Action["Action enum"]
    Reducer["Reducer @Reducer"]
    State["State @ObservableState"]

    View -->|"์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ"| Action
    Action --> Reducer
    Reducer -->|"์ƒํƒœ ๋ณ€๊ฒฝ"| State
    State -->|"UI ๋ฐ”์ธ๋”ฉ"| View

    Reducer -.->|"@Dependency"| APIClient["APIClient"]
    Reducer -.->|"@Dependency"| SocketClient["SocketClient"]
    Reducer -.->|"@Dependency"| KeychainClient["KeychainClient"]
    Reducer -.->|"@Dependency"| RealmClient["RealmClient"]
Loading

View๋Š” State๋งŒ ์ฝ์–ด ํ™”๋ฉด์„ ๊ทธ๋ฆฌ๊ณ , ์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ๋ฅผ Action์œผ๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. Reducer๊ฐ€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๊ณ  State๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉด, SwiftUI๊ฐ€ ์ž๋™์œผ๋กœ UI๋ฅผ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ์™ธ๋ถ€ ์˜์กด์„ฑ์€ @Dependency๋กœ ์ฃผ์ž…๋˜์–ด ํ…Œ์ŠคํŠธ ์‹œ Mock์œผ๋กœ ๊ต์ฒดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@Reducer
struct ChatDetailFeature {
    @ObservableState
    struct State: Equatable { ... }
    enum Action { ... }

    @Dependency(\.apiClient) var apiClient
    @Dependency(\.socketClient) var socketClient

    var body: some ReducerOf<Self> {
        Reduce { state, action in ... }
    }
}

2. ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ๋กœ๋”ฉ ์ „๋žต (3-Stage Loading)

sequenceDiagram
    participant UI as ChatDetailView
    participant R as ChatDetailFeature
    participant DB as Realm
    participant API as Server API
    participant WS as Socket.IO

    UI->>R: onAppear
    R->>DB: ๋กœ์ปฌ ์บ์‹œ ์กฐํšŒ
    DB-->>R: ์ €์žฅ๋œ ๋ฉ”์‹œ์ง€
    R->>R: receivedMessageIds ์ดˆ๊ธฐํ™”

    par API ๋™๊ธฐํ™”์™€ Socket ์—ฐ๊ฒฐ์„ ๋™์‹œ ์‹คํ–‰
        R->>API: ์ตœ์‹  ๋ฉ”์‹œ์ง€ ์š”์ฒญ
        R->>WS: AsyncStream ์—ฐ๊ฒฐ
    end

    API-->>R: ์ตœ์‹  ๋ฉ”์‹œ์ง€
    R->>R: Set ID ์ค‘๋ณต ์ฒดํฌ ํ›„ ๋ณ‘ํ•ฉ
    R-->>UI: messages ์—…๋ฐ์ดํŠธ

    Note over WS,R: ์‹ค์‹œ๊ฐ„ ์ˆ˜์‹ 
    WS->>R: socketMessageReceived
    R->>R: receivedMessageIds ์ค‘๋ณต ์ฒดํฌ
    R-->>UI: ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
Loading

์ฑ„ํŒ… ํ™”๋ฉด ์ง„์ž… ์‹œ Realm์—์„œ ๋กœ์ปฌ ์บ์‹œ๋ฅผ ๋จผ์ € ํ‘œ์‹œํ•˜๊ณ , API๋กœ ์ตœ์‹  ๋ฉ”์‹œ์ง€๋ฅผ ๋™๊ธฐํ™”ํ•œ ๋’ค, Socket.IO๋ฅผ ํ†ตํ•ด ์‹ค์‹œ๊ฐ„ ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•ฉ๋‹ˆ๋‹ค. ์„ธ ๊ฐ€์ง€ ๊ฒฝ๋กœ์—์„œ ์œ ์ž…๋˜๋Š” ๋ฉ”์‹œ์ง€๋Š” Set<String> ๊ธฐ๋ฐ˜ receivedMessageIds๋กœ ์ค‘๋ณต์„ ์ฒดํฌํ•˜์—ฌ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜์™€ ์ค‘๋ณต ๋ Œ๋”๋ง์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.


3. ํ† ํฐ ๊ฐฑ์‹  ํ”Œ๋กœ์šฐ (AuthInterceptor - actor)

sequenceDiagram
    participant F as Feature Reducer
    participant AC as APIClient
    participant INT as AuthInterceptor
    participant API as Server

    F->>AC: API ์š”์ฒญ
    AC->>API: Request with accessToken
    API-->>AC: 419 ํ† ํฐ ๋งŒ๋ฃŒ

    AC->>INT: refresh ํ˜ธ์ถœ
    INT->>API: /auth/refresh with refreshToken
    API-->>INT: ์ƒˆ Access Token + Refresh Token
    INT-->>AC: TokenPair ๋ฐ˜ํ™˜

    AC->>AC: Keychain์— ์ƒˆ ํ† ํฐ ์ €์žฅ
    AC->>API: ์›๋ž˜ ์š”์ฒญ ์žฌ์‹œ๋„
    API-->>AC: 200 OK
    AC-->>F: ์‘๋‹ต ์ „๋‹ฌ
Loading

419 ์‘๋‹ต์„ ๊ฐ์ง€ํ•˜๋ฉด AuthInterceptor(actor)๊ฐ€ ํ† ํฐ ๊ฐฑ์‹ ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. Swift Concurrency์˜ actor ๊ฒฉ๋ฆฌ๋กœ ๋™์‹œ ๊ฐฑ์‹  ์š”์ฒญ์ด ์•ˆ์ „ํ•˜๊ฒŒ ์ง๋ ฌํ™”๋˜๋ฉฐ, CheckedContinuation์œผ๋กœ ๋Œ€๊ธฐ ์ค‘์ธ ์š”์ฒญ๋“ค์„ ์ผ๊ด„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. EyeMon(UIKit)์—์„œ๋Š” ๋ณ„๋„ Session ๋ถ„๋ฆฌ๋กœ ๋ฌดํ•œ ๋ฃจํ”„๋ฅผ ๋ฐฉ์ง€ํ–ˆ๋‹ค๋ฉด, EyeMonDoctor(SwiftUI)์—์„œ๋Š” actor + async/await๋กœ ๋™์‹œ์„ฑ ๋ฌธ์ œ๊นŒ์ง€ ํ•ด๊ฒฐํ•œ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค. ๊ฐฑ์‹  ์‹คํŒจ(418) ์‹œ์—๋Š” ํ† ํฐ์„ ์‚ญ์ œํ•˜๊ณ  ๊ฐ•์ œ ๋กœ๊ทธ์•„์›ƒ์„ ํŠธ๋ฆฌ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.


4. ํŒŒ์ผ ์—…๋กœ๋“œ ์ „๋žต (2-Stage Upload)

sequenceDiagram
    participant UI as ChatDetailView
    participant R as ChatDetailFeature
    participant AC as APIClient
    participant API as Server

    UI->>R: imageSelected ๋˜๋Š” pdfSelected
    Note over R: isUploading = true

    R->>AC: Multipart Upload
    AC->>API: POST /chats/roomId/files
    API-->>AC: ์—…๋กœ๋“œ๋œ ํŒŒ์ผ URL
    AC-->>R: uploadFilesResponse

    R->>AC: sendMessage with ํŒŒ์ผ URL
    AC->>API: POST /chats/roomId
    API-->>AC: ChatMessageDTO
    AC-->>R: sendMessageResponse

    R-->>UI: ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก์— ์ถ”๊ฐ€
Loading

ํŒŒ์ผ ์—…๋กœ๋“œ์™€ ๋ฉ”์‹œ์ง€ ์ „์†ก์„ ๋ถ„๋ฆฌํ•˜๋Š” 2๋‹จ๊ณ„ ์ „๋žต์„ ์ฑ„ํƒํ–ˆ์Šต๋‹ˆ๋‹ค. 1๋‹จ๊ณ„์—์„œ ํŒŒ์ผ์„ ์„œ๋ฒ„์— ์—…๋กœ๋“œํ•˜์—ฌ URL์„ ํš๋“ํ•˜๊ณ , 2๋‹จ๊ณ„์—์„œ ํ•ด๋‹น URL์„ ํฌํ•จํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ๋‹จ๊ณ„์˜ ์—๋Ÿฌ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ, ์—…๋กœ๋“œ ์‹คํŒจ์™€ ์ „์†ก ์‹คํŒจ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


Key Features

1. ์ด๋ฉ”์ผ ๋กœ๊ทธ์ธ / ํšŒ์›๊ฐ€์ž…

์ด๋ฉ”์ผ ์ค‘๋ณต ํ™•์ธ๊ณผ ์œ ํšจ์„ฑ ๊ฒ€์ฆ(์ด๋ฉ”์ผ ํ˜•์‹, ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ทœ์น™)์„ ๊ฑฐ์ณ ๊ณ„์ •์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. JWT ๊ธฐ๋ฐ˜ ํ† ํฐ์„ Keychain์— ์ €์žฅํ•˜๊ณ , 419 ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ actor ๊ธฐ๋ฐ˜ AuthInterceptor๊ฐ€ ์ž๋™ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค. ๊ฐฑ์‹  ์‹คํŒจ(418) ์‹œ์—๋Š” ํ† ํฐ์„ ์‚ญ์ œํ•˜๊ณ  ๋กœ๊ทธ์ธ ํ™”๋ฉด์œผ๋กœ ๊ฐ•์ œ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

2. ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… (NavigationSplitView)

iPad NavigationSplitView๋กœ ์ฑ„ํŒ… ๋ชฉ๋ก๊ณผ ์ƒ์„ธ๋ฅผ 2-Column ๋ ˆ์ด์•„์›ƒ์œผ๋กœ ๋™์‹œ์— ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. Socket.IO๋ฅผ AsyncStream์œผ๋กœ ๋ž˜ํ•‘ํ•˜์—ฌ TCA์˜ Effect ์ฒด๊ณ„์™€ ํ†ตํ•ฉํ–ˆ์œผ๋ฉฐ, cancellable(id:)๋กœ ํ™”๋ฉด ์ดํƒˆ ์‹œ ์†Œ์ผ“ ์—ฐ๊ฒฐ์„ ์ž๋™ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ํ…์ŠคํŠธ, ์ด๋ฏธ์ง€, PDF ํŒŒ์ผ ์ „์†ก์„ ์ง€์›ํ•˜๋ฉฐ, Realm ๋กœ์ปฌ ์บ์‹ฑ์œผ๋กœ ์˜คํ”„๋ผ์ธ์—์„œ๋„ ์ด์ „ ๋Œ€ํ™”๋ฅผ ์ฆ‰์‹œ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.

3. VOD ์ŠคํŠธ๋ฆฌ๋ฐ

HLS(m3u8) ๊ธฐ๋ฐ˜ ๋น„๋””์˜ค ์ŠคํŠธ๋ฆฌ๋ฐ์„ ํ”„๋กœํ•„ ํ™”๋ฉด์—์„œ ์ธ๋ผ์ธ ์žฌ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ œํœด VOD ์„น์…˜์—์„œ ์ฒซ ๋ฒˆ์งธ ์˜์ƒ์„ ์ž๋™ ๋กœ๋“œํ•˜๊ณ , ์žฌ์ƒ/์ •์ง€ ์ œ์–ด๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

4. ํ”„๋กœํ•„ ๋ฐ ์„ค์ •

์˜์‚ฌ ํ”„๋กœํ•„ ์ •๋ณด(์ด๋ฆ„, ์ด๋ฉ”์ผ, ์†Œ๊ฐœ)๋ฅผ ํ‘œ์‹œํ•˜๋ฉฐ, ์˜ˆ์•ฝ ํ™˜์ž ์ˆ˜๋ฅผ ์ฑ„ํŒ…๋ฐฉ ๋ชฉ๋ก๊ณผ ์—ฐ๋™ํ•˜์—ฌ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ๋กœ๊ทธ์•„์›ƒ ์‹œ Keychain ํ† ํฐ๊ณผ UserDefaults ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.

5. Unit Test (Swift Testing + TCA TestStore)

@Test ๋งคํฌ๋กœ ๊ธฐ๋ฐ˜์œผ๋กœ 10๊ฐœ์˜ Unit Test๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. withDependencies๋กœ Mock์„ ์ฃผ์ž…ํ•˜๊ณ , TestStore๋ฅผ ํ†ตํ•ด Action โ†’ State ๋ณ€ํ™”๋ฅผ ๋‹จ๊ณ„๋ณ„๋กœ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. LockIsolated๋ฅผ ํ™œ์šฉํ•˜์—ฌ Swift 6 Concurrency ํ™˜๊ฒฝ์—์„œ๋„ Side Effect๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์บก์ฒ˜ํ•ฉ๋‹ˆ๋‹ค.


Troubleshooting

1. ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์ค‘๋ณต โ€” Set ๊ธฐ๋ฐ˜ ์ค‘๋ณต ์ œ๊ฑฐ

๋ฌธ์ œ ์ฑ„ํŒ… ํ™”๋ฉด์—์„œ DB ์บ์‹œ ๋ฉ”์‹œ์ง€, API ๋™๊ธฐํ™” ๋ฉ”์‹œ์ง€, Socket ์‹ค์‹œ๊ฐ„ ๋ฉ”์‹œ์ง€๊ฐ€ ๋™์‹œ์— ์œ ์ž…๋˜๋ฉด์„œ ๋™์ผํ•œ ๋ฉ”์‹œ์ง€๊ฐ€ ์ค‘๋ณต์œผ๋กœ ํ‘œ์‹œ๋˜๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ Socket ๋ฉ”์‹œ์ง€๊ฐ€ API ์‘๋‹ต๋ณด๋‹ค ๋จผ์ € ๋„์ฐฉํ•˜๋Š” ๊ฒฝ์šฐ, ๊ฐ™์€ ๋ฉ”์‹œ์ง€๊ฐ€ 2๋ฒˆ ๋ Œ๋”๋ง๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์›์ธ ์„ธ ๊ฐ€์ง€ ๋ฐ์ดํ„ฐ ์†Œ์Šค(DB/API/Socket)๊ฐ€ ๊ฐ๊ฐ ๋…๋ฆฝ์ ์œผ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ State์— ์ถ”๊ฐ€ํ•˜๋ฉด์„œ, ๋ฉ”์‹œ์ง€ ID ๊ธฐ๋ฐ˜ ์ค‘๋ณต ์ฒดํฌ๊ฐ€ ๋ˆ„๋ฝ๋œ ๊ฒฝ๋กœ๊ฐ€ ์กด์žฌํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ State์— receivedMessageIds: Set<String>์„ ๋‘๊ณ , ๋ชจ๋“  ๋ฉ”์‹œ์ง€ ์œ ์ž… ๊ฒฝ๋กœ(DB/API/Socket/์ „์†ก ์„ฑ๊ณต)์—์„œ ID ์ค‘๋ณต ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•œ ํ›„ ์ถ”๊ฐ€ํ•˜๋„๋ก ํ†ต์ผํ–ˆ์Šต๋‹ˆ๋‹ค.

case .socketMessageReceived(let dto):
    guard !state.receivedMessageIds.contains(dto.chatId) else {
        return .none  // ์ด๋ฏธ ์ˆ˜์‹ ํ•œ ๋ฉ”์‹œ์ง€๋Š” ๋ฌด์‹œ
    }
    state.receivedMessageIds.insert(dto.chatId)
    state.messages.append(Message(dto: dto))

2. ํ† ํฐ ๊ฐฑ์‹  ๋™์‹œ ์š”์ฒญ โ€” actor + CheckedContinuation

๋ฌธ์ œ ์—ฌ๋Ÿฌ API ์š”์ฒญ์ด ๋™์‹œ์— 419๋ฅผ ๋ฐ›์œผ๋ฉด ํ† ํฐ ๊ฐฑ์‹ ์ด ์ค‘๋ณต ํ˜ธ์ถœ๋˜์–ด, ์ด๋ฏธ ๊ฐฑ์‹ ๋œ Refresh Token์œผ๋กœ ์žฌ๊ฐฑ์‹ ์„ ์‹œ๋„ํ•˜๋ฉด์„œ 418(๊ฐ•์ œ ๋กœ๊ทธ์•„์›ƒ)์ด ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์›์ธ async/await ํ™˜๊ฒฝ์—์„œ ์—ฌ๋Ÿฌ Task๊ฐ€ ๋™์‹œ์— 419 ์‘๋‹ต์„ ๋ฐ›์œผ๋ฉด, ๊ฐ๊ฐ ๋…๋ฆฝ์ ์œผ๋กœ ๊ฐฑ์‹  API๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ Race Condition์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ๊ฐฑ์‹ ์ด ์„ฑ๊ณตํ•˜๋ฉด Refresh Token์ด ๊ต์ฒด๋˜๋ฏ€๋กœ, ๋‚˜๋จธ์ง€ ์š”์ฒญ์˜ ๊ฐฑ์‹ ์€ ๋งŒ๋ฃŒ๋œ ํ† ํฐ์œผ๋กœ ์‹œ๋„๋˜์–ด ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ AuthInterceptor๋ฅผ Swift actor๋กœ ๊ตฌํ˜„ํ•˜์—ฌ ๊ฐฑ์‹  ์š”์ฒญ์„ ์ž๋™ ์ง๋ ฌํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ์š”์ฒญ๋งŒ ์‹ค์ œ ๊ฐฑ์‹ ์„ ์ˆ˜ํ–‰ํ•˜๊ณ , ๋‚˜๋จธ์ง€ ์š”์ฒญ์€ CheckedContinuation์œผ๋กœ ๋Œ€๊ธฐ์‹œํ‚จ ๋’ค ๊ฐฑ์‹  ์™„๋ฃŒ ์‹œ ์ผ๊ด„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

actor AuthInterceptor {
    private var isRefreshing = false
    private var pendingContinuations: [CheckedContinuation<TokenPair, Error>] = []

    func refresh(...) async throws -> TokenPair {
        if isRefreshing {
            // ์ด๋ฏธ ๊ฐฑ์‹  ์ค‘์ด๋ฉด ๋Œ€๊ธฐ
            return try await withCheckedThrowingContinuation { continuation in
                pendingContinuations.append(continuation)
            }
        }
        isRefreshing = true
        // ์‹ค์ œ ๊ฐฑ์‹  ์ˆ˜ํ–‰ โ†’ ๋Œ€๊ธฐ ์ค‘์ธ continuation๋“ค ์ผ๊ด„ resume
    }
}

3. Socket.IO + TCA ํ†ตํ•ฉ โ€” AsyncStream ๋ž˜ํ•‘

๋ฌธ์ œ Socket.IO์˜ ์ฝœ๋ฐฑ ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋ง์„ TCA์˜ Effect<Action> ์ฒด๊ณ„์— ํ†ตํ•ฉํ•˜๋Š” ๊ฒƒ์ด ๊ณผ์ œ์˜€์Šต๋‹ˆ๋‹ค. ์ง์ ‘์ ์ธ ์ฝœ๋ฐฑ ์‚ฌ์šฉ์€ TCA์˜ ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ๊นจ๋œจ๋ฆฌ๋Š” ๊ตฌ์กฐ์˜€์Šต๋‹ˆ๋‹ค.

์›์ธ Socket.IO๋Š” on("chat") { data, ack in } ํ˜•ํƒœ์˜ ์ฝœ๋ฐฑ API๋งŒ ์ œ๊ณตํ•˜์—ฌ, TCA Reducer์˜ Effect โ†’ Action ํŒŒ์ดํ”„๋ผ์ธ์— ์ง์ ‘ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ Socket.IO ์ด๋ฒคํŠธ๋ฅผ AsyncStream<ChatMessageDTO>๋กœ ๋ž˜ํ•‘ํ•˜์—ฌ, TCA์˜ .run Effect์—์„œ for await ๋ฃจํ”„๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•ฉ๋‹ˆ๋‹ค. cancellable(id:)๋ฅผ ์ ์šฉํ•˜์—ฌ ํ™”๋ฉด ์ดํƒˆ ์‹œ ์†Œ์ผ“ ์—ฐ๊ฒฐ์„ ์ž๋™ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

.run { [roomId, socketClient, keychain] send in
    guard let token = keychain.getAccessToken() else { return }
    let stream = socketClient.connect(roomId, token)
    for await dto in stream {
        await send(.socketMessageReceived(dto))
    }
}
.cancellable(id: CancelID.socketStream)

4. View์—์„œ @Dependency ์ง์ ‘ ์ ‘๊ทผ โ€” State ๊ธฐ๋ฐ˜ ์ „ํ™˜

๋ฌธ์ œ ์ดˆ๊ธฐ ๊ตฌํ˜„์—์„œ SwiftUI View๊ฐ€ @Dependency(\.keychainClient)๋กœ Keychain์— ์ง์ ‘ ์ ‘๊ทผํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. TCA์˜ "View๋Š” State๋งŒ ์ฝ๋Š”๋‹ค" ์›์น™์— ์œ„๋ฐฐ๋˜๋ฉฐ, TestStore๋กœ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ฆํ•  ์ˆ˜ ์—†๋Š” ๊ตฌ์กฐ์˜€์Šต๋‹ˆ๋‹ค.

์›์ธ TCA์˜ @Dependency๋Š” Reducer ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉํ•˜๋„๋ก ์„ค๊ณ„๋˜์–ด ์žˆ์œผ๋‚˜, SwiftUI View์—์„œ๋„ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํŽธ์˜์ƒ View์—์„œ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ๋ฐ์ดํ„ฐ ํ๋ฆ„์ด State โ†’ View ๋‹จ๋ฐฉํ–ฅ์ด ์•„๋‹Œ, Dependency โ†’ View ์šฐํšŒ ๊ฒฝ๋กœ๊ฐ€ ์ƒ๊ฒผ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ View์˜ @Dependency๋ฅผ ๋ชจ๋‘ ์ œ๊ฑฐํ•˜๊ณ , Reducer์˜ onAppear์—์„œ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ State์— ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ „ํ™˜ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ ํ๋ฆ„์ด State โ†’ View ๋‹จ๋ฐฉํ–ฅ์œผ๋กœ ํ†ต์ผ๋˜๊ณ , TestStore์—์„œ ๋ชจ๋“  ์ƒํƒœ๋ฅผ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

// Reducer
case .onAppear:
    state.accessToken = keychain.getAccessToken()
    state.currentUserId = userDefaults.getUserId()

// View โ€” State์—์„œ๋งŒ ์ฝ๊ธฐ
ProfileImageView(accessToken: store.accessToken)

Project Structure

EyeMonDoctor/
โ”œโ”€ EyeMonDoctorApp.swift              # @main ์ง„์ž…์ 
โ”œโ”€ Features/
โ”‚  โ”œโ”€ App/                            # AppFeature (์ธ์ฆ ๋ถ„๊ธฐ), MainTabFeature
โ”‚  โ”œโ”€ Auth/                           # LoginFeature, SignUpFeature
โ”‚  โ”œโ”€ ChatList/                       # ChatListFeature (๋ชฉ๋ก + NavigationSplitView)
โ”‚  โ”œโ”€ ChatDetail/                     # ChatDetailFeature (Socket + Realm + ํŒŒ์ผ ์ „์†ก)
โ”‚  โ””โ”€ Profile/                        # ProfileFeature (VOD ์ŠคํŠธ๋ฆฌ๋ฐ)
โ”œโ”€ Core/
โ”‚  โ”œโ”€ Network/                        # APIClient, APIRouter, APIError
โ”‚  โ”‚  โ”œโ”€ DTOs/                        # Auth, Chat, Video, User, ServerError DTO
โ”‚  โ”‚  โ””โ”€ Interceptor/                 # AuthInterceptor (actor ๊ธฐ๋ฐ˜ ํ† ํฐ ๊ฐฑ์‹ )
โ”‚  โ”œโ”€ Socket/                         # SocketClient (AsyncStream ๋ž˜ํ•‘)
โ”‚  โ”œโ”€ Clients/                        # KeychainClient, UserDefaultsClient, VideoClient
โ”‚  โ”œโ”€ Database/                       # RealmClient, ChatMapper, Realm Models
โ”‚  โ””โ”€ DesignSystem/                   # ColorSystem, Typography, ProfileImageView
โ”œโ”€ Models/                            # User, ChatRoom, Message, Video
โ”œโ”€ Resource/                          # Assets, Fonts (Pretendard)
โ””โ”€ Secret/                            # APIConfig (.gitignore)

EyeMonDoctorTests/
โ”œโ”€ AppFeatureTests.swift              # ์ธ์ฆ ๋ถ„๊ธฐ ํ…Œ์ŠคํŠธ (2๊ฐœ)
โ”œโ”€ ChatListFeatureTests.swift         # ์ฑ„ํŒ… ๋ชฉ๋ก ํ…Œ์ŠคํŠธ (4๊ฐœ)
โ”œโ”€ LoginFeatureTests.swift            # ๋กœ๊ทธ์ธ ํ…Œ์ŠคํŠธ (4๊ฐœ)
โ””โ”€ Helpers/
   โ”œโ”€ TestFixtures.swift              # ํ…Œ์ŠคํŠธ์šฉ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ
   โ””โ”€ DTOEncodable+Test.swift         # DTO ์ธ์ฝ”๋”ฉ ํ—ฌํผ

License

์ด ํ”„๋กœ์ ํŠธ๋Š” ๊ฐœ์ธ ํฌํŠธํด๋ฆฌ์˜ค ํ”„๋กœ์ ํŠธ์ด๋ฉฐ, ์ƒ์—…์  ์‚ฌ์šฉ์„ ๊ธˆ์ง€ํ•ฉ๋‹ˆ๋‹ค.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages