C.W.K.
Stream
Lesson 04 of 05 · published

엔진 진실을 읽어, 그림자를 두지 마

~12 min · embed-engine, observed-properties, state, libmpv

Level 0릴 입문자
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"관찰되는 속성을 구현해: duration, time, pause, tracks, chapters, cache, errors."

그림자 상태의 유혹

엔진을 감쌀 때 뻔한 수는 네 변수를 두는 거야: var isPlaying, var currentTime, var duration. 명령 보낼 때 뒤집고, UI 가 거기 바인딩하고, 끝. 되긴 해 — 엔진이랑 네 그림자가 안 맞기 전까진. 사용자가 엔진이 직접 처리한 미디어 키로 정지하고; 파일이 혼자 끝나고; seek 이 네가 가정한 것보다 짧은 duration 으로 clamp 돼. 이제 네 변수는 한 가지를 말하고 영상은 다른 걸 하고, 거짓말하는 UI 보다 미치게 하는 버그는 없어.

대신 엔진을 구독해

libmpv 는 더 나은 계약을 제공해: 관찰되는 속성. 네가 신경 쓰는 값을 알려줘 — time-pos, pause, duration, 트랙 목록, 챕터, cache, 에러 — 그럼 엔진의 값이 실제로 바뀔 때마다 변경 이벤트를 밀어줘. 네 UI 는 네가 뒤집는 변수 뭉치이길 멈추고 엔진이 보고하는 것의 거울이 돼.

엔진이 단일 진실의 출처야; 네 UI 는 거울이지, 두 번째 사본이 절대 아냐. 재생 상태를 병렬로 추적하면서 싱크 맞길 바라지 마. 진짜를 구독하고 그걸 반영해.

구독은 작고, 신뢰의 방향을 뒤집어 — 네 추측을 미는 걸 멈추고 엔진의 사실을 받기 시작해:

이게 Observed-State 트랙 전체의 씨앗이야. Track 4 가 seek 에 대해 말하는 모든 것 — pending 대 applied, 명시적 시간이 resume 를 이김, 컨트롤이 현실을 반영 — 이 이 하나의 습관에서 자라: 관찰할 수 있는 걸 저장하지 마, 그리고 엔진이 확인한 값이 아니라 네가 설정한 값을 믿지 마.

왜 이게 어려운 모든 것으로 확장되나

패턴이 자리 잡으면, 어려운 경우들이 쉬워져. 미디어 키? 엔진이 pause 를 바꿨고, 네가 관찰했고, UI 가 업데이트됐어 — 키를 네가 라우팅할 필요도 없었어. 파일 끝? time-pos 가 멈추고 end 이벤트가 발화했어. seek 이 짧게 떨어졌어? time-pos 가 네가 요청한 데가 아니라 실제로 있는 데를 보고해. 넌 reconciliation 코드를 절대 안 짜, 애초에 맞출 두 사본이 없었으니까. 그게 그림자 대신 관찰하는 것의 조용한 초능력이야: 드리프트 버그가 그냥 살 곳이 없어.

Code

엔진 속성을 그림자로 두지 말고 관찰해·swift
// Subscribe to the values the UI mirrors. From now on libmpv pushes a
// change event whenever the ENGINE's value changes -- not when we ask.
mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE)
mpv_observe_property(mpv, 0, "pause",    MPV_FORMAT_FLAG)
mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE)

// In the event loop, translate engine truth into published UI state.
func handle(_ event: UnsafePointer<mpv_event>) {
    guard event.pointee.event_id == MPV_EVENT_PROPERTY_CHANGE else { return }
    let prop = event.pointee.data.assumingMemoryBound(to: mpv_event_property.self)
    switch String(cString: prop.pointee.name) {
    case "time-pos":  publishedTime     = readDouble(prop)   // engine's now
    case "pause":     isPaused          = readFlag(prop)     // engine's state
    case "duration":  publishedDuration = readDouble(prop)   // engine's length
    default: break
    }
}
// Notice: no variable is set by a command. Every value is the engine's.

External links

Exercise

네 코드에서 외부 시스템 상태를 미러링하는 로컬 변수를 두는 곳을 찾아 — 연결의 'isConnected', 다운로드의 'progress', 플레이어의 'isPlaying'. 물어봐: 그 외부 게 네 setter 를 안 거치고 바뀌면 무슨 일이 나? 답이 '내 변수가 stale 해짐' 이면 넌 그림자를 가진 거야. 네 사본이 못 드리프트하게 진짜 값을 구독하는 법을 스케치해.
Hint
그림자 상태는 다른 어딘가에 사는 진실과 맞아야 하는, 네가 직접 설정하는 아무 변수야. 고침은 항상 같은 모양이야: 나갈 때 설정하는 걸 멈추고, 들어올 때 받기 시작해 — 구독하고, 관찰하고, 미러링해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.