<figure class="image image_resized" style="width:100%;"><a href="https://www.wishket.com/crsr/?next=/w/EaN4AhXVQN/&referer_type=7110000705"><img src="https://yozm.wishket.com/media/news/1717/%EC%9C%84%EC%8B%9C%EC%BC%93_%EC%A0%84%ED%99%98_%EB%B0%B0%EB%84%88.png"></a></figure><p style="text-align:justify;"> </p><p style="text-align:justify;">국내 유명 IT 기업은 한국을 넘어 세계를 무대로 할 정도로 뛰어난 기술과 아이디어를 자랑합니다. 이들은 기업 블로그를 통해 이러한 정보를 공개하고 있습니다. 요즘IT는 각 기업의 특색 있고 유익한 콘텐츠를 소개하는 시리즈를 준비했습니다. 이들은 어떻게 사고하고, 어떤 방식으로 일하는 걸까요?</p><p style="text-align:justify;"> </p><p style="text-align:justify;">이번 글은 ‘성과 극대화를 위한 머신러닝 모바일 마케팅 성과분석 솔루션’ 에어브릿지(airbridge.io)를 만드는 AB180 Airbridge SDK 팀의 이야기입니다. 타입스크립트(TypeScript)의 안전한 코드작성을 도와주는 'satisfies' 키워드를 어떻게 활용하는지 소개하고 있습니다.</p><div class="page-break" style="page-break-after:always;"><span style="display:none;"> </span></div><p style="text-align:justify;"><a href="https://github.com/microsoft/TypeScript/issues/50457">TypeScript 4.9 Iteration Plan</a>이 공개되면서 이제 9월 20일부터 ‘타입스크립터(TypeScript) 4.9 Beta 버전’을 사용할 수 있게 됩니다. 이번 릴리즈에서 개인적으로 가장 기대하는 기능은 바로 ‘satisfies’ 키워드입니다. 이 글에서는 이 키워드가 무엇이고, 왜 필요하고, 어떤 문제를 해결하는지에 대해서 서술하고자 합니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;"> </p><h3 style="text-align:justify;"><strong>satisfies 키워드란? 왜 필요한가?</strong></h3><p style="text-align:justify;">‘satisfies’ 키워드는 literal (값)이나 변수를 안전하게 upcast 하는 기능을 수행합니다. 그런데 이게 어떤 것을 의미할까요?</p><p style="text-align:justify;"> </p><p style="text-align:justify;">먼저 아래 예시의 코드를 봐주세요.</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">const variable = 10</code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;">위 코드에서 TypeScript 은 아래와 같이 type을 추론합니다.</p><ol><li style="text-align:justify;">variable의 type을 알 수 없다.</li><li style="text-align:justify;">10은 number type을 가지는 literal이다.</li><li style="text-align:justify;">variable은 literal이 assign 되므로 variable의 type은 number이다.</li></ol><p style="text-align:justify;"> </p><p style="text-align:justify;">좀 더 복잡한 예제를 다루어 봅시다. 아래 코드는 어떻게 추론될까요?</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">const variable = { grade: "a", score: 90 }</code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;">아래와 같습니다.</p><p style="text-align:justify;"> </p><ol><li style="text-align:justify;">variable의 type을 알 수 없다.</li><li style="text-align:justify;"><span style="color:#e52929;"><code>{ grade: "a", score: 90 }</code></span>은 <span style="color:#e52929;"><code>{ grade: string, score: number }</code></span> type을 가지는 literal이다.</li><li style="text-align:justify;">variable은 literal이 assign 되므로 variable의 type은 <span style="color:#e52929;"><code>{ grade: string, score: number }</code></span>이다.</li></ol><p style="text-align:justify;"> </p><p style="text-align:justify;">그런데 여기서 문제가 발생합니다. 지금 variable의 type은 <span style="color:#e52929;"><code>{ grade: string, score: number }</code></span>입니다. 만약 variable을 사용할 때 grade member에만 접근할 수 있도록 강제하려면 어떻게 하면 될까요?</p><p style="text-align:justify;"> </p><h4 style="text-align:justify;"><strong>첫째, variable의 type을 미리 정의합니다.</strong></h4><pre><code class="language-plaintext">const variable1: { grade: string } = { grade: "a", score: 90 } // error const variable2: { grade: string, score: number, attribute: object } = { grade: "a", score: 90 } const variable3 = { // no way to force type { grade: string } key: { grade: "a", score: 90 } } type Variable4 = { key: { grade: string } } const variable4: Variable4 = { key: { grade: "a", score: 90 } } type Variable5 = { key: { grade: string, score: number, attribute: object } } const variable5: Variable5 = { // error key: { grade: "a", score: 90 } } </code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;">이 방법은 변수를 새로 생성하는 경우에는 문제없이 동작합니다. 위 코드에서 ‘variable1’ 케이스를 보시면 <strong>안전하게 assign 될 수 있는 경우</strong>일 때 에러 없이 type이 변경되는 것을 보실 수 있습니다. 하지만 ‘variable2’ 케이스를 보시면 <strong>안전하게 assign 될 수 없는 경우</strong>에는 에러가 발생합니다. 이를 이용해서 저희는 더욱 안전한 코드를 작성할 수 있습니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;">하지만 이 방법은 object의 key-value를 정의할 때 사용할 수 없습니다. ‘variable3’ 케이스를 보시면 해당 key-value 라인에서 type을 강제할 방법이 없습니다. 물론 ‘variable4’, ‘variable5’ 케이스와 같이 직접 type을 새로 정의하면 문제를 해결할 수 있지만, type이 크고 복잡해질수록 이에 대한 관리비용이 증가합니다.</p><p style="text-align:justify;"> </p><h4 style="text-align:justify;"><strong>둘째, ‘as’ 키워드를 사용합니다.</strong></h4><pre><code class="language-plaintext">const variable1 = { grade: "a", score: 90 } as { grade: string } // no error (!!!) const variable2 = { grade: "a", score: 90 } as { grade: string, score: number, attribute: object } const variable3 = { key: { grade: "a", score: 90 } as { grade: string } } const variable4 = { key: { grade: "a", score: 90 } as { grade: string } } const variable5 = { // no error (!!!) key: { grade: "a", score: 90 } as { grade: string, score: number, attribute: object } } </code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;">이 방법은 object의 key-value를 정의할 때도 사용할 수 있습니다. ‘variable3’, ‘variable4’ 케이스를 보시면 원하는 type으로 지정이 가능한 것을 볼 수 있습니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;">하지만 <strong>이 방법은 위험합니다.</strong> ‘variable2’, ‘variable5’ 케이스를 보시면 안전하게 type 변환될 수 있는 경우가 아님에도 type이 변환됩니다. 이는 이후 해당 변수를 사용할 때 버그의 원인이 될 수 있습니다.</p><p style="text-align:justify;"> </p><h4 style="text-align:justify;"><strong>‘satisfies’ 키워드를 사용하면…</strong></h4><pre><code class="language-plaintext">const variable1 = { grade: "a", score: 90 } satisfies { grade: string } // error const variable2 = { grade: "a", score: 90 } satisfies { grade: string, score: number, attribute: object } const variable3 = { key: { grade: "a", score: 90 } satisfies { grade: string } } const variable4 = { key: { grade: "a", score: 90 } satisfies { grade: string } } const variable5 = { // error key: { grade: "a", score: 90 } satisfies { grade: string, score: number, attribute: object } }</code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;">‘satisfies’ 키워드는 안전한 type 제한도, object key-value의 type 제한도 할 수 있습니다. satisfies는 as 키워드와 같이 expression에 사용 가능하기 때문에 object key-value의 type을 제한하는 경우에도 쓸 수 있습니다. 또한, satisfies는 as 키워드와 달리 안전한 type 제한을 지원하기 때문에 위험한 ‘variable2’, ‘variable5’ 케이스에 대해서 컴파일 에러를 발생시켜 개발자가 더욱 안전한 코드를 작성할 수 있도록 돕습니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;">위 방법들을 정리하면 아래와 같습니다.</p><figure class="table"><table><tbody><tr><td style="background-color:hsl(0, 0%, 90%);text-align:center;width:150px;"> </td><td style="background-color:hsl(0, 0%, 90%);text-align:center;">type 정의</td><td style="background-color:hsl(0, 0%, 90%);text-align:center;">as 키워드</td><td style="background-color:hsl(0, 0%, 90%);text-align:center;">satisfies 키워드</td></tr><tr><td style="background-color:hsl(0, 0%, 90%);text-align:center;width:150px;">안전한 type 제한</td><td style="text-align:center;">O</td><td style="text-align:center;">X</td><td style="text-align:center;">O</td></tr><tr><td style="background-color:hsl(0, 0%, 90%);text-align:center;width:150px;">object key-value의 type 제한</td><td style="text-align:center;">X</td><td style="text-align:center;">O</td><td style="text-align:center;">O</td></tr></tbody></table></figure><p style="text-align:justify;"> </p><p style="text-align:justify;">‘type 정의’ 방법은 안전한 type 제한을 할 수 있지만, object key-value의 type 제한은 할 수 없고, 하기 위해서는 전체 object의 type을 정의해야 합니다. 그리고 as 키워드는 object key-value의 type 제한은 할 수 있지만, 안전한 type 제한을 할 수 있습니다. 하지만 satisfies 키워드는 2개 모두 만족시킬 수 있습니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;"> </p><h3 style="text-align:justify;"><strong>satisfies 키워드를 사용할 수 있는 사례 소개</strong></h3><p style="text-align:justify;">AB180 Airbridge SDK 팀에서 Web SDK를 개발하는 경우에 ‘satisfies’ 키워드가 가장 필요한 경우는 Unit Test를 위한 의존성을 주입하는 경우입니다. 실제로 사용하는 코드를 통해 ‘satisfies’ 키워드를 사용하는 사례를 소개하고자 합니다.</p><p style="text-align:justify;"> </p><h4 style="text-align:justify;"><strong>국제화를 지원하기 위한 함수의 의존성을 주입하는 사례</strong></h4><pre><code class="language-plaintext">const upcast = <Interface> (implementation: Interface): Interface => ( implementation ) const createDependency = () => {} createDependency.internationalize = () => ({ navigator: upcast<{ language?: string, browserLanguage?: string }>( window.navigator, ), }) const internationalize = < Default extends string, Setting extends { default: Default resource: { [key: string]: TextObject } & { [key in Default]: TextObject } }, TextObject = Setting['resource'][Setting['default']], > ( object: Setting, ): TextObject => { const { navigator } = createDependency.internationalize() const language = ( navigator.language ?? navigator.browserLanguage ) const selector = language?.slice(0, 2) if (selector !== undefined && object.resource[selector] !== undefined) { return object.resource[selector]! } else { return object.resource[object.default] } } export { internationalize, createDependency }</code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;">위 함수는 Web SDK에서 국제화 관련 지원을 할 때 사용하는 함수입니다. 그리고 위 함수는 아래와 같이 활용하는 것이 가능합니다.</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">// navigator.language 가 ko 으로 시작할 때 // => { hello: '안녕하세요.' } // navigator.language 가 en 으로 시작할 때, 그리고 다른 경우 // => { hello: 'hello.' } // type: { hello: string } const text = internationalize({ default: 'en', resource: { en: { hello: 'hello.', }, ko: { hello: '안녕하세요.', }, }, })</code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;">위 함수에서 아랫 부분을 주목해주시면, window.navigator를 upcast라는 함수로 감싼 것을 확인하실 수 있습니다. upcast 함수는 위의 ‘type 정의’ 방법을 함수로 사용해 object key-value의 type 제한에도 활용할 수 있도록 합니다. 이를 통해 createDependency.internationalize 함수가 반환하는 object의 type은 <span style="color:#e52929;"><code>{ navigator: Navigator }</code></span> 가 아닌, <span style="color:#e52929;"><code>{ navigator: { language?: string, browserLanguage?: string } }</code></span>가 됩니다.</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">const upcast = <Interface> (implementation: Interface): Interface => { return implementation } const createDependency = () => {} createDependency.internationalize = () => ({ navigator: upcast<{ language?: string, browserLanguage?: string }>( window.navigator, ), })</code></pre><p style="text-align:justify;"> </p><h4 style="text-align:justify;"><strong>upcast 함수가 필요한 이유</strong></h4><p style="text-align:justify;">이런 type 제한이 왜 필요할까요? 그냥 createDependency.internationalize 함수가 반환하는 object의 type이 <span style="color:#e52929;"><code>{ navigator: Navigator }</code></span>가 되면 안 될까요? 그 이유는 Unit Test에서 찾을 수 있습니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;">internationalize 함수는 navigator를 사용하는 부분을 제외한 모든 부분이 순수합니다. 즉 저는 navigator만 주입할 수 있으면 internationalize 함수를 ECMAScript를 지원하는 모든 환경에서 테스트하는 것이 가능합니다. 하지만 Navigator 타입은 매우 많은 member, method를 지원합니다. 그러므로 이에 대한 mock을 만드는 것은 상당히 힘든 작업입니다.</p><figure class="table"><table><tbody><tr><td><strong>Navigator 타입에 대해서</strong>: <a href="https://developer.mozilla.org/ko/docs/Web/API/Navigator">https://developer.mozilla.org/ko/docs/Web/API/Navigator</a></td></tr></tbody></table></figure><p style="text-align:justify;"> </p><p style="text-align:justify;">대신, upcast 함수를 이용해 type을 <span style="color:#e52929;"><code>{ navigator: { language?: string, browserLanguage?: string } }</code></span>으로 제한해서 필요한 member만 사용한다면, mock을 만들기 매우 쉬워집니다. <span style="color:#e52929;"><code>{ language: 'ko' }</code></span> 이것 또한 훌륭한 mock이 됩니다.</p><p style="text-align:justify;"> </p><pre><code class="language-plaintext">import { internationalize, createDependency } from '...' test('internationalize - ko', () => { createDependency.internationalize = () => ({ navigator: { language: 'ko' } }) const text = internationalize({ default: 'en', resource: { en: { hello: 'hello.', }, ko: { hello: '안녕하세요.', }, }, }) expect(text.hello).toBe('안녕하세요.') })</code></pre><p style="text-align:justify;"> </p><h4 style="text-align:justify;"><strong>‘satisfies’ 키워드 사용</strong></h4><pre><code class="language-plaintext">const createDependency = () => {} createDependency.internationalize = () => ({ navigator: window.navigator satisfies { language?: string, browserLanguage?: string } })</code></pre><p style="text-align:justify;"> </p><p style="text-align:justify;">‘satisfies’ 키워드를 사용하면 위와 같이 upcast 함수 없이도 목적을 달성할 수 있습니다. 또 upcast 함수가 대부분의 minifier에서 최적화되겠지만, 그렇지 못한 minifier도 있을 수 있습니다. (설정에 따라 최적화가 안 될 수도 있고요.) 대신 ‘satisfies’ 키워드를 사용한다면 TypeScript가 지원하는 모든 환경에서 최적화된 결과물을 얻을 수 있습니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;"> </p><h3 style="text-align:justify;"><strong>결론</strong></h3><p style="text-align:justify;">TypeScript 4.9에서 추가되는 ‘satisfies’ 키워드는 언제 어디서나 안전한 upcast를 하는데 도움을 줍니다. 이는 더 안전한 코드를 작성하는데 활용할 수 있으며, 기존에 이를 달성하기 위해 사용하던 도구함수를 제거할 수 있게 해줍니다. 이를 활용해 더 좋은 코드를 작성할 수 있게 될 것이라고 기대합니다.</p><p style="text-align:justify;"> </p><p style="text-align:justify;"><원문></p><p style="text-align:justify;"><a href="https://engineering.ab180.co/stories/satisfies-safe-upcasting">satisfies: 안전한 업캐스팅을 통해 더 안전한 코드작성을 도와주는 새로운 키워드(TypeScript 4.9)</a></p>