🖥️

4. 배열과 튜플

 

4.1 배열(Array)

4.1.1 배열이란?

지난 장의 ‘타입 알아보기 챕터’에서 배열에 대해 간단히 알아보았다. 자바스크립트 뿐만 아니라 타입스크립트에서도 배열은 유용하게 사용하는 데이터 타입이므로 이번 챕터에서 조금 더 자세하게 다뤄보려고 한다.
 
일반적으로 변수를 사용하는 것과 배열을 사용해서 데이터를 관리하는 것에는 차이가 있다. 바로 값의 개수이다. 변수를 사용하는 경우에는 한 번에 하나의 값만 할당하여 사용할 수 있지만, 배열을 사용하는 경우 여러 개의 값을 담아 사용할 수 있다. 배열 안에 담긴 값 하나하나를 요소라고 부르며 인덱스를 통해 배열 안의 요소에 접근이 가능하다. 인덱스를 사용한다는 점에서 배열은 순번을 중요하게 여긴다는 것을 알 수 있다.
 

4.1.2 배열 선언 방식

자바스크립트의 배열의 선언 방식에는 배열 리터럴[] 대괄호를 사용하여 선언하는 방식과 Array 생성자 함수를 사용하는 방식으로 두 가지가 존재한다. Array 생성자 함수로 배열을 만드는 것은 조금 번거롭고 대부분 배열 리터럴 방식을 많이 사용하므로 이 책에서는 배열 리터럴을 사용하겠다.
 
  • 자바스크립트의 배열 선언 방식
// 배열 리터럴 let arr = [item1, item2, item, ...]; // Array 생성자 함수 let arr = new Array();
 
  • 타입스크립트의 배열 선언 방식
// 배열 리터럴 let arr: 데이터타입[] = [item1, item2, item, ...]; // 제네릭 배열 let arr: Array<데이터타입> = [item1, item2, item3, ...];
 
타입스크립트의 배열도 마찬가지로 두 가지 선언 방법이 존재한다. 첫 번째로 배열 리터럴을 통해서 선언한 경우에는 변수 바로 뒤에 : 데이터 타입[] 을 적어주며, 우항의 [] 안에는 앞서 명시한 데이터 타입만 배열의 요소로 담길 수 있다. 두 번째로는 제네릭 배열을 통해 Array<데이터타입> 과 같은 형태로 선언을 할 수 있다.
 
하나의 배열에 다양한 데이터가 섞여 들어와도 에러를 내뿜지 않는 자바스크립트의 배열과 다르게 타입스크립트의 배열은 보다 엄격하고 명시적으로 데이터를 다룰 수 있다는 장점이 있다. 아래 예시를 통해서 배열에 요소를 직접 추가했을 경우 발생하는 상황을 확인해 보겠다.
 
  • 예시1
// JS let fruits = ['사과', '오렌지']; fruits.push('복숭아'); fruits.push(5000); fruits.push(true); console.log(fruits); // ['사과', '오렌지', '복숭아', 5000, true];
위의 예시를 보면 자바스크립트에서는 하나의 배열에 동일하지 않은 타입의 요소를 넣어도 에러 없이 fruits 배열에 잘 들어가는 모습을 볼 수 있다.
 
  • 예시2
// TS let fruits: string[] = ['사과', '오렌지']; fruits.push('복숭아'); fruits.push(5000); // 'number' 형식의 인수는 'string' 형식의 매개 변수에 할당될 수 없다.ts(2345) fruits.push(true); // 'boolean' 형식의 인수는 'string' 형식의 매개 변수에 할당될 수 없다.ts(2345) console.log(fruits);
 
notion imagenotion image
 
반면에 타입스크립트를 사용한 위의 예시는 fruits 라는 배열에 string 타입의 배열 요소만 들어올 수 있는 제약이 있기 때문에 ‘복숭아’라는 문자열 데이터는 문제없이 push가 되지만, number 타입인 5000과 boolean 타입인 true 에서는 할당이 불가하다는 에러가 뜨고 있다. 만약 두 종류 이상의 데이터 타입을 지정해서 할당하고 싶다면 어떻게 하면 좋을까? 이 부분은 4.1.3 배열 유연하게 사용하기에서 알아보도록 하자.
 
 

