회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
국내 유명 IT 기업은 한국을 넘어 세계를 무대로 할 정도로 뛰어난 기술과 아이디어를 자랑합니다. 이들은 기업 블로그를 통해 이러한 정보를 공개하고 있습니다. 요즘IT는 각 기업의 특색 있고 유익한 콘텐츠를 소개하는 시리즈를 준비했습니다. 이들은 어떻게 사고하고, 어떤 방식으로 일하는 걸까요?
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
국내 유명 IT 기업은 한국을 넘어 세계를 무대로 할 정도로 뛰어난 기술과 아이디어를 자랑합니다. 이들은 기업 블로그를 통해 이러한 정보를 공개하고 있습니다. 요즘IT는 각 기업의 특색 있고 유익한 콘텐츠를 소개하는 시리즈를 준비했습니다. 이들은 어떻게 사고하고, 어떤 방식으로 일하는 걸까요?
이번 글은 ‘성과 극대화를 위한 머신러닝 모바일 마케팅 성과분석 솔루션’ 에어브릿지(airbridge.io)를 만드는 AB180 Airbridge SDK 팀의 이야기입니다. 타입스크립트(TypeScript)의 안전한 코드작성을 도와주는 'satisfies' 키워드를 어떻게 활용하는지 소개하고 있습니다.
TypeScript 4.9 Iteration Plan이 공개되면서 이제 9월 20일부터 ‘타입스크립터(TypeScript) 4.9 Beta 버전’을 사용할 수 있게 됩니다. 이번 릴리즈에서 개인적으로 가장 기대하는 기능은 바로 ‘satisfies’ 키워드입니다. 이 글에서는 이 키워드가 무엇이고, 왜 필요하고, 어떤 문제를 해결하는지에 대해서 서술하고자 합니다.
‘satisfies’ 키워드는 literal (값)이나 변수를 안전하게 upcast 하는 기능을 수행합니다. 그런데 이게 어떤 것을 의미할까요?
먼저 아래 예시의 코드를 봐주세요.
const variable = 10
위 코드에서 TypeScript 은 아래와 같이 type을 추론합니다.
좀 더 복잡한 예제를 다루어 봅시다. 아래 코드는 어떻게 추론될까요?
const variable = { grade: "a", score: 90 }
아래와 같습니다.
{ grade: "a", score: 90 }
은 { grade: string, score: number }
type을 가지는 literal이다.{ grade: string, score: number }
이다.
그런데 여기서 문제가 발생합니다. 지금 variable의 type은 { grade: string, score: number }
입니다. 만약 variable을 사용할 때 grade member에만 접근할 수 있도록 강제하려면 어떻게 하면 될까요?
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 }
}
이 방법은 변수를 새로 생성하는 경우에는 문제없이 동작합니다. 위 코드에서 ‘variable1’ 케이스를 보시면 안전하게 assign 될 수 있는 경우일 때 에러 없이 type이 변경되는 것을 보실 수 있습니다. 하지만 ‘variable2’ 케이스를 보시면 안전하게 assign 될 수 없는 경우에는 에러가 발생합니다. 이를 이용해서 저희는 더욱 안전한 코드를 작성할 수 있습니다.
하지만 이 방법은 object의 key-value를 정의할 때 사용할 수 없습니다. ‘variable3’ 케이스를 보시면 해당 key-value 라인에서 type을 강제할 방법이 없습니다. 물론 ‘variable4’, ‘variable5’ 케이스와 같이 직접 type을 새로 정의하면 문제를 해결할 수 있지만, type이 크고 복잡해질수록 이에 대한 관리비용이 증가합니다.
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 }
}
이 방법은 object의 key-value를 정의할 때도 사용할 수 있습니다. ‘variable3’, ‘variable4’ 케이스를 보시면 원하는 type으로 지정이 가능한 것을 볼 수 있습니다.
하지만 이 방법은 위험합니다. ‘variable2’, ‘variable5’ 케이스를 보시면 안전하게 type 변환될 수 있는 경우가 아님에도 type이 변환됩니다. 이는 이후 해당 변수를 사용할 때 버그의 원인이 될 수 있습니다.
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 }
}
‘satisfies’ 키워드는 안전한 type 제한도, object key-value의 type 제한도 할 수 있습니다. satisfies는 as 키워드와 같이 expression에 사용 가능하기 때문에 object key-value의 type을 제한하는 경우에도 쓸 수 있습니다. 또한, satisfies는 as 키워드와 달리 안전한 type 제한을 지원하기 때문에 위험한 ‘variable2’, ‘variable5’ 케이스에 대해서 컴파일 에러를 발생시켜 개발자가 더욱 안전한 코드를 작성할 수 있도록 돕습니다.
위 방법들을 정리하면 아래와 같습니다.
type 정의 | as 키워드 | satisfies 키워드 | |
안전한 type 제한 | O | X | O |
object key-value의 type 제한 | X | O | O |
‘type 정의’ 방법은 안전한 type 제한을 할 수 있지만, object key-value의 type 제한은 할 수 없고, 하기 위해서는 전체 object의 type을 정의해야 합니다. 그리고 as 키워드는 object key-value의 type 제한은 할 수 있지만, 안전한 type 제한을 할 수 있습니다. 하지만 satisfies 키워드는 2개 모두 만족시킬 수 있습니다.
AB180 Airbridge SDK 팀에서 Web SDK를 개발하는 경우에 ‘satisfies’ 키워드가 가장 필요한 경우는 Unit Test를 위한 의존성을 주입하는 경우입니다. 실제로 사용하는 코드를 통해 ‘satisfies’ 키워드를 사용하는 사례를 소개하고자 합니다.
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 }
위 함수는 Web SDK에서 국제화 관련 지원을 할 때 사용하는 함수입니다. 그리고 위 함수는 아래와 같이 활용하는 것이 가능합니다.
// navigator.language 가 ko 으로 시작할 때
// => { hello: '안녕하세요.' }
// navigator.language 가 en 으로 시작할 때, 그리고 다른 경우
// => { hello: 'hello.' }
// type: { hello: string }
const text = internationalize({
default: 'en',
resource: {
en: {
hello: 'hello.',
},
ko: {
hello: '안녕하세요.',
},
},
})
위 함수에서 아랫 부분을 주목해주시면, window.navigator를 upcast라는 함수로 감싼 것을 확인하실 수 있습니다. upcast 함수는 위의 ‘type 정의’ 방법을 함수로 사용해 object key-value의 type 제한에도 활용할 수 있도록 합니다. 이를 통해 createDependency.internationalize 함수가 반환하는 object의 type은 { navigator: Navigator }
가 아닌, { navigator: { language?: string, browserLanguage?: string } }
가 됩니다.
const upcast = <Interface> (implementation: Interface): Interface => {
return implementation
}
const createDependency = () => {}
createDependency.internationalize = () => ({
navigator: upcast<{ language?: string, browserLanguage?: string }>(
window.navigator,
),
})
이런 type 제한이 왜 필요할까요? 그냥 createDependency.internationalize 함수가 반환하는 object의 type이 { navigator: Navigator }
가 되면 안 될까요? 그 이유는 Unit Test에서 찾을 수 있습니다.
internationalize 함수는 navigator를 사용하는 부분을 제외한 모든 부분이 순수합니다. 즉 저는 navigator만 주입할 수 있으면 internationalize 함수를 ECMAScript를 지원하는 모든 환경에서 테스트하는 것이 가능합니다. 하지만 Navigator 타입은 매우 많은 member, method를 지원합니다. 그러므로 이에 대한 mock을 만드는 것은 상당히 힘든 작업입니다.
Navigator 타입에 대해서: https://developer.mozilla.org/ko/docs/Web/API/Navigator |
대신, upcast 함수를 이용해 type을 { navigator: { language?: string, browserLanguage?: string } }
으로 제한해서 필요한 member만 사용한다면, mock을 만들기 매우 쉬워집니다. { language: 'ko' }
이것 또한 훌륭한 mock이 됩니다.
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('안녕하세요.')
})
const createDependency = () => {}
createDependency.internationalize = () => ({
navigator: window.navigator satisfies { language?: string, browserLanguage?: string }
})
‘satisfies’ 키워드를 사용하면 위와 같이 upcast 함수 없이도 목적을 달성할 수 있습니다. 또 upcast 함수가 대부분의 minifier에서 최적화되겠지만, 그렇지 못한 minifier도 있을 수 있습니다. (설정에 따라 최적화가 안 될 수도 있고요.) 대신 ‘satisfies’ 키워드를 사용한다면 TypeScript가 지원하는 모든 환경에서 최적화된 결과물을 얻을 수 있습니다.
TypeScript 4.9에서 추가되는 ‘satisfies’ 키워드는 언제 어디서나 안전한 upcast를 하는데 도움을 줍니다. 이는 더 안전한 코드를 작성하는데 활용할 수 있으며, 기존에 이를 달성하기 위해 사용하던 도구함수를 제거할 수 있게 해줍니다. 이를 활용해 더 좋은 코드를 작성할 수 있게 될 것이라고 기대합니다.
<원문>
satisfies: 안전한 업캐스팅을 통해 더 안전한 코드작성을 도와주는 새로운 키워드(TypeScript 4.9)
좋아요
댓글
공유
공유
작가 홈
좋아요
댓글
스크랩
공유
공유