<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>아키텍처 on monkshark.dev</title><link>https://monkshark.github.io/tags/%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98/</link><description>Recent content in 아키텍처 on monkshark.dev</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Tue, 09 Jun 2026 10:00:00 +0900</lastBuildDate><atom:link href="https://monkshark.github.io/tags/%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98/index.xml" rel="self" type="application/rss+xml"/><item><title>#14 - 콜백이 흩어질 때, 상태를 onEvent 하나로 모으기</title><link>https://monkshark.github.io/p/page-ide-mvi-onevent/</link><pubDate>Tue, 09 Jun 2026 10:00:00 +0900</pubDate><guid>https://monkshark.github.io/p/page-ide-mvi-onevent/</guid><description>&lt;p&gt;기능을 하나 추가할 때마다 손대야 하는 파일이 늘어나는 시기가 온다. 새 버튼 하나를 누르면 무슨 일이 일어나게 하고 싶을 뿐인데, 그러려면 콜백을 선언하고, 그 콜백을 번들에 끼우고, 번들 시그니처를 고치고, 번들을 넘기는 호출부를 고치고, 다시 그 콜백이 실제로 하는 일을 어딘가에 적어야 했다. PAGE 의 에디터 셸이 딱 그 상태였다.&lt;/p&gt;
&lt;h2 id="콜백이-흩어진다는-것"&gt;&lt;a href="#%ec%bd%9c%eb%b0%b1%ec%9d%b4-%ed%9d%a9%ec%96%b4%ec%a7%84%eb%8b%a4%eb%8a%94-%ea%b2%83" class="header-anchor"&gt;&lt;/a&gt;콜백이 흩어진다는 것
&lt;/h2&gt;&lt;p&gt;직전 리팩터링으로 로직 자체는 이미 plain 클래스로 빠져 있었다. &lt;code&gt;AppController&lt;/code&gt; 가 IO 와 LSP 호출을 들고 있었고, 세션 복원은 &lt;code&gt;SessionCoordinator&lt;/code&gt; 가 맡았다. 문제는 로직이 아니라 UI 와 로직이 만나는 자리(seam)였다.&lt;/p&gt;
&lt;p&gt;렌더 루트인 &lt;code&gt;IdeMainLayout&lt;/code&gt; 은 holder 네 개와 바인딩 번들 다섯 개, 그리고 거기에 묶이지 못한 느슨한 콜백 십수 개를 받았다. 그 아래 &lt;code&gt;PaneRegion&lt;/code&gt; 은 또 액션 콜백 열한 개를 더 받았다. 콜백 하나하나는 평범했다. 문제는 &amp;ldquo;이 컴포넌트가 대체 무엇을 트리거할 수 있는가&amp;quot;라는 질문의 답이 시그니처 여기저기에 흩어져 있다는 점이었다. 어떤 동작을 따라가려면 콜백 이름 → 번들 → 빌더 → 실제 구현으로 네 번 점프해야 했고, 새 동작을 추가하려면 그 사슬을 거꾸로 네 번 다시 엮어야 했다.&lt;/p&gt;
&lt;p&gt;이쯤 되면 패턴을 바꿔야 한다는 신호다. 원했던 건 단순했다. UI 는 &amp;ldquo;무슨 일이 일어났다&amp;quot;만 말하고, 상태가 어떻게 바뀌는지는 한곳에서 결정한다.&lt;/p&gt;
&lt;h2 id="onevent-하나로-수렴시키기"&gt;&lt;a href="#onevent-%ed%95%98%eb%82%98%eb%a1%9c-%ec%88%98%eb%a0%b4%ec%8b%9c%ed%82%a4%ea%b8%b0" class="header-anchor"&gt;&lt;/a&gt;onEvent 하나로 수렴시키기
&lt;/h2&gt;&lt;p&gt;목표를 한 줄로 적으면 이렇다. UI→로직 seam 을 단일 &lt;code&gt;onEvent: (IdeEvent) -&amp;gt; Unit&lt;/code&gt; 으로 모으고, 상태를 불변 &lt;code&gt;AppState&lt;/code&gt; 와 순수 함수 &lt;code&gt;reduce(state, event)&lt;/code&gt; 로 모델링한다. 흔히 MVI 라고 부르는 모양이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IdeEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Unit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;prev&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;next&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;버튼은 이제 &lt;code&gt;onEvent(IdeEvent.Panel.ToggleTerminal)&lt;/code&gt; 하나만 부른다. 무엇을 트리거하는지는 이벤트 이름에 다 적혀 있고, 그 이벤트가 상태를 어떻게 바꾸는지는 &lt;code&gt;reduce&lt;/code&gt; 한 곳에만 있다. 이론은 깔끔했다. 그런데 교과서를 그대로 따라가면 밟는 함정이 두 개 있었다.&lt;/p&gt;
&lt;h2 id="첫-번째-함정-상태를-하나로-묶으면-안-된다"&gt;&lt;a href="#%ec%b2%ab-%eb%b2%88%ec%a7%b8-%ed%95%a8%ec%a0%95-%ec%83%81%ed%83%9c%eb%a5%bc-%ed%95%98%eb%82%98%eb%a1%9c-%eb%ac%b6%ec%9c%bc%eb%a9%b4-%ec%95%88-%eb%90%9c%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;첫 번째 함정: 상태를 하나로 묶으면 안 된다
&lt;/h2&gt;&lt;p&gt;MVI 예제는 보통 &lt;code&gt;mutableStateOf(AppState)&lt;/code&gt; 하나를 두고 거기서 모든 걸 읽는다. Compose 에서 그렇게 하면 곤란하다. &lt;code&gt;AppState&lt;/code&gt; 를 통째로 읽는 컴포저블은 그 안의 &lt;em&gt;아무&lt;/em&gt; 필드나 바뀌어도 재조합된다. 패널 너비를 1px 줄였을 뿐인데 타이틀바도, 상태바도, 파일 트리도 다 같이 다시 그려진다. 리팩터링을 했더니 재조합 프로필이 오히려 나빠지는, 가장 피하고 싶은 결말이다.&lt;/p&gt;
&lt;p&gt;그래서 상태는 하나로 안 묶었다. 슬라이스별로 쪼갠 store 를 뒀다. &lt;code&gt;layout&lt;/code&gt;, &lt;code&gt;dialogs&lt;/code&gt;, &lt;code&gt;chrome&lt;/code&gt;, &lt;code&gt;tree&lt;/code&gt;, &lt;code&gt;editorLayout&lt;/code&gt; 이 각각 자기 &lt;code&gt;mutableStateOf&lt;/code&gt; 를 갖는다. &lt;code&gt;reduce&lt;/code&gt; 는 여전히 &lt;code&gt;AppState&lt;/code&gt; 전체에 대한 순수 함수지만, 디스패처는 결과를 써넣을 때 &lt;strong&gt;바뀐 슬라이스만&lt;/strong&gt; 갱신한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layout&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chrome&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chrome&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chrome&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;reduce&lt;/code&gt; 는 바뀌지 않은 슬라이스를 같은 인스턴스 그대로 돌려주므로, &lt;code&gt;!=&lt;/code&gt; 비교가 짧게 끊겨 그 슬라이스의 리더는 무효화되지 않는다. 패널 토글은 &lt;code&gt;layout&lt;/code&gt; 을 읽는 컴포저블만 다시 그린다. 모델은 불변·순수의 외형을 갖되, 재조합 단위는 예전의 필드 granular 한 수준을 그대로 유지한다. 단일 상태의 명료함과 슬라이스의 성능을 둘 다 갖기 위한 타협이었다.&lt;/p&gt;
&lt;h2 id="두-번째-경계-reduce-에-못-들어가는-것들"&gt;&lt;a href="#%eb%91%90-%eb%b2%88%ec%a7%b8-%ea%b2%bd%ea%b3%84-reduce-%ec%97%90-%eb%aa%bb-%eb%93%a4%ec%96%b4%ea%b0%80%eb%8a%94-%ea%b2%83%eb%93%a4" class="header-anchor"&gt;&lt;/a&gt;두 번째 경계: reduce 에 못 들어가는 것들
&lt;/h2&gt;&lt;p&gt;모든 상태가 순수 함수를 통과할 수 있는 건 아니다. 두 부류가 명확히 거부당했다.&lt;/p&gt;
&lt;p&gt;하나는 &lt;strong&gt;타건마다 바뀌는 에디터 문서&lt;/strong&gt;다. 편집 중인 텍스트(&lt;code&gt;TextFieldValue&lt;/code&gt;)와 탭 구조를 키 입력마다 불변 복사로 reduce 에 통과시키면 타이핑에 지연이 생긴다. 이건 타협의 문제가 아니라 들어오면 안 되는 것이다.&lt;/p&gt;
&lt;p&gt;다른 하나는 &lt;strong&gt;살아 있는 서비스&lt;/strong&gt;다. LSP 라우터, 터미널 매니저, 실행 컨트롤러, 출력 패널은 값 타입이 아니다. 불변 스냅샷으로 만들 수가 없다. 진단 결과나 실행 출력처럼 고빈도로 흐르는 스트림은 서비스가 자기 &lt;code&gt;mutableStateOf&lt;/code&gt; 를 들고 있고, UI 는 그걸 직접 읽어 &lt;code&gt;reduce&lt;/code&gt; 를 우회한다.&lt;/p&gt;
&lt;p&gt;그래서 상태는 세 겹이 됐다. 순수 함수를 통과하는 cold &lt;code&gt;AppState&lt;/code&gt;, 키 입력이 직접 변이하는 hot 문서 holder, 그리고 자기 상태를 직접 노출하는 live 서비스. 깔끔한 단일 모델보다 정직한 경계가 낫다고 봤다.&lt;/p&gt;
&lt;h2 id="디스패치를-동기로-둔-이유"&gt;&lt;a href="#%eb%94%94%ec%8a%a4%ed%8c%a8%ec%b9%98%eb%a5%bc-%eb%8f%99%ea%b8%b0%eb%a1%9c-%eb%91%94-%ec%9d%b4%ec%9c%a0" class="header-anchor"&gt;&lt;/a&gt;디스패치를 동기로 둔 이유
&lt;/h2&gt;&lt;p&gt;이 리팩터링의 목표는 &amp;ldquo;동작 무변화&amp;quot;였다. 같은 입력에 같은 결과, 같은 타이밍, 같은 순서. 그걸 보장한 건 디스패처를 동기로 둔 결정이었다.&lt;/p&gt;
&lt;p&gt;위의 &lt;code&gt;onEvent&lt;/code&gt; 은 &lt;code&gt;reduce → store.apply → effects.handle&lt;/code&gt; 을 호출 스레드에서 차례로 실행한다. 비동기 큐도, 디바운스도 없다. 즉 UI 콜백이 &lt;code&gt;onEvent&lt;/code&gt; 을 부르면, 예전에 콜백이 직접 컨트롤러를 부르던 그 자리에서 그대로 effect 가 실행된다. 덕분에 seam 을 옮기는 작업이 동작을 바꾸지 않는다는 걸 코드만 보고도 증명할 수 있었다. 큐를 끼웠다면 타이밍이 달라질 여지가 생기고, 그러면 &amp;ldquo;정말 똑같이 동작하는가&amp;quot;를 매번 손으로 확인해야 했을 것이다.&lt;/p&gt;
&lt;h2 id="query-는-이벤트가-아니다"&gt;&lt;a href="#query-%eb%8a%94-%ec%9d%b4%eb%b2%a4%ed%8a%b8%ea%b0%80-%ec%95%84%eb%8b%88%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;query 는 이벤트가 아니다
&lt;/h2&gt;&lt;p&gt;옮기다 보면 이벤트로 만들고 싶은 유혹이 드는 콜백이 있다. 값을 돌려주는 것들이다. 단축키 처리기는 &amp;ldquo;내가 이 키를 소비했는가&amp;quot;를 &lt;code&gt;Boolean&lt;/code&gt; 으로 돌려주고, 스크롤 위치나 접힌 줄 정보를 묻는 콜백도 답을 돌려준다.&lt;/p&gt;
&lt;p&gt;이런 건 이벤트가 될 수 없다. 이벤트는 fire-and-forget 이라 돌아오는 값이 없다. 그래서 값을 돌려주는 콜백은 끝까지 읽기 전용 함수로 남겼다. 모든 걸 한 모양으로 통일하고 싶은 충동을 여기서 한 번 눌렀다. seam 을 줄이는 게 목적이지, 맞지 않는 것을 억지로 끼우는 게 목적은 아니었다.&lt;/p&gt;
&lt;h2 id="한-번에-바꾸지-않기"&gt;&lt;a href="#%ed%95%9c-%eb%b2%88%ec%97%90-%eb%b0%94%ea%be%b8%ec%a7%80-%ec%95%8a%ea%b8%b0" class="header-anchor"&gt;&lt;/a&gt;한 번에 바꾸지 않기
&lt;/h2&gt;&lt;p&gt;도메인을 한꺼번에 옮기지 않았다. 빈 store 와 &lt;code&gt;onEvent&lt;/code&gt; 을 먼저 들여놓고, 한 단계에 한 도메인씩 holder 변이에서 reduce 경로로 이동했다. 나머지는 임시 read-adapter 로 컴파일을 유지했다. 레이아웃 패널, 그다음 chrome, 트리와 에디터 레이아웃, 다이얼로그, 마지막으로 실행·LSP·파일 트리 같은 비동기 도메인 순서였다.&lt;/p&gt;
&lt;p&gt;단계마다 단위 테스트와 실제 앱 스모크를 통과시키고 나서 다음으로 넘어갔다. 각 단계가 그 자체로 배포 가능하고 되돌릴 수 있는 지점이라는 게 중요했다. 큰 리팩터링에서 가장 무서운 건 버그가 아니라, 문제가 생겼을 때 어디로 돌아가야 할지 모르는 상황이다.&lt;/p&gt;
&lt;h2 id="끝내-하지-않은-것"&gt;&lt;a href="#%eb%81%9d%eb%82%b4-%ed%95%98%ec%a7%80-%ec%95%8a%ec%9d%80-%ea%b2%83" class="header-anchor"&gt;&lt;/a&gt;끝내 하지 않은 것
&lt;/h2&gt;&lt;p&gt;계획에는 마지막 단계가 하나 더 있었다. 에디터 문서까지 commit seam 을 씌워, 디바운스된 시점에만 문서 변경을 이벤트로 흘리는 것. 가장 위험하고, 그래서 선택적이라고 적어둔 단계였다.&lt;/p&gt;
&lt;p&gt;조사해 보니 할 일이 없었다. 문서 holder 는 이미 &lt;code&gt;AppState&lt;/code&gt; 바깥에 있었고, &lt;code&gt;TextFieldValue&lt;/code&gt; 가 순수 상태로 새어 들어가지 않는다는 불변식은 이미 지켜지고 있었다. 그렇다고 디바운스 commit 이벤트를 추가하면? 그 이벤트를 받아 갱신할 cold 리더가 한 곳도 없었다. 문서 텍스트를 보는 자리들은 전부 이미 자기 코루틴에서 디바운스를 하거나(자동 저장, 활성 텍스트 변경), 캐럿 위치처럼 키 입력마다 갱신돼야 정상인 표시들이었다. 읽는 쪽이 없는 이벤트는 배선만 늘리고 아무것도 보존하지 않는다.&lt;/p&gt;
&lt;p&gt;그래서 이 단계는 하지 않기로 하고 닫았다. 타이핑 경로는 의도된 대로 hot holder 에 남는다. 처음부터 정직한 한계로 적어둔 바로 그 종료 상태였다.&lt;/p&gt;
&lt;h2 id="무엇을-얻었나"&gt;&lt;a href="#%eb%ac%b4%ec%97%87%ec%9d%84-%ec%96%bb%ec%97%88%eb%82%98" class="header-anchor"&gt;&lt;/a&gt;무엇을 얻었나
&lt;/h2&gt;&lt;p&gt;이 작업이 성능을 끌어올렸다고 말할 수는 없다. 애초에 그게 목표가 아니었고, 슬라이스 store 의 목적은 기존 재조합 프로필을 그대로 보존하는 것이었다. 얻은 건 다른 데 있다.&lt;/p&gt;
&lt;p&gt;이제 컴포넌트가 무엇을 할 수 있는지는 그 컴포넌트가 보내는 이벤트 목록이 말해준다. 상태가 어떻게 바뀌는지는 &lt;code&gt;reduce&lt;/code&gt; 한 곳에서 읽힌다. 그리고 그 &lt;code&gt;reduce&lt;/code&gt; 는 Compose 없이 헤드리스로 테스트된다. 콜백 사슬을 네 번 점프하던 자리가, 이벤트를 보내는 쪽과 받는 쪽 두 곳으로 정리됐다.&lt;/p&gt;
&lt;p&gt;큰 리팩터링에서 배운 게 있다면, 교과서를 어디까지 따르고 어디서 멈출지를 정하는 게 절반이라는 점이다. 단일 상태 객체는 따르지 않았고, 동기 디스패치는 일부러 택했고, 값을 돌려주는 콜백과 타이핑 경로는 그대로 뒀다. 가장 위험한 마지막 단계는 결국 하지 않는 것이 답이었다. 무엇을 하지 않을지 정확히 아는 것도 설계의 일부였다.&lt;/p&gt;</description></item></channel></rss>