본문 바로가기
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

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

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