4.1.3 배열에 타입 지정하기

타입스크립트의 배열은 데이터 타입을 단일 타입다중 타입으로 지정해서 사용해 볼 수 있다. 앞에서 이야기한 ‘명시한 데이터 타입만 배열의 요소로 담길 수 있다.’의 경우가 단일 타입으로 사용한 경우이고, 이번에 다뤄볼 내용은 다중 타입으로 원하는 데이터 타입만 지정하여 사용하는 경우이다. 단일 타입은 간단하게 설명하고 넘어가겠다.
 
  • 단일 타입
단일 타입은 아래 예시와 같이 직접 명시한 하나의 데이터 타입의 요소만 배열에 담길 수 있다.
// string 타입의 요소만 담을 수 있다. let drinksName: string[] = ['콜라', '사이다', '환타']; // number 타입의 요소만 담을 수 있다. let drinksPrice: number[] = [1200, 900, 2500]; // boolean 타입의 요소만 담을 수 있다. let drinksStatus: boolean[] = [true, false];
 
  • 다중 타입
배열 안에 두 개 이상의 데이터 타입을 명시해서 넣고자 할 때 사용하며 여기에는 유니온 타입이 있다.
// 배열 리터럴 let coffeeC: (string | number)[] = ['카라멜마끼아또', 4500]; coffeeC.push('커피쿠폰'); coffeeC.push(true); // 'boolean' 형식의 인수는 'string | number' 형식의 매개 변수에 할당될 수 없다. // 제네릭 배열 let coffeeD: Array<string | number> = ['카라멜마끼아또', 4500]; coffeeD.push('커피쿠폰'); coffeeD.push(true); // 'boolean' 형식의 인수는 'string | number' 형식의 매개 변수에 할당될 수 없다.
 
  • any 타입
다중 타입에 속하지는 않지만 유연하게 사용할 수 있는 타입으로 any 타입과 유니온 타입을 비교해 볼 수 있다. any 타입을 사용하면 자유롭게 원하는 타입의 데이터를 담을 수 있다는 장점이 있다. 하지만 그렇게 되면 데이터 요소의 일관성도 사라질뿐더러 타입스크립트를 사용하는 목적이 사라진다. 둘 이상의 타입을 적용하고자 할 땐, any 타입은 지양하도록 한다.
// 배열 리터럴 let coffeeA: any = ['카라멜마끼아또', 4500, true, function(){}, null]; console.log(coffeeA); // ['카라멜마끼아또', 4500, true, function(){}, null] // 제네릭 배열 let coffeeB: Array<any> = ['아메리카노', 4000, false, function(){}]; console.log(coffeeB); // ['아메리카노', 4000, false, function(){}]
 
⚠️
유니온 타입을 이용해서 배열을 작성할 때의 주의점
 
만일 배열로 담길 수 있는 요소의 데이터 타입으로 string과 number 두 가지의 타입을 넣고 싶다면, (string | number)[] 의 형태로 사용해야 한다. 아래의 예시에서 string | number[]string 또는 number[] 타입의 요소가 들어갈 수 있음을 의미하기 때문에. 유니온 타입을 이용해서 작성할 때는 string | number[]()괄호로 감싼 형태인 (string | number)[] 의 형태로 작성해줘야 한다.
 
// string 타입 → 유니온타입 X let coffee: string | number[] = 'americano'; // number[] 타입 → 유니온타입 X let coffee: string | number[] = [5000, 6000]; // 유니온타입 O let coffee: (string | number)[] = [’americano’, 5000];
 
() 괄호로 감싸주지 않은 채로 coffee 배열에 문자열을 추가했더니 아래와 같은 에러가 나타난다.
notion imagenotion image
 
 

4.1.4 배열의 유용한 메서드

자바스크립트에는 배열과 함께 사용하기에 유용한 메서드가 있다. 타입스크립트에 적용하며 다뤄보려고 한다.
 
  • filter
