타입과 친해지기: 문자열, 숫자, 배열, 그리고 any의 유혹
TypeScript를 처음 배우면 많은 사람이 이렇게 생각합니다.
"타입이요? 뭔가 딱딱하고 무서운데요?"
그런데 막상 알고 보면 타입은 무서운 감시자가 아니라, 코드를 쓰는 우리를 도와주는 친절한 안내판에 가깝습니다. 길을 잃지 않게 해 주고, 이상한 방향으로 가면 살짝 붙잡아 줍니다.
이번 장에서는 TypeScript의 가장 기본이 되는 타입들을 만나 봅니다. 문자열, 숫자, 불리언, 배열부터 시작해서, TypeScript가 스스로 타입을 알아내는 방식, 우리가 직접 타입을 적어 주는 이유, 그리고 초보자가 가장 쉽게 빠지는 함정인 any와 그 대안인 unknown까지 차근차근 살펴보겠습니다.
타입은 값의 설명서다
자바스크립트에서는 변수에 무엇이든 넣을 수 있습니다. 유연하다는 장점이 있지만, 그만큼 실수도 숨어들기 쉽습니다. 반면 TypeScript는 값에 "이건 문자열입니다", "이건 숫자입니다" 같은 설명서를 붙여 줍니다. 그래서 코드를 읽는 사람도 편하고, 에디터도 똑똑해지고, 실수도 빨리 잡을 수 있습니다.
TypeScript에서 가장 기본이 되는 원시 타입은 string, number, boolean입니다. 타입 이름은 String, Number, Boolean처럼 대문자로 쓰기보다 소문자 형태를 사용하는 것이 일반적입니다. 그리고 자바스크립트의 숫자는 정수와 실수를 따로 나누지 않고 모두 number로 다룹니다.1
let userName: string = "김타입";
let age: number = 28;
let isStudent: boolean = true;이 코드는 아주 단순해 보이지만, 이미 중요한 약속이 숨어 있습니다.
userName에는 문자열만, age에는 숫자만, isStudent에는 true 또는 false만 들어갈 수 있다는 약속입니다.
let age: number = 28;
age = 29; // OK
age = "스물아홉"; // 오류이런 오류는 프로그램을 실행하기 전에 미리 발견됩니다. 바로 이 점이 TypeScript의 가장 큰 매력입니다. 버그를 나중에 잡는 대신, 애초에 덜 만들게 도와줍니다.
문자열, 숫자, 불리언과 친해지기
문자열은 텍스트를 담습니다. 이름, 제목, 메시지처럼 말이죠.
let greeting: string = "안녕하세요";
console.log(greeting.toUpperCase());숫자는 계산에 사용됩니다.
let price: number = 3900;
let quantity: number = 2;
let total = price * quantity;불리언은 참과 거짓을 표현합니다. 보통 "켜짐/꺼짐", "로그인함/안 함", "완료/미완료" 같은 상태를 표현할 때 많이 씁니다.
let isLoggedIn: boolean = false;
let isDarkMode: boolean = true;이 세 가지는 정말 기본 중의 기본입니다. 나중에 복잡한 타입을 배우더라도 결국은 이 작은 조각들이 모여 큰 구조를 만듭니다. 마치 레고 블록처럼요. 대형 우주선도 결국 작은 블록에서 시작합니다.
배열은 같은 종류의 값을 담는 상자 묶음
배열은 여러 값을 순서대로 담는 자료구조입니다. TypeScript에서는 배열 안에 어떤 타입의 값이 들어가는지도 함께 관리할 수 있습니다.1
let fruits: string[] = ["사과", "바나나", "포도"];
let scores: number[] = [100, 95, 88];string[]는 "문자열들의 배열", number[]는 "숫자들의 배열"이라는 뜻입니다.
let fruits: string[] = ["사과", "바나나"];
fruits.push("딸기"); // OK
fruits.push(123); // 오류이렇게 하면 배열에 엉뚱한 값이 들어가는 실수를 막아 줍니다. 장바구니에 과일만 담기로 했는데 갑자기 계산기가 들어가는 일은 없어지는 셈입니다.
배열 타입은 Array<string>처럼 쓸 수도 있습니다. 하지만 초보자에게는 string[] 문법이 더 눈에 잘 들어와서 많이 사용됩니다.1
배열 안에 여러 타입이 섞이면 TypeScript는 유니언 타입 형태로 추론할 수 있습니다.
let mixed = [1, "둘", true];
// (number | string | boolean)[] 로 추론또한 배열 요소들 사이에서 하나의 대표 타입을 고르기 어려우면, TypeScript는 가능한 후보를 묶어서 추론합니다. 예를 들어 [1, 2, null]은 (number | null)[]처럼 이해됩니다.2
let values = [1, 2, null];
// (number | null)[]처음엔 조금 낯설 수 있지만, 뜻은 단순합니다. "이 배열에는 숫자가 들어오기도 하고, 비어 있음을 뜻하는 null도 들어올 수 있다"는 말입니다.
타입 추론: TypeScript의 눈치 보기 능력
TypeScript는 매번 우리가 타입을 다 적지 않아도 꽤 많은 것을 스스로 알아냅니다. 이것을 타입 추론이라고 합니다. 변수 초기화, 기본 매개변수 값, 함수 반환값 같은 자리에서 추론이 자주 일어납니다.2
let title = "타입스크립트 입문";
// string 으로 추론
let count = 10;
// number 로 추론
let done = false;
// boolean 으로 추론이 경우 굳이 아래처럼 길게 쓸 필요는 없습니다.
let title: string = "타입스크립트 입문";물론 이렇게 써도 틀린 것은 아니지만, 이미 너무 분명한 정보라면 반복해서 적지 않아도 됩니다. TypeScript는 생각보다 눈치가 좋습니다.
함수의 반환값도 추론할 수 있습니다.
function add(a: number, b: number) {
return a + b;
}
// 반환 타입을 number 로 추론이런 추론 덕분에 코드는 덜 장황해지고, 읽기 쉬워집니다.
문맥으로 타입을 알아내는 경우
TypeScript는 값 자체만 보고 추론하는 것이 아니라, 그 값이 놓인 자리도 보고 판단합니다. 이를 문맥적 타이핑이라고 합니다.2
예를 들어 문자열 배열에 forEach를 쓰면, 콜백 함수의 매개변수도 문자열이라고 알아냅니다.1
const names = ["민수", "지수", "하나"];
names.forEach((name) => {
console.log(name.toUpperCase());
});여기서 name에 타입을 직접 적지 않았지만, TypeScript는 names가 문자열 배열이라는 사실을 보고 name도 string이라고 이해합니다.
이 경험을 몇 번 해 보면 "아, TypeScript는 내가 쓴 코드를 꽤 열심히 읽고 있구나"라는 느낌이 듭니다.
명시적 타입 표기: 직접 말해 주는 타이밍
그렇다고 언제나 추론에만 맡기면 되는 것은 아닙니다. 어떤 곳에서는 타입을 직접 적어 주는 편이 더 좋습니다.
실무에서는 값이 분명한 간단한 변수는 추론에 맡기고, 함수의 매개변수와 반환값처럼 코드의 경계가 되는 부분은 명시적으로 적어 주는 편이 읽기 쉽고 유지보수에도 도움이 됩니다.3
예를 들어 함수의 매개변수는 직접 적어 주는 것이 좋습니다.
function greet(name: string): string {
return `안녕하세요, ${name}님!`;
}이 코드를 처음 보는 사람도 바로 이해할 수 있습니다.
"아, 이 함수는 문자열을 받아서 문자열을 돌려주는구나."
또, 변수를 선언할 때는 아직 값이 없지만 나중에 어떤 타입이 들어올지 정해져 있는 경우가 있습니다.
let totalPrice: number;
totalPrice = 0;
totalPrice = 15000;이런 경우는 추론할 초기값이 없으니 타입을 직접 알려 주는 편이 자연스럽습니다.
즉, 추론과 명시는 싸우는 관계가 아닙니다. 서로 역할이 다릅니다. 추론은 편하게 해 주고, 명시는 분명하게 해 줍니다.
any: 너무 자유로워서 위험한 타입
이제 초보자가 한 번쯤은 반드시 만나게 되는 타입, any를 볼 차례입니다.
any는 말 그대로 "아무 타입이나 괜찮다"는 뜻입니다. 문제는 이 순간부터 TypeScript가 그 값에 대해 거의 검사를 하지 않게 된다는 점입니다.1 그래서 any는 편하지만 위험한 탈출구, 즉 타입 시스템의 비상구 같은 존재로 여겨집니다.5
let mystery: any = "hello";
mystery.toUpperCase(); // OK
mystery.toFixed(2); // 컴파일도 통과할 수 있음겉으로는 멀쩡해 보이지만, 실제 실행 시점에는 문제가 생길 수 있습니다. 문자열에는 toFixed()가 없기 때문입니다. 즉, TypeScript가 원래 잡아 줬어야 할 실수를 그냥 통과시켜 버리는 셈입니다.
또한 타입을 적지 않았고, TypeScript도 타입을 추론할 수 없는 경우에는 암묵적으로 any가 생길 수 있습니다. 이를 막기 위해 보통 noImplicitAny 옵션을 사용합니다.14
let value;
value = "문자열";
value = 123;이런 코드는 처음엔 편해 보여도, 나중에는 "지금 이 값이 대체 뭐였지?"라는 대혼란을 부릅니다.
any가 완전히 금지된 것은 아닙니다. 아주 복잡한 레거시 코드, 타입 정보가 없는 외부 라이브러리, 잠깐 빠르게 옮겨 적는 작업에서는 필요할 수 있습니다. 하지만 기본 자세는 이렇습니다.
"정말 어쩔 수 없을 때만 쓰자."
unknown: 조심성이 있는 만능 상자
unknown도 모든 값을 담을 수 있다는 점에서는 any와 비슷합니다. 하지만 결정적인 차이가 있습니다. unknown은 사용하기 전에 먼저 정체를 확인하라고 요구합니다. 그래서 훨씬 안전합니다.4
let input: unknown = "typescript";
// input.toUpperCase(); // 오류
if (typeof input === "string") {
console.log(input.toUpperCase()); // OK
}TypeScript는 이렇게 말하는 셈입니다.
"좋아, 네가 이 값의 타입을 아직 모른다는 건 이해했어. 그런데 쓰기 전에 확인은 하고 써."
이 태도는 특히 외부에서 들어오는 데이터를 다룰 때 중요합니다. 예를 들어 API 응답, 사용자 입력, JSON.parse() 결과 같은 값은 내가 100% 통제할 수 없습니다. 실제로 JSON.parse()처럼 동적인 입력을 다루는 값은 타입이 불분명해지기 쉬워서 주의가 필요합니다.4
function printLength(data: unknown) {
if (typeof data === "string") {
console.log(data.length);
} else if (Array.isArray(data)) {
console.log(data.length);
} else {
console.log("길이를 알 수 없어요.");
}
}이 코드는 조금 더 번거롭지만, 그만큼 덜 위험합니다. unknown은 우리를 귀찮게 하려는 타입이 아니라, 사고를 줄이기 위해 한 번 더 생각하게 만드는 타입입니다.
any와 unknown의 차이, 한 번에 정리하기
둘 다 "지금 정확한 타입을 모른다"는 느낌을 주지만, 성격은 전혀 다릅니다.
any는 타입 검사를 거의 끕니다.unknown은 타입 검사를 유지합니다.any는 일단 편하지만 실수를 숨기기 쉽습니다.unknown은 바로 쓰지 못하지만 안전하게 다루게 만듭니다.
짧게 비유하면 이렇습니다.
any는 "아무나 들어와도 검사 없이 통과"입니다.
unknown은 "신분증 보여 주세요"입니다.
초보자일수록 이 차이를 빨리 몸에 익히는 게 좋습니다. 나중에 유니언 타입, 타입 가드, 제네릭 같은 고급 주제를 배울 때도 중심이 흔들리지 않기 때문입니다.
타입 사고를 시작하는 작은 습관
이 장의 핵심은 문법을 많이 외우는 것이 아닙니다. 타입을 보는 눈을 만드는 것입니다. 코드를 볼 때마다 이런 질문을 던져 보세요.
이 값은 문자열인가, 숫자인가, 참/거짓인가?
이 변수는 하나의 값인가, 여러 값의 묶음인가?
TypeScript가 스스로 알 수 있는가, 아니면 내가 직접 알려 줘야 하는가?
타입이 불분명하다면
any로 덮을 것인가,unknown으로 안전하게 다룰 것인가?
이 질문에 익숙해지면 타입은 외워야 할 규칙이 아니라, 코드를 읽는 방식이 됩니다.
작은 연습: 눈으로 타입 읽기
아래 코드를 보고 각 변수의 타입을 한 번 예상해 보세요.
const nickname = "모험가";
const level = 7;
const inventory = ["검", "방패", "물약"];
const reward = Math.random() > 0.5 ? 100 : "보너스";답은 이렇습니다.
nickname은 string, level은 number, inventory는 string[], reward는 number | string입니다. 마지막 줄처럼 결과가 둘 중 하나일 수 있으면 TypeScript는 가능한 타입들을 함께 묶어서 이해합니다.
이 감각이 아주 중요합니다. 나중에 더 복잡한 타입을 만나도 결국 출발점은 같습니다. "이 값은 무엇이 될 수 있는가?"를 묻는 습관입니다.
마무리
이번 장에서 우리는 타입의 첫인상을 제대로 잡았습니다. 문자열, 숫자, 불리언, 배열은 TypeScript의 가장 기본 재료입니다. 그리고 타입 추론은 불필요한 장황함을 줄여 주고, 명시적 타입 표기는 코드의 경계를 선명하게 만들어 줍니다. 마지막으로 any는 달콤하지만 조심해야 하고, unknown은 조금 엄격하지만 훨씬 믿을 만하다는 것도 배웠습니다.
TypeScript는 처음엔 우리에게 질문을 많이 던지는 언어처럼 보입니다. 하지만 사실은 반대입니다. 나중에 우리가 덜 헤매도록, 지금 먼저 질문해 주는 언어입니다.
그 질문에 익숙해지면, 타입은 부담이 아니라 든든한 동료가 됩니다.
참고
1TypeScript 공식 문서 - Everyday Types
2TypeScript 공식 문서 - Type Inference
3W3Schools - TypeScript Explicit Types and Inference
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.