TypeScript "Strict Mode" 설정에 대해 알아보자.
개요
TypeScript는 코드의 안정성과 가독성을 높이기 위해 다양한 컴파일러 옵션을 제공한다. 그중에서도 strict
관련 옵션들은 잠재적인 오류를 사전에 방지하고 더욱 견고한 코드를 작성하도록 돕는 중요한 역할을 한다. tsconfig.json
파일에서 strict
옵션을 true
로 설정하면 아래에 설명할 7가지 옵션이 모두 활성화된다.
tsconfig.json
1
2
3
4
5
6
7
8
9
10
11
12
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"alwaysStrict": true,
"strictBindCallApply": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
},
}
7가지 strict
옵션
noImplicitAny
타입이 명시적으로 선언되지 않은 변수나 매개변수에 대해 TypeScript 컴파일러가 암묵적으로 any
타입을 추론하는 것을 방지한다. 모든 변수, 함수 매개변수, 반환 값 등에 명시적인 타입 선언을 강제한다. 만약 타입이 누락되면 컴파일 오류가 발생한다. any
타입의 무분별한 사용은 TypeScript의 타입 검사 기능을 무력화시켜 JavaScript와 유사한 방식으로 동작하게 만든다. noImplicitAny
는 이러한 상황을 방지하여 코드의 안정성을 높인다.
1
2
3
4
5
6
7
8
9
10
11
// noImplicitAny: false (또는 설정 안 함)
function logMessage(message) { // 'message'는 암묵적으로 any 타입
console.log(message);
}
// noImplicitAny: true
function logMessageStrict(message: string) { // 'message'에 명시적으로 string 타입 선언 필요
console.log(message);
}
// logMessageStrict(); // 오류: 1개의 인수가 필요한데 0개를 가져왔습니다.
// logMessageStrict(123); // 오류: 'number' 형식의 인수는 'string' 형식의 매개 변수에 할당될 수 없습니다.
noImplicitThis
this
키워드가 명시적인 타입 없이 사용될 때 오류를 발생시킨다. JavaScript에서 this
는 호출 컨텍스트에 따라 동적으로 바인딩되므로 예상치 못한 동작을 유발할 수 있다. 함수 내에서 this
를 사용하려면 해당 함수의 첫 번째 매개변수로 this
의 타입을 명시하거나, 화살표 함수를 사용하여 어휘적 this
바인딩을 활용해야 한다. this
의 모호한 동작으로 인한 런타임 오류를 컴파일 시점에 방지한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// noImplicitThis: false
class MyClass {
value = 10;
getValue() {
return function() {
// console.log(this.value); // 'this'는 암묵적으로 any 타입이며, 런타임에 undefined가 될 수 있음
}
}
}
// noImplicitThis: true
class MyClassStrict {
value = 10;
getValue() {
// 방법 1: this 타입 명시
return function(this: MyClassStrict) {
console.log(this.value);
}
}
getValueArrow = () => {
// 방법 2: 화살표 함수 사용 (어휘적 this)
console.log(this.value);
}
}
const instance = new MyClassStrict();
const fn = instance.getValue();
// fn(); // 오류: 'this' 컨텍스트가 'void' 형식인 메서드를 호출할 수 없습니다.
// fn.call(instance); // 명시적으로 컨텍스트 바인딩 필요
const arrowFn = instance.getValueArrow;
arrowFn(); // 정상 작동
alwaysStrict
모든 소스 파일을 JavaScript의 ‘strict mode’로 파싱하고, 각 생성된 JavaScript 파일 상단에 "use strict";
지시문을 자동으로 추가한다. JavaScript의 ‘엄격 모드’에서 금지하는 특정 동작들(예: 선언되지 않은 변수에 값 할당, arguments.callee
사용 등)을 컴파일 시점에 오류로 처리한다. 더 안전하고 최적화된 JavaScript 코드를 생성하도록 유도하며, 잠재적인 오류를 줄여준다.
1
2
3
4
5
6
7
8
9
10
11
// alwaysStrict: false
// 생성된 JavaScript 파일에 "use strict"; 없음
function sloppyMode() {
// undeclaredVar = 10; // 런타임 오류 (또는 전역 변수 생성)
}
// alwaysStrict: true
// 생성된 JavaScript 파일 상단에 "use strict"; 추가됨
function strictEnabledMode() {
// undeclaredVar = 10; // 컴파일 오류: 'undeclaredVar' 이름을 찾을 수 없습니다. (strict 모드에서는 선언되지 않은 변수 할당 시 오류)
}
strictBindCallApply
함수(Function)의 내장 메서드인 bind
, call
, apply
를 사용할 때 더 엄격한 타입 검사를 수행한다. bind
, call
, apply
메서드에 전달되는 인수의 타입과 개수가 원본 함수의 시그니처와 일치하는지 확인한다. 이들 메서드를 잘못 사용했을 때 발생할 수 있는 런타임 오류를 컴파일 시점에 잡아낼 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function greet(name: string, age: number) {
console.log(`Hello, ${name}. You are ${age} years old.`);
}
// strictBindCallApply: false
// greet.call(undefined, 123, "wrong type"); // 런타임에 문제가 발생할 수 있지만, 컴파일 시에는 오류 없음
// strictBindCallApply: true
// greet.call(undefined, 123, "wrong type");
// 오류: 'number' 형식의 인수는 'string' 형식의 매개 변수에 할당될 수 없습니다.
// 오류: 'string' 형식의 인수는 'number' 형식의 매개 변수에 할당될 수 없습니다.
greet.call(undefined, "Alice", 30); // 정상
greet.apply(undefined, ["Bob", 25]); // 정상
const boundGreet = greet.bind(undefined, "Charlie");
boundGreet(40); // 정상
// boundGreet("wrong type"); // 오류: 'string' 형식의 인수는 'number' 형식의 매개 변수에 할당될 수 없습니다.
strictNullChecks
null
과 undefined
값을 모든 타입에서 기본적으로 허용하지 않고, 명시적으로 해당 타입을 선언해야만 사용할 수 있도록 한다. 변수나 매개변수가 null
또는 undefined
가 될 수 있다면, 유니언 타입(예: string | null
)을 사용하거나 옵셔널 체이닝(?.
), null 병합 연산자(??
) 등을 활용하여 null
및 undefined
값을 안전하게 처리해야 한다. JavaScript에서 가장 흔한 오류 중 하나인 “Cannot read property ‘…’ of undefined/null”과 같은 런타임 오류를 컴파일 시점에 효과적으로 방지한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// strictNullChecks: false
let name: string = null; // 오류 없음
function printLength(text: string) {
console.log(text.length); // text가 null이면 런타임 오류 발생 가능
}
// printLength(null); // 런타임 오류!
// strictNullChecks: true
// let nameStrict: string = null; // 오류: 'string' 형식에 'null'을 할당할 수 없습니다.
let nameNullable: string | null = null; // 명시적으로 null 허용
function printLengthStrict(text: string | null) {
// 방법 1: 타입 가드
if (text !== null) {
console.log(text.length);
} else {
console.log("Text is null");
}
// 방법 2: 옵셔널 체이닝
console.log(text?.length);
}
printLengthStrict(null); // "Text is null" 또는 undefined 출력 (런타임 오류 없음)
printLengthStrict("hello"); // 5
strictFunctionTypes
함수 타입의 매개변수에 대해 반공변적(contravariant)으로 타입을 검사한다. 이는 함수를 다른 함수에 할당할 때 매개변수 타입이 더 일반적인(덜 특수한) 타입이어야 함을 의미한다. 더 정확한 함수 타입 호환성 검사를 제공하여, 함수를 콜백으로 전달하거나 고차 함수를 사용할 때 발생할 수 있는 미묘한 타입 오류를 방지한다. 특히 복잡한 함수 시그니처나 고차 함수를 다룰 때 타입 안전성을 높여준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Animal은 Dog의 상위 타입
class Animal {}
class Dog extends Animal { bark() {} }
class Cat extends Animal { meow() {} }
// strictFunctionTypes: false
// 매개변수가 더 특수한 타입(Dog)이어도 할당 가능 (공변적 허용)
let processAnimalLax: (animal: Animal) => void = (dog: Dog) => dog.bark();
// processAnimalLax(new Cat()); // 런타임 오류! Cat에는 bark 메서드가 없음
// strictFunctionTypes: true
// 매개변수는 반공변적으로 검사됨. 즉, 할당하려는 함수의 매개변수가 원본 함수 매개변수의 상위 타입이어야 함.
// 아래는 오류: 'Dog' 타입의 매개변수를 'Animal' 타입의 매개변수가 필요한 곳에 할당할 수 없음
// let processAnimalStrict: (animal: Animal) => void = (dog: Dog) => dog.bark();
// 올바른 예시 (매개변수 타입이 같거나 더 일반적이어야 함)
let processDog: (dog: Dog) => void = (animal: Animal) => { /* animal로 할 수 있는 일반적인 작업 */ }; // 이것도 사실상 안전하지 않음
let processAnimalStrictCorrect: (animal: Animal) => void = (animal: Animal) => console.log("Processing animal");
let feedDog: (dog: Dog) => void = processAnimalStrictCorrect; // Animal은 Dog의 상위타입이므로 안전
// feedDog(new Dog()); // 정상
strictFunctionTypes
가 true
일 때, (dog: Dog) => void
타입의 함수를 (animal: Animal) => void
타입의 변수에 할당하려고 하면 오류가 발생한다. 왜냐하면 processAnimalStrict
는 Animal
타입의 어떤 객체든 받을 수 있어야 하는데, 실제 할당된 함수는 Dog
타입만 처리할 수 있기 때문이다. 반대로, (animal: Animal) => void
타입의 함수를 (dog: Dog) => void
타입의 변수에 할당하는 것은 안전하다. 왜냐하면 Dog
는 Animal
의 하위 타입이므로 Animal
을 처리할 수 있는 함수는 당연히 Dog
도 처리할 수 있기 때문이다.
strictPropertyInitialization
클래스의 인스턴스 속성이 생성자(constructor)에서 초기화되거나, 선언 시점에 명시적으로 초기화되는지 확인한다. undefined
타입이 명시적으로 유니언 타입에 포함되지 않은 경우, 초기화되지 않은 속성은 오류를 발생시킨다. 클래스 멤버 변수가 선언되었으나 생성자에서 값을 할당받지 못하면 컴파일 오류가 발생한다. 단, undefined
가 될 수 있음을 명시하거나(propertyName?: type
또는 propertyName: type | undefined
), definite assignment assertion
(propertyName!: type
)을 사용하면 이 검사를 우회할 수 있다. 클래스 인스턴스가 생성될 때 모든 속성이 예측 가능한 상태로 초기화되도록 보장하여, 런타임에 undefined
속성에 접근하려는 오류를 줄인다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// strictPropertyInitialization: false
class User {
username: string; // 초기화되지 않았지만 오류 없음
constructor(name: string) {
// 만약 여기서 this.username을 초기화하지 않으면 undefined 상태가 됨
}
}
// const user = new User("test");
// console.log(user.username.toUpperCase()); // 런타임 오류 가능성
// strictPropertyInitialization: true
class UserStrict {
// 방법 1: 선언 시 초기화
id: number = Math.random();
// 방법 2: 생성자에서 초기화
username: string;
// 방법 3: undefined 허용 (옵셔널 프로퍼티)
email?: string;
// 방법 4: undefined 허용 (유니언 타입)
address: string | undefined;
// 방법 5: Definite Assignment Assertion (확실히 할당됨을 명시 - 주의해서 사용)
// 외부 라이브러리나 다른 메서드에 의해 초기화가 보장될 때 사용
profileUrl!: string;
constructor(name: string) {
this.username = name; // 여기서 초기화하지 않으면 'username' 속성에 대한 오류 발생
this.initializeProfile(); // profileUrl은 여기서 초기화됨을 가정
}
initializeProfile() {
this.profileUrl = "http://example.com/profile";
}
}
// const userStrictError = new UserStrict(); // 오류: 생성자에 'name' 인수가 필요합니다.
const userStrict = new UserStrict("Alice");
console.log(userStrict.username.toUpperCase()); // 정상
console.log(userStrict.profileUrl); // 정상 (initializeProfile에서 할당)
if (userStrict.email) {
console.log(userStrict.email);
}
정리
TypeScript의 strict
관련 옵션들은 코드의 품질을 크게 향상시키는 데 기여한다. 처음에는 다소 번거롭게 느껴질 수 있지만, 장기적으로는 버그를 줄이고 유지보수성을 높이며, 협업 시 코드 이해도를 높이는 데 매우 효과적이다. 새로운 프로젝트를 시작한다면 strict: true
로 설정하고 시작하는 것을 강력히 권장한다. 기존 프로젝트에 적용할 경우에는 각 옵션을 하나씩 활성화하면서 점진적으로 코드베이스를 개선해나가는 방식을 고려할 수 있다.