filter 메서드는 배열을 순회하며 조건에 일치하는 하나 이상의 요소를 모아 새로운 배열을 반환한다. 새로운 배열을 반환하기 때문에 예시의 원본 데이터인 price를 훼손하지 않는다는 장점이 있다. 아래 예시는 가격 데이터를 배열에 담아 필터 메서드로 가격을 필터링하는 기능의 일부이다.
// 형태 arr.filter(콜백함수); arr.filter((요소: 타입, 인덱스?: number): boolean => {});
let price: number[] = [10000, 12000, 15000, 20000]; // 콜백함수 function priceFilter(item: number, index: number): boolean { return item <= 15000; } // 함수 표현식 let products1: number[] = price.filter(priceFilter); // 화살표 함수 let products2: number[] = price.filter(item => item <= 15000); console.log(products); // [10000, 12000, 15000]
 
  • map
map 메서드는 원본 데이터를 훼손하지 않고 기존 배열을 유지하며 배열 요소 전체를 대상으로 함수를 호출해서 호출 결과를 요소값에 적용하여 다른 값으로 매핑 한 새로운 배열을 생성한다. map 메서드는 배열 요소 전체를 대상으로 함수를 호출하고, 호출 결과를 요소값에 적용하여 다른 값으로 매핑 한 새로운 배열을 반환한다. filter 메서드와 마찬가지로 원본 데이터를 훼손하지 않고 기존 배열을 유지한다.
 
map 메서드는 배열 요소 전체를 대상으로 함수를 호출하고, 호출 결과를 요소값에 적용하여 다른 값으로 매핑 한 새로운 배열을 반환한다. filter 메서드와 마찬가지로 원본 데이터를 훼손하지 않고 기존 배열을 유지한다.
// 형태 arr.map(콜백함수); arr.map((요소: 타입, 인덱스?: number) => {});
let num: number[] = [2, 3, 4, 5, 6]; // 콜백함수 function multifleResult(item: number): number { return item * item; } // 함수 표현식 let result1: number[] = num.map(multifleResult); // 화살표 함수 let result2: number[] = num.map(item => item * item); console.log(result); // [4, 9, 16, 25, 36]
 
  • reduce
reduce는 변환한다는 의미를 가진다. 요소를 순회하며 함수를 호출하고 나온 반환값은 또다시 다음 함수 호출에 전달하여 배열 각 데이터를 순회하여 하나의 값을 반환한다. 아래 예제는 reduce 메서드로 정수 배열의 합을 구한 것이다.
// 형태 arr.reduce(콜백함수, 초기요소); arr.reduce((누적값: 타입, 현재요소: 타입, 인덱스: 타입, 현재배열: 타입): 타입 => {}, 초기요소);
let arr: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // 콜백함수 function accSum(accumulator: number, currentValue: number): number { return (accumulator + currentValue); } // 함수 표현식 let sum1: number = arr.reduce(accSum, 0); // 화살표 함수 let sum2: number = arr.reduce((accumulator: number, currentValue: number) => accumulator + currentValue, 0); console.log(sum); // 55
 
위의 3가지 메서드 외에도 배열에서 자주 사용되는 메서드를 표로 정리해 보았다.
메서드명
메서드의 기능
pop()
배열의 마지막 요소를 제거하고 해당 요소를 반환한다.
push()
배열의 마지막에 요소를 추가하고 배열의 새 길이를 반환한다.
shift()
배열의 첫 번째 요소를 제거하고 해당 요소를 반환한다.
unshift()
배열의 맨 앞에 요소를 추가하고 새로운 배열의 새 길이를 반환한다.
sort()
배열의 요소를 문자열의 유니코드 순서에 따라 정렬한다. 기본적으로 오름차순 정렬이다.
concat()
인자로 주어진 배열이나 값들을 기존 배열에 합쳐서 새 배열을 반환한다.
join()
배열의 모든 요소를 문자열로 결합하여 반환한다.
includes()
인자로 주어진 요소가 배열에 포함되어 있는지 확인한다.
splice()
배열로부터 인자로 주어진 특정 범위를 삭제하거나 새로운 값을 추가 또는 기존 값을 대체한다.
slice()
배열로부터 인자로 주어진 특정 범위를 복사한 값들을 새로운 배열 객체로 반환한다.

4.2 튜플(Tuple)

4.2.1 내겐 너무 낯선 튜플

자바스크립트에는 없지만 타입스크립트에는 있는 타입이 있다. 이번 챕터에서는 그 중에 하나인 튜플에 대해 자세히 알아보고자 한다.
 
