튜플 타입 — 고정 길이 이종 배열의 완전 정복
TypeScript 튜플 타입의 선언 방법, 명명된 튜플, 옵셔널 요소, rest 요소를 코드 예제와 함께 완전히 정리합니다.
지난 글에서 읽기 전용 배열을 살펴봤다. 이번 글에서는 TypeScript의 튜플 타입(Tuple Types)을 완전히 정복한다. 튜플은 배열과 비슷해 보이지만, 각 위치마다 타입이 고정되고 길이도 미리 정해진다는 점에서 근본적으로 다르다.
튜플 타입이란
튜플은 고정된 길이와 위치별 타입을 갖는 배열이다. 일반 배열이 number[]처럼 “숫자들의 목록”을 표현한다면, 튜플은 [string, number, boolean]처럼 “첫 번째는 문자열, 두 번째는 숫자, 세 번째는 불리언”이라는 구체적인 구조를 표현한다.
// 일반 배열: 모든 요소가 같은 타입
const nums: number[] = [1, 2, 3, 4, 5];
// 튜플: 각 위치에 다른 타입
type Point = [number, number]; // 2D 좌표
type Entry = [string, number]; // 이름과 점수
type RGB = [number, number, number]; // 색상 채널
const origin: Point = [0, 0];
const score: Entry = ["Alice", 95];
const red: RGB = [255, 0, 0];
// 타입 오류: 개수나 타입이 다르면 에러
const wrong1: Point = [1]; // Error: 요소가 부족
const wrong2: Entry = [42, "name"]; // Error: 순서가 틀림
const wrong3: RGB = [255, 0, 0, 1]; // Error: 요소가 너무 많음
배열로는 (string | number)[]라고 써야 하는 것을 튜플로는 더 정확하게 타입을 지정할 수 있다. 컴파일러가 각 인덱스의 타입까지 추적해주기 때문에 훨씬 안전하다.
명명된 튜플 (TS 4.0+)
TypeScript 4.0부터 명명된 튜플(Named Tuples)을 사용할 수 있다. 각 요소에 라벨을 붙여 IDE 툴팁과 오류 메시지를 훨씬 읽기 쉽게 만든다.
// 일반 튜플: 위치만 표시
type Point2D = [number, number];
// 명명된 튜플: 라벨이 있어 의미 명확
type Point2DLabeled = [x: number, y: number];
type ColorRGB = [red: number, green: number, blue: number];
type Range = [start: number, end: number];
라벨은 타입 추론에는 영향을 주지 않는다. 라벨이 있어도 없어도 실제 타입 동작은 동일하며, 오직 가독성과 IDE 지원만 개선된다.
type ColorRGB = [red: number, green: number, blue: number];
const pink: ColorRGB = [255, 182, 193];
// IDE에서 pink[0]에 마우스를 올리면 "(parameter) red: number"로 표시됨
// 라벨 없이 [number, number, number]였다면 그냥 "number"로만 표시
함수 매개변수 타입을 보여줄 때 특히 유용하다:
// 라벨 없는 경우: (number, number) => string — 무슨 숫자인지 불명확
type Formatter = (args: [number, number]) => string;
// 라벨 있는 경우: (args: [width: number, height: number]) => string
type FormatterLabeled = (args: [width: number, height: number]) => string;
옵셔널 요소와 rest 요소
옵셔널 요소
?를 붙이면 해당 위치의 요소를 생략할 수 있다. 단, 옵셔널 요소는 반드시 마지막 부분에만 위치해야 한다.
type Pair = [string, number?];
// [string] 또는 [string, number] 모두 유효
const a: Pair = ["hello"]; // OK
const b: Pair = ["hello", 42]; // OK
const c: Pair = ["hello", 42, true]; // Error: 요소가 너무 많음
// 옵셔널은 끝에서부터만
type ThreeOpt = [string, number?, boolean?]; // OK
// type InvalidOpt = [string?, number]; // Error: 필수 요소가 옵셔널 뒤에 올 수 없음
rest 요소
...를 사용해 가변 길이 부분을 표현할 수 있다. rest 요소는 중간이나 끝에 위치할 수 있다.
// 끝에 rest 요소: 최소 1개의 string 뒤에 number들
type StringsThenNumber = [...string[], number];
const d: StringsThenNumber = [42]; // OK (string 0개, number 1개)
const e: StringsThenNumber = ["a", "b", 42]; // OK
const f: StringsThenNumber = ["a", "b", "c", 99]; // OK
// 처음에 rest 요소
type NumberThenStrings = [number, ...string[]];
const g: NumberThenStrings = [1]; // OK
const h: NumberThenStrings = [1, "a", "b"]; // OK
// 중간에 rest 요소 (TS 4.2+)
type Middle = [string, ...number[], boolean];
구조 분해와 튜플
튜플의 가장 유용한 기능 중 하나는 구조 분해 할당(Destructuring Assignment)과의 궁합이다.
type Point3D = [x: number, y: number, z: number];
const point: Point3D = [10, 20, 30];
// 구조 분해로 의미 있는 변수명 부여
const [x, y, z] = point;
console.log(x); // 10
console.log(y); // 20
// 일부 요소만 추출 (나머지는 _로 무시)
const [px, , pz] = point; // y 무시
const [first, ...rest] = point; // rest = [20, 30]
// 함수 매개변수에서 바로 분해
function formatPoint([x, y, z]: Point3D): string {
return `(${x}, ${y}, ${z})`;
}
formatPoint([1, 2, 3]); // "(1, 2, 3)"
구조 분해 시 TypeScript는 각 변수의 타입을 자동으로 추론한다. x, y, z는 모두 number 타입으로 추론된다.
함수 반환값으로의 튜플
튜플의 가장 강력한 활용 사례는 함수가 여러 값을 반환할 때다. React의 useState 훅이 대표적인 예시다.
// [값, 세터] 패턴
function useState<T>(init: T): [T, (value: T) => void] {
let state = init;
const setState = (value: T) => { state = value; };
return [state, setState];
}
const [count, setCount] = useState(0);
// count: number, setCount: (value: number) => void
const [name, setName] = useState("Alice");
// name: string, setName: (value: string) => void
as const로 더 정확한 타입을 만들 수도 있다:
function makeRange(start: number, end: number) {
return [start, end] as const;
// 반환 타입: readonly [number, number] — 튜플로 추론됨
}
// 주의: as const 없이는 number[]로 추론
function makeRangeWide(start: number, end: number) {
return [start, end]; // 반환 타입: number[]
}
배열 vs 튜플 선택 기준
언제 배열을 쓰고 언제 튜플을 써야 할까?
// 배열을 써야 할 때: 같은 타입, 가변 길이
const primes: number[] = [2, 3, 5, 7, 11];
const names: string[] = ["Alice", "Bob", "Charlie"];
const flags: boolean[] = [true, false, true];
// 튜플을 써야 할 때: 다른 타입, 고정 구조
type HttpResponse = [status: number, body: string];
type Coordinate = [lat: number, lng: number];
type KeyValue<K, V> = [key: K, value: V];
// 경계선 케이스: 같은 타입이지만 의미가 다르면 튜플
type ScreenSize = [width: number, height: number]; // 튜플 권장
// number[]로 쓰면 순서 실수의 위험이 있다
튜플이 유리한 경우:
- 요소 개수가 고정되어 있고 각 위치의 의미가 중요할 때
- 함수가 의미적으로 다른 여러 값을 반환할 때
- 구조 분해로 바로 변수에 바인딩할 때
객체가 유리한 경우:
- 요소가 4개 이상으로 많아질 때
- 각 요소에 의미 있는 이름이 필요할 때
- 일부 필드만 선택적으로 사용할 때
실전 예시: CSV 파싱
튜플은 CSV나 TSV처럼 위치 기반 데이터를 처리할 때 특히 강력하다.
type CSVRow = [name: string, age: number, active: boolean];
function parseRow(line: string): CSVRow {
const [name, ageStr, activeStr] = line.split(",");
return [name.trim(), parseInt(ageStr), activeStr.trim() === "true"];
}
const rows: CSVRow[] = [
"Alice,30,true",
"Bob,25,false",
"Charlie,35,true",
].map(parseRow);
for (const [name, age, active] of rows) {
if (active) {
console.log(`${name} (${age}세): 활성`);
}
}
배열보다 구체적인 타입 정보 덕분에, active를 boolean이 아닌 string으로 사용하면 컴파일 에러로 잡아낼 수 있다.
지난 글: 읽기 전용 배열 — ReadonlyArray와 readonly 수식어 완전 정리
다음 글: 가변 인자 튜플 — 스프레드와 추론으로 복잡한 타입 다루기
읽어주셔서 감사합니다. 😊