Category

번역 및 정리

2 posts in this category.

타입스크립트에서의 Array 타입

타입스크립트에서의 Array 타입

2025. 12. 25.

이 포스트는 Dominik Dorfmeister가 자신의 블로그에 올린 Array types in TypeScript 게시글을 번역한 것이다. 번역하는 과정에서 다소 의역이 있을 수 있으며, 일부 번역에는 사견이 포함되어있기도 하다.올해(23년) 초에 있었던 일입니다. Total TypeScript의 저자이자 과거 vercel에서도 근무하던 개발자 Matt Pocock 씨가 트위터를 통해 한 가지 설문 조사를 진행하더군요. 그런데 설문 결과가 저를 꽤 당혹스럽게 했습니다.해당 설문은 타입스크립트에서 Array 타입을 지정할 때 Array<string> 과 string[] 중 어떤 방식을 선호하는지에 대한 것이었습니다. 전자는 제네릭 구문이고 후자는 Array 구문이죠. 그리고 그 결과는 아래와 같았습니다.<blockquote class="twitter-tweet"><p lang="en" dir="ltr">What do you use more often in TS?</p>— Matt Pocock (@mattpocockuk) <a href="https://twitter.com/mattpocockuk/status/1544083145833717767?ref_src=twsrc%5Etfw">July 4, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>분명히 해둡시다. 두 방식 사이에 기능적인 차이는 전혀 없습니다. 어떤 방식을 선택하는지는 전적으로 개개인의 취향에 달려있다고 봐야겠죠. 당신이 어떤 방식을 선호하는지와 관계 없이 하나의 방식을 일관적으로 유지하는 것이 더 중요합니다. 그리고 이 경우 eslint의 array-type 룰을 사용하면 도움이 됩니다.그렇기는 하지만, 저는 트위터에서 string[]에 투표한 78%의 사람들이 완전히 틀렸다고 생각합니다. 일반적으로 저는 이런 식으로 무언가를 단언하거나 하지는 않습니다. 하지만 이 경우라면 제네릭 표기법을 사용하는 경우가 (무조건 더 좋다고는 하기 어렵지만서도) 훨씬 더 낫다고 확신합니다.누군가가 "나는 string[]과 같은 Array 표기법이 더 좋은뎁쇼?"라고 말할 때마다 저는 그렇지 않다는 사실을 사례를 들어 설명합니다. 제 이야기를 들으면 보통은 제네릭 구문을 사용하는 편이 더 좋다는 사실을 납득하더라고요. 오늘은 제네릭 표기법을 사용하는 편이 더 좋다는 사례를 설명하기에 앞서, 사람들이 Array 표기법을 선호하는 이유 한 가지를 살펴보겠습니다.더 짧다!사실 더 짧다는 건 확실히 장점이라 볼 수 있습니다. 키보드 두드리는 횟수가 더 적잖아요. 코드가 짧다면 유지 관리에 이점이 있기도 하고요. 그런데 let은 const보다 짧습니다. 여러분은 오만군데에 let을 쓰시는 분이신가요? 하하, 저도 이 논쟁을 알고 있지만 깊게 파고들지는 않겠습니다.하지만 진지하게 생각해봅시다. 무언가 짧다고 해서 언제나 더 좋아지는 것은 아닙니다. i는 index보다 짧고 d는 dashboard보다 짧지만, 우리는 코드를 쓰는 것보다 훨씬 더 자주 읽는다는 것을 기억해야 합니다. 코드를 짧게 쓰는 것에 집중하기보다는 쉽게 읽히도록 하는 것에 집중하는 것이 더 낫습니다. 그리고 이로부터 제네릭 표기법의 장점의 첫 번째 장점이 드러납니다.가독성우리는 보통 왼쪽에서 오른쪽으로 글을 읽습니다. 따라서 중요한 것을 앞 쪽에 배치하는 것은 어쩌면 당연하다고도 할 수 있겠습니다. 우리는 "array of strings"나 "array of numbers"와 같은 방식으로 말하니까요.* 한국 사람은 문자 배열이나 숫자 배열과 같은 방식으로 말하니까 string[]이 더 가독성 좋은 게 아닐까?// ✅ reads nice from left to right function add(items: Array<string>, newItem: string) // ❌ looks very similar to just "string" function add(items: string[], newItem: string)​이는 배열의 유형이 다소 긴 (가령 타입이 어딘가에서 추론된) 경우에 특히 중요합니다. IDE는 일반적으로 Array 표기법으로 배열 유형을 표시하므로, 때로는 객체 배열 위로 마우스를 가져가면 다음과 같은 결과가 나타납니다.const options: { [key: string]: unknown }[]저에게는 이게 options 객체처럼 읽혀집니다. 맨 끝의 []를 확인해야 비로소 이게 배열이라는 걸 알 수 있으며, 슬쩍 보고 넘어가면 큰 실수를 할 수도 있게 됩니다. 객체에 속성이 많을 수록 상황은 더욱 악화됩니다. 콘텐츠가 길어지면 팝오버에 스크롤 막대가 생기고, 마지막에 []이 있다는 걸 보는 게 거의 불가능해지기 때문입니다. 물론 이것은 IDE에 따라 다를 수 있습니다. 하지만 처음부터 Array라는 것을 보여주면 생기지 않을 문제이기도 합니다.const options: Array<{ [key: string]: unknown }>이렇게 되면 여러 줄에 걸쳐 있어도 Array 타입임을 쉽게 알 수 있습니다. 어쨌든, 이것이 제네릭 표기법의 유일한 장점은 아니기 때문에 넘어가겠습니다.Readonly Arrays현실을 직시해봅시다. 우리가 함수의 아규먼트로 받는 대부분의 배열은 실수로 변경하지 않도록 readonly 처리를 해주어야 합니다. 이미 별도의 포스트에서 해당 주제에 대해 다루고 있으므로 관심 있으신 분들은 한 번씩 읽어보시기 바랍니다. 아무튼 제네릭 표기법을 사용하는 경우 Array를 ReadonlyArray로 일괄 변경해주는 것만으로도 끝날 문제입니다. 하지만 Array 표기법을 사용하면 두 파트로 나누어 작성해야 하죠.// ✅ prefer readonly so that you don't accidentally mutate items function add(items: ReadonlyArray<string>, newItem: string) {} // ❌ "readonly" and "Array" are now separated function add(items: readonly string[], newItem: string) {}물론 이렇게 나누어 작성하는 게 대단히 어렵거나 하다는 건 아닙니다. 다만, 같은 일을 하는 built-in utility 타입이 존재하기 때문에 readonly를 배열과 튜플에서만 작동하는 예약어로써 받아들이가 쉽지 않습니다. 특히 readonly와 []가 나누어져있기 때문에 읽을때 흐름이 끊긴다니까요?물론 이것도 그렇게까지 대단한 문제는 아닙니다. 이제부터 진짜 짜증나는 문제들을 살펴보겠습니다. Union types앞서 살펴본 add 함수가 숫자도 허용하도록 하려면 어떻게 해야할까요? items가 숫자 배열을 받게 해야하는데, 만약 제네릭 표기법을 사용하고 있었다면 문제가 없습니다.// ✅ works exactly the same as before function add(items: Array<string | number>, newItem: string | number) {}그러나 Array 표기법을 사용하면 상황이 조금 달라집니다.// ❌ looks okay, but isn't function add(items: string | number[], newItem: string | number) {}오류를 즉시 발견할 수 없다면 그것은 그것대로 문제가 될 수 있습니다. 그리고 이 경우 오류가 너무 숨겨져있어서 알아차리는데 시간이 좀 걸립니다. 해당 함수를 실제로 구현하여 어떤 오류가 나타나고, 왜 이것이 문제인지 살펴보겠습니다.// ❌ why doesn't this work 😭 function add(items: string | number[], newItem: string | number) { return items.concat(newItem) }위 함수는 'string' 형식은 'ConcatArray<number> & string' 형식에 할당할 수 없습니다.ts(2769) 라는 오류가 발생합니다. 문제를 해결하기 위해서는 연산자 우선순위에 대해 알아야 합니다. [] 연산자가 유니온 연산자보다 강력하므로, 우리는 사실 items가 string과 number[] 타입을 받도록 한 셈입니다.(string | number)[]처럼 작성하면 의도한대로 동작하게 됩니다. 제네릭 표기법을 사용하면 꺽쇠 괄호를 사용해 이를 구분하기 때문에 문제가 되지 않습니다.아직도 제네릭 표기법이 더 좋다고 확신을 못하겠다고요? 좋습니다. 마지막 사례를 확인해봅시다.keyof자바스크립트 개발자들이 자주 사용하는 pick 또는 omit 같은 함수를 구현하려면 객체와 이 객체의 가능한 key 배열을 파라미터로 함수에 전달해야 합니다.const myObject = { foo: true, bar: 1, baz: 'hello world', } pick(myObject, ['foo', 'bar'])만약 두 번째 파라미터가 가능한 key만을 받기를 원한다면 keyof 연산자를 사용하여 구현할 수 있습니다.function pick<TObject extends Record<string, unknown>>( object: TObject, keys: Array<keyof TObject> ) {}이를 Array 표현법으로 변경하면 아래와 같을 겁니다.function pick<TObject extends Record<string, unknown>>( object: TObject, keys: keyof TObject[] ) {}놀랍게도 이렇게 바꾼다고 해서 에러가 발생한다거나 하지는 않습니다. 그러나 에러가 없다는 것은 문제가 됩니다. 왜냐면 여기에는 에러가 있기 때문입니다! 함수를 선언하는 시점에는 확인할 수 없지만, 이를 호출하면 'string[]' 형식의 인수는 'keyof TObject[]' 형식의 매개 변수에 할당될 수 없습니다.ts(2345) 라는 에러가 발생하기 때문입니다.실제 코드베이스에서 이 오류를 처음 봤을 때, 못해도 5분 정도는 그냥 보고 있기만 했던 거 같습니다. 왜 제 key들이 string이 아니라는 것인지 이해할 수 없었습니다. 왼쪽과 오른쪽을 바꿔보고, 타입을 추출해서 별칭type aliases을 달아주면 좀 더 이해할 수 있는 오류를 얻지 않을까 싶었는데, 실패했습니다. 그러다가 문득 그런 생각이 들었습니다. 또 괄호 문제인가?네, 또 괄호 문제였습니다. 그리고 저는 좀 슬퍼지더군요. 왜 이딴 것까지 신경을 써야 하는 거지? 오늘날까지도 저는 keyof TObject[]가 무얼 나타내는지 알지 못합니다. 그저 우리가 원하는 방식으로 동작하게 하는 방법이 (keyof TObject)[]라는 것만 확인했습니다.function pick<TObject extends Record<string, unknown>>( object: TObject, keys: (keyof TObject)[] ) {}고오오오오오맙다, ㅈ같은 표현 방식아.아무튼 이것들이 Array 표기법을 사용할 때 직면했던 문제들입니다. 안타깝게도 Array 표기법은 eslint 규칙의 기본 설정이고, 아직 많은 사람들이 이를 선호한다는 게 안타깝습니다. 몇몇 IDE와 타입스크립트 플레이그라운드에서 "명백히 제네릭 표기법을 사용했음에도" Array 표기법을 사용한 것처럼 타입을 표시해주는 것도 좀 그렇습니다.어쩌면 이 포스트가 제네릭 표기법이 더 낫다는 것을 커뮤니티에 납득시키는 데 도움이 될 수도 있겠습니다. 어쩌면 이 포스트를 읽고 많은 분들이 제네릭 표기법을 실제로 사용하게 될 지도 모르지요. 그렇다면, 어쩌면, 정말 어쩌면, IDE와 타입스크립트 플레이그라운드와 같은 도구들이 뒤따를지도요.

TOON

TOON

2025. 12. 25.

최근 대규모 언어 모델을 활용한 애플리케이션 개발이 일반화되면서, 프롬프트에 구조화된 데이터를 어떻게 전달할 것인지가 점점 더 중요한 문제로 떠오르고 있다. 기존의 JSON은 웹 API의 표준으로서 널리 사용되고 있지만, LLM에 데이터를 전달하는 데에는 JSON이 불필요한 토큰 사용을 유발하거나 반복 구조를 지나치게 장황하게 만드는 경우가 많다.이러한 문제를 해결하기 위해 등장한 형식이 TOON(Token-Oriented Object Notation)이다. TOON은 JSON 데이터 모델을 기반으로 설계되었지만, 프롬프트 맥락에서 효율성과 가독성을 극대화하기 위해 문법과 구조를 재구성한 포맷이다. 특히 반복되는 객체 배열과 같은 구조적 데이터를 압축적으로 표현하면서도 사람이 읽기 쉽게 유지하는 것을 핵심 목표로 한다.TOON은 JSON과 동일하게 객체, 배열, 문자열, 숫자, Boolean, null과 같은 데이터 타입을 모두 표현할 수 있으며, 중요한 점은 이러한 데이터가 JSON ↔ TOON 간 변환되더라도 손실 없이 동일하게 유지된다는 점이다.즉, TOON은 고유한 데이터 모델을 도입하지 않고 오직 표현 방식을 개선하는 데에 집중하며, 결과적으로 인공지능 프롬프트 환경에서 “데이터 표시 최적화 방식”이라는 역할을 갖는다. JSON이 웹 표준 데이터 포맷이라면, TOON은 LLM 프롬프트용 데이터 포맷에 더 가깝다고 볼 수 있다. TOON의 가장 큰 특징은 들여쓰기 기반의 중첩 구조와 반복 객체 배열을 간결하게 줄여내는 배열 문법이다. 일반적인 JSON 객체는 {}와 []로 감싸야 하고 모든 속성명을 매번 반복해야 한다. 예를 들어 사용자 객체 배열을 JSON으로 표현하면 각 객체마다 모든 필드를 반복해야 한다.반면 TOON에서는 배열을 정의할 때 필드명을 한 번만 선언하고, 이후 각 객체는 CSV와 유사한 형태로 이어서 나열하면 된다. 이 방식은 특히 JSON에서 가장 많은 토큰을 소비하는 패턴인 “objects inside arrays”를 압축하는 데에 효과적이다.JSON에서 다음과 같은 구조가 있다고 가정하자.{ "users": [ { "id": 1, "name": "Alice", "role": "admin" }, { "id": 2, "name": "Bob", "role": "editor" } ] }이를 TOON으로 표현하면 다음과 같이 단 한 번의 선언으로 필드를 정의할 수 있다.users[2]{id,name,role}: 1,Alice,admin 2,Bob,editor여기서 배열의 길이와 필드명이 그 자체로 하나의 스키마처럼 동작하여 LLM이나 사람 모두에게 구조를 한눈에 보여준다. JSON은 기호와 반복된 필드명으로 인해 데이터보다 “구조를 표현하기 위한 텍스트”가 훨씬 많이 등장하는 반면, TOON은 실제 데이터 비중이 훨씬 높아진다.그 결과 LLM 프롬프트에서는 평균적으로 30%에서 60% 정도의 토큰을 절감할 수 있다는 분석도 있다. 이러한 절감은 곧바로 비용과 성능에 영향을 미치며, 특히 긴 문맥을 요구하는 프롬프트에서는 더 큰 차이를 만든다.TOON은 배열뿐 아니라 일반적인 중첩 객체도 들여쓰기 기반으로 표현한다. 예를 들어 JSON에서 다음과 같이 작성하던 구조가 있다고 하면,{ "user": { "name": "Alice", "isActive": true, "languages": ["Korean", "English"] } }TOON에서는 다음과 같은 형태로 변환된다.user: name: Alice isActive: true languages[2]: Korean,EnglishTOON과 JSON의 차이를 문맥 중심으로 비교하면 몇 가지 중요한 관점이 드러난다. JSON은 여전히 API 통신, 웹 개발, 서버 간 데이터 교환을 위한 사실상의 표준 포맷이다. 모든 언어와 플랫폼이 JSON을 지원하며, 그 생태계는 이미 충분히 성숙해 있다. 반면 TOON은 “LLM에게 데이터 구조를 설명하기 위한 포맷”이라는 명확한 목적을 가지고 있다.즉, JSON이 기계 친화적인 형식이라면 TOON은 LLM 친화적이며, 프롬프트에 삽입되는 데이터에 최적화된 표현식이라고 할 수 있다. 반복 구조를 가진 데이터를 프롬프트에 넣어야 하거나, 데이터 가독성을 유지하면서 토큰 비용을 줄여야 하는 시나리오라면 TOON은 JSON보다 훨씬 실용적인 대안으로 작동한다.TOON은 실제 업무에서도 몇 가지 구체적인 사용 시나리오를 가진다. 우선 LLM을 통해 데이터 분석, 요약, 패턴 추출 등을 수행할 때 대량의 JSON 데이터를 그대로 넘기면 토큰 비용이 매우 커진다. 이때 JSON을 TOON으로 변환한 뒤 프롬프트에 삽입하면 같은 데이터를 훨씬 적은 토큰으로 전달할 수 있다. 또한 기능 명세서나 요구사항 문서처럼 개발자와 기획자, 디자이너가 함께 읽는 문서에서는 JSON보다 TOON이 훨씬 읽기 쉽고 자연스럽기 때문에 문서 가독성을 크게 향상시킬 수 있다. 마지막으로 LLM 기반 서비스에서 내부 데이터 포맷을 JSON으로 유지하되, LLM에게 전달되는 모든 데이터만 TOON으로 변환해 사용하는 전략은 데이터 구조의 일관성과 프롬프트 효율성을 동시에 확보할 수 있는 효과적인 접근 방식이다. 결론적으로 TOON과 JSON은 서로 경쟁하는 포맷이라기보다, 서로 다른 목적을 가진 두 가지 표현 방식이다. JSON은 웹과 서버 간 데이터 교환의 절대적인 표준으로 계속 사용될 것이며, TOON은 LLM 환경에서 구조화된 데이터를 더 효율적으로 전달하기 위한 전문적인 표현 방식으로 활용될 것이다.