사전적 의미의 튜플은 ‘셀 수 있는 수량의 순서 있는 열거’이다. 타입스크립트에서 튜플이란 요소의 길이와 타입이 고정된 배열을 의미하는데, 여기서 요소란 배열에 들어있는 각각의 값을 말한다. 튜플도 배열의 한 종류이므로 배열 리터럴인 []를 사용하여 선언하며, 요소를 담은 대괄호를 변수에 할당한다. 다만, 배열 선언 방식과 다른 점이 있다면 배열 요소의 타입 순서와 길이가 중요하다는 것이다. 아래 코드를 통해 살펴보자.
 
// 튜플 타입 선언 let drink: [string, string]; // 튜플 초기화 drink = ['cola', '(주)콜라공장']; // 튜플 타입 선언과 초기화를 동시에 하는 법 let drink: [string, string] = ['cola', '(주)콜라공장'];
배열에 string 타입의 요소를 2개 담고 싶다면 [string, string] 으로 명시해 주어야 한다. 즉, 길이가 2이고 string 타입의 값만 와야 하는 배열임을 알려준다. 위의 예시에서는 string 타입만 필요했지만, 이것이 하나의 데이터 타입만 지정할 수 있다는 뜻은 아니다. 즉, 요소들의 타입이 모두 같을 필요는 없다는 의미이다.
 
let drink: [string, number] = ['cola', 2500];
이처럼 서로 다른 데이터 타입을 같이 사용할 수도 있다. 하지만, 지정해준 배열 타입의 순서가 아닌 임의의 순서로 값을 넣게 되면 어떻게 될까?
 
notion imagenotion image
 
당연하게도 위와 같이 에러가 발생하는 것을 볼 수 있다. 에러 메시지를 살펴보면, string 타입이 와야 할 위치에 number 타입의 값을 넣을 수 없다고 한다. 앞에서 언급했듯 튜플은 요소의 타입이 고정된 배열이므로 지정한 타입 순서대로 값을 할당해야 한다. 요소의 길이 또한 마찬가지로 지정한 길이와 같아야 하므로 아래와 같은 코드는 에러를 발생시킨다.
 
notion imagenotion image
drink에는 2개의 요소만 허용한다고 지정하였으나 배열의 요소에 3개를 할당하려고 하였기에 발생하는 에러이다. 이렇게 튜플은 일반적인 배열과 다른 특징을 가지고 있다. 하지만, 배열의 한 형태이기 때문에 공통점도 존재한다. 그 예시를 아래에서 살펴보자.
 
  • 인덱스 접근
인덱스로 접근하여 데이터의 값을 변경할 수 있다.
let drink: [string, number] = ['cola', 2500]; // 인덱스로 접근해 값을 변경 drink[0] = 'soda'; drink[1] = 3000; console.log(drink); // ['soda', 3000] 출력
 
  • 배열 메서드 사용
배열에서 사용하는 메서드를 튜플에도 적용 가능하다. 다음은 메서드를 사용해 보기 위한 예시로 타입은 신경 쓰지 않기로 한다. 아래의 세 가지 메서드 외에도 다양한 배열 관련 메서드를 사용할 수 있다.
let drink: [string, number] = ['cola', 2500]; console.log(drink.includes('cola')); // true 출력 console.log(drink.join("&")); // cola&2500 출력 console.log(drink.map((item) => item + "_map")); // ["cola_map", "2500_map"] 출력
 
  • 스프레드 문법 사용
튜플에서 요소의 타입은 알지만 길이에 대해 알 수 없을 때, 스프레드 문법을 사용하여 나머지 데이터를 길이에 상관없이 특정한 타입으로 나타낼 수 있다.
let drink: [string, ...number[]] = ['cola', 2500, 2000, 1000];
 

4.2.2 ReadOnly

앞에서 살펴봤듯 튜플은 요소의 길이와 타입을 엄격히 제한하는 듯 보이지만 사실은 맹점이 존재한다. 바로, 배열의 메서드인 push를 사용하면 튜플의 규칙을 무시하고 배열의 길이가 늘어나게 된다는 점이다.
아래의 코드를 실행해 보면 push 메서드로 넣은 데이터가 배열의 뒤에서부터 추가되었음을 알 수 있다. 이는 튜플의 목적에 위배되는 것으로 해결할 필요가 있다.
 
let drink: [string, number] = ['cola', 2500]; drink.push('(주)콜라공장'); drink.push('22-10-13'); console.log(drink);
# 출력결과 [ 'cola', 2500, '(주)콜라공장', '22-10-13' ]
 
이러한 튜플의 맹점을 해결하기 위해 readonly 키워드를 사용하여 요소의 타입 순서와 길이를 완벽히 고정시킬 수 있다. 아래의 코드를 작성해 보면 push 메서드는 읽기 전용 타입에 존재할 수 없다는 에러가 뜨는 것을 볼 수 있다.
 
let drink: readonly[string, number] = ['cola', 2500]; drink.push('(주)콜라공장'); // error drink.push('22-10-13'); // error console.log(drink);
notion imagenotion image
 
선언한 변수를 읽기 전용으로 만들어주는 readonly 키워드는 type[] 형태의 배열 및 튜플 리터럴 형식에서만 허용이 된다. 아래의 코드를 작성해 보면 ‘readonly 형식 한정자는 배열 및 튜플 리터럴 형식에서만 허용된다’는 에러가 뜬다.
 
notion imagenotion image
Array<T> 형태인 제네릭으로 배열을 선언할 때는 readonly로 사용할 수 없고 ReadonlyArray<T> 형태로 사용해야 한다.
let drink: ReadonlyArray<string> = ['cola', '(주)콜라공장']; drink.push('22-10-13'); // error - Property 'push' does not exist on type 'readonly string[]'.
 
다음과 같이 Readonly 키워드를 사용하면 제네릭 타입도 읽기 전용으로 지정할 수 있어서 데이터를 마음대로 수정할 수 없다.
type Drink = { name: string; company: string; } type DrinkReadonly = Readonly<Drink>; let drink: DrinkReadonly = {name: 'cola', company: '(주)콜라공장'}; drink.name = 'cool_cola'; // error - Cannot assign to 'name' because it is a read-only property.
 
제네릭에 관한 자세한 내용은 제네릭 장에서 자세히 다루도록 한다.
 

4.2.3 튜플을 사용하는 이유

튜플 타입을 사용하는 이유는 무엇일까? 튜플은 요소 각각의 타입을 고려하기 때문에 일반적인 배열 대신 튜플을 사용하면 엄격하고 명확히 데이터를 관리해야 하는 작업일 경우 이점을 가져다줄 수 있다.
아래와 같은 상황처럼, drink 배열은 음료 인덱스, 음료 이름, 음료 가격, 음료 제조사 순서의 규칙을 가지고 있고 여러 종류의 음료 정보를 담고 있다고 가정하자.
 
let drink1 = [1, 'cola', 2500, '(주)콜라공장'); let drink2 = [2, 'soda', 3000, '(주)사이다공장'); let drink3 = [3, 'lemonade', 2800, '(주)레모네이드공장');
만약, 이러한 상황에서 규칙을 모르는 누군가가 규칙을 무시하고 데이터를 만들게 된다면 해당 배열을 사용하는 곳에선 문제가 발생할 가능성이 높다. 이러한 문제를 해결하기 위해 타입스크립트에서 튜플이라는 타입이 나오게 되었다.
 
// 튜플의 타입을 별칭으로 선언 type drinkInfo = [number, string, number, string]; let drink1 : drinkInfo = [1, 'cola', 2500, '(주)콜라공장']; let drink2 : drinkInfo = ['soda', 2, '(주)사이다공장', 3000]; // error
튜플 타입을 선언할 때 데이터 타입을 하나하나 적어줘도 되지만, 매번 적어주는 것은 쉽지 않은 일이다. 그래서, 위의 예시처럼 type 키워드를 사용해 별칭을 지어줌으로써 음료 정보에 필요한 데이터 타입과 순서를 고정하였다.
여기서는 drinkInfo 가 별칭으로, [number, string, number, string] 순으로 정해주었기에 drink2에 규칙에 어긋난 데이터를 할당하려고 하면 에러를 발생시킨다.