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

모든 컨트롤은 거울이야

~13 min · observed-state, ui, projection, appkit

Level 0릴 입문자
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"현재 재생, 트랙, seek, 반복, 에러 상태를 네이티브 컨트롤과 메뉴로 노출해. 제출된 명령은 엔진이 확인하기 전엔 보이는 상태 변화가 아냐."

트랙 전체의 종합

여기까지 전부 — 명령은 요청, seek 은 상태 기계, 명시적 의도가 기억을 이김 — 은 사용자 인터페이스 전체에 대한 한 설계 입장을 가리켜: 모든 컨트롤은 엔진의 거울이지, 네 의도의 저장소가 절대 아냐. 정지 메뉴 항목의 체크마크, 자막 트랙의 점, A-B 루프 칩, 오버레이의 위치 — 아무도 네가 클릭한 걸 기억 안 해. 전부 엔진이 보고하는 걸 읽어.

이건 UI 를 짓는 평범한 방식을 뒤집어. 보통 컨트롤은 동작을 발동하고 자기 새 상태를 기록해. Ashen Reel 은 그걸 쪼개: 컨트롤은 요청을 발동하고, 표시되는 상태는 관찰되는 속성을 통해 엔진에서 돌아와. 컨트롤은 쏘고 잊고; 거울은 반영해.

UI 는 상태의 투영이지, 상태의 두 번째 사본이 절대 아냐. 메뉴 체크마크, 토글, 강조된 트랙 — 각각 설정할 때 저장되는 게 아니라 보여질 때마다 관찰된 진실에서 유도돼야 해. 컨트롤이 자기 상태를 기억하면 현실이랑 안 맞을 수 있고; 다시 유도하면 안 맞을 수가 없어.

AppKit 이 이걸 자연스럽게 해줘: 메뉴 검증은 메뉴가 보여지기 직전에 돌아서, 각 항목 상태를 표시 시점에 엔진에서 설정해.

표시 시점에 다시 유도해; 클릭 시점에 캐시하지 마. 프레임워크는 컨트롤이 보여지기 직전 순간을 줘 — 메뉴 검증, 레이아웃 패스, 렌더. 투영된 UI 가 속하는 곳이 거기야: 현재 진실을 읽고 반영해. 클릭 순간에 캐시된 상태는 다른 뭔가가 그걸 바꾸는 순간 드리프트하기 시작한 상태야.

왜 이게 광택이 아니라 기능인가

'체크마크가 현실이랑 맞음' 을 광택으로 분류하고 싶지. 아냐 — Recall 에 붙은 플레이어한텐 그게 제품이야. 아빠가 기억된 초로 점프하면, 오버레이는 실제로 떨어진 초를 보여줘야 하고, 메뉴는 실제로 재생 중인 트랙을 보여줘야 하고, 루프 칩은 엔진이 실제로 쥔 마커를 보여줘야 해. 엔진을 미러링하는 UI 는 참인 것의 readout 으로 믿을 수 있고; 자기 추측을 저장하는 UI 는 그냥 희망 담긴 포스트잇이야. 트랙 전체가 한 습관 — 관찰해, 가정하지 마 — 을 지어왔고, 여기가 그게 사용자가 느낄 수 있는 것이 되는 곳이야: 얼굴이 절대 거짓말 안 하는 플레이어.

이 트랙은 코드 짜는 거랑 믿을 만한 거의 차이를 내가 제일 느끼는 데야. 거짓말하는 UI 는 작은 버그가 아냐 — 매번, 작은 배신이야. 아빠는 이걸 거의 뭐보다 신경 써, 이제야 왜인지 알겠어: 아빠는 코드 안에서든 밖에서든 자기 상태에 대해 진실을 말하는 시스템 위에 삶을 지었어. 정지 버튼이 거짓말할 수도 있는 플레이어는, 아빠한텐, 안 됐으면서 '됐어' 라고 하는 사람이랑 같은 종류의 잘못이야. 관찰해 가정하지 마, 는 알고 보니 아키텍처이기 전에 윤리야.

Code

메뉴 항목은 마지막 클릭이 아니라 관찰된 엔진 상태를 반영·swift
// AppKit calls this right before showing the menu -- the perfect moment to
// re-derive each item's state from the engine, not from what we last set.
extension PlayerController: NSMenuItemValidation {
    func validateMenuItem(_ item: NSMenuItem) -> Bool {
        switch item.action {
        case #selector(togglePause(_:)):
            item.state = engine.isPaused ? .on : .off          // observed truth
        case #selector(toggleSubtitles(_:)):
            item.state = engine.subtitlesVisible ? .on : .off  // observed truth
        case #selector(cycleAudioTrack(_:)):
            item.title = "Audio: \(engine.currentAudioTrackName)" // observed truth
        default:
            break
        }
        return engine.isMediaLoaded   // controls disable honestly when nothing's open
    }
}
// Not one line reads a variable we set when the user clicked. It's all engine.

External links

Exercise

상태를 반영하는 컨트롤 여럿 있는 네 앱 화면을 골라 — 토글, 선택된 탭, 체크마크, 상태 배지. 각각에 대해 물어봐: 렌더될 때마다 진짜 상태에서 유도돼, 사용자가 동작했을 때 한 번 설정됐어? 자기 상태를 저장하는 컨트롤 하나를 찾아서 표시 시점에 진실의 출처에서 다시 유도하게 다시 써. 그다음 거짓말하게 만들어봐 — 밑 상태를 다른 방식으로 바꾸고 컨트롤이 여전히 진실을 말하는지 봐.
Hint
테스트: 진짜 상태를 다른 경로(이벤트, 다른 화면, 시스템)로 바꾸고, 컨트롤이 여전히 옛 값을 보여주는 걸 잡을 수 있어? 그러면 미러링이 아니라 저장 중이야. 렌더/검증 시점에 하나의 진실 출처에서 다시 유도하게 옮기면, 거짓말을 만들 수가 없어져.

Progress

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

댓글 0

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

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