
강타입 vs 약타입: 자바스크립트가 욕먹는 이유
사과 + 3 = ? 여기서 에러를 내면 강타입, '사과3'을 만들면 약타입입니다. [] + []가 0이 되는 마법과 TypeScript가 구원투수인 이유.

사과 + 3 = ? 여기서 에러를 내면 강타입, '사과3'을 만들면 약타입입니다. [] + []가 0이 되는 마법과 TypeScript가 구원투수인 이유.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

[] + []를 보고 멘붕한 날프로그래밍 언어를 배우면서 늘 궁금했던 게 있다. 같은 코드인데 왜 어떤 언어는 에러를 뱉고, 어떤 언어는 "알아서" 돌아가는 걸까? Python에서 "3" + 3을 하면 TypeError가 터지는데, JavaScript에서는 "33"이 된다. 처음엔 "JavaScript가 똑똑한 건가?"라고 생각했다. 하지만 [] + []가 빈 문자열 ""이 되고, [] + {}가 "[object Object]"가 되는 걸 보고 깨달았다. 이건 똑똑한 게 아니라 타입 시스템이 포기한 거구나.
Static/Dynamic Typing과 자주 헷갈리는 개념이 바로 Strong/Weak Typing이다. Static/Dynamic은 언제 타입을 체크하느냐의 문제다 (컴파일 vs 런타임). 반면 Strong/Weak은 얼마나 엄격하게 타입을 지키느냐의 문제다. 좀 더 정확히 말하면, 암묵적 타입 변환(Implicit Type Coercion)을 얼마나 허용하느냐다.
오늘은 내가 JavaScript의 악명 높은 타입 변환 규칙을 마주하며 겪은 좌절과, Python의 명쾌함을 통해 얻은 깨달음, 그리고 TypeScript와 린터로 이 혼돈을 제어하는 법까지 정리해본다.
프로젝트에서 간단한 계산기 기능을 만들 일이 있었다. 사용자 입력을 받아서 더하는 건데, HTML input에서 가져온 값은 당연히 문자열이다.
const userInput = document.getElementById('number').value; // "5"
const result = userInput + 3;
console.log(result); // 내 예상: 8, 실제: "53"
첫 번째 멘붕. + 연산자가 문자열 연결로 작동했다. "아 그럼 -는 안 되겠네?"라고 생각하고 테스트했다.
const userInput = "5";
console.log(userInput - 3); // 2 (???)
두 번째 멘붕. - 연산자는 숫자로 변환해서 계산한다. 같은 input인데 +와 -의 동작이 다르다. 이게 바로 Weak Typing의 본질이다. 연산자마다 타입 변환 규칙이 제멋대로다.
Gary Bernhardt의 유명한 "Wat" 발표에서 나온 예시들을 직접 돌려봤다.
// JavaScript의 암묵적 변환 쇼케이스
console.log([] + []); // "" (빈 문자열)
console.log([] + {}); // "[object Object]"
console.log({} + []); // 0 (브라우저 콘솔에서는 다를 수도)
console.log(true + true); // 2
console.log("5" + null); // "5null"
console.log("5" - null); // 5
console.log(null == 0); // false
console.log(null >= 0); // true (????)
마지막 두 줄은 정말 충격이었다. null == 0은 false인데 null >= 0은 true다. 왜냐하면 ==는 타입 변환을 하되 null과 undefined는 특별 취급하고, >=는 숫자 변환을 강제하기 때문이다. 이런 걸 누가 다 외우고 코딩하나?
같은 시기에 데이터 분석 작업으로 Python을 처음 써봤다. JavaScript에 익숙해진 상태라 당연히 이렇게 썼다.
user_input = "5"
result = user_input + 3
TypeError: can only concatenate str (not "int") to str
"에러가 났다. 그런데 기분이 좋다." 이상한 소리 같지만 진심이었다. Python은 나한테 거짓말을 하지 않는다. 문자열과 숫자를 섞으면 안 된다고 명확히 말해준다. JavaScript처럼 "알아서 해줄게~" 하면서 "53"이라는 이상한 결과를 만들지 않는다.
Python에서 타입을 섞고 싶으면 명시적으로 변환해야 한다.
user_input = "5"
result = int(user_input) + 3 # 명시적 변환
print(result) # 8
# 또는
result = user_input + str(3) # "53"
이게 Strong Typing이다. 타입이 안 맞으면 에러를 낸다. 개발자가 의도를 명확히 밝히길 요구한다. 처음엔 번거로워 보이지만, 버그를 미리 잡아준다는 점에서 훨씬 안전하다.
재밌는 건 Python이 Dynamic Typing 언어라는 점이다. 변수 선언할 때 타입을 안 적는다 (x = 5). 런타임에 타입을 체크한다. 그런데도 Strong Typing이다. 즉, 언제 체크하느냐와 얼마나 엄격하냐는 별개 개념이다.
JavaScript가 웃긴 결과를 만든다면, C는 위험한 결과를 만든다. C도 Weak Typing 언어인데, 포인터 캐스팅 때문에 타입 안전성이 완전히 무너진다.
int main() {
int num = 1025;
int *ptr = #
char *char_ptr = (char *)ptr; // 강제 캐스팅
printf("%d\n", *char_ptr); // 1 (하위 바이트만 읽음)
// 더 위험한 예시: 버퍼 오버플로우
char buffer[8];
int *evil_ptr = (int *)buffer; // char 배열을 int로 취급
evil_ptr[5] = 42; // 버퍼 범위를 벗어난 메모리 조작
}
포인터 캐스팅은 메모리를 다른 타입으로 "해석"하게 만든다. 컴파일러는 경고만 하거나 아예 허용한다. 이게 버퍼 오버플로우 같은 보안 취약점의 원인이다. 잘못된 타입으로 메모리를 읽고 쓰면서 프로그램이 크래시하거나, 더 나쁜 경우 해커가 악용할 수 있는 구멍이 생긴다.
JavaScript의 [] + []는 웃긴 결과를 만들지만, C의 포인터 캐스팅은 시스템 전체를 날릴 수 있다. Weak Typing의 위험성은 언어마다 다르지만, 공통점은 컴파일러가 타입을 제대로 안 지켜준다는 것이다.
JavaScript 입문서를 보면 "타입 변환 규칙"을 표로 정리해놓은 걸 볼 수 있다. Truthy/Falsy 값, + 연산자의 우선순위, ==와 ===의 차이 등등. 하지만 내 경험상, 이걸 외우려고 하면 안 된다. 대신 "피하는 규칙"을 세워야 한다.
===를 쓴다// 절대 쓰지 마
if (value == true) { }
// 이렇게 써
if (value === true) { }
// 또는 Boolean으로 명시적 변환
if (Boolean(value)) { }
+ 연산자는 숫자 계산에만 쓴다// 이러지 마
const result = userId + 100; // userId가 문자열이면?
// 이렇게 해
const result = Number(userId) + 100;
// 또는
const result = parseInt(userId, 10) + 100;
// 위험한 코드
if (count) { doSomething(); }
// count가 0이면? 0은 falsy라 doSomething이 안 돌아간다.
// 명시적으로 써
if (count !== 0) { doSomething(); }
if (count > 0) { doSomething(); }
no-implicit-coercion 규칙 켜기// ESLint가 금지하는 패턴들
const num = +str; // Number(str)로 써
const str = val + ""; // String(val)로 써
const bool = !!val; // Boolean(val)로 써
JavaScript의 Weak Typing 문제를 근본적으로 해결하는 방법은 정적 타입 시스템을 얹는 것이다. 그게 TypeScript다.
// TypeScript는 컴파일 시점에 잡아낸다
const userInput: string = "5";
const result = userInput + 3; // 에러: string + number
// 명시적 변환 강제
const result = Number(userInput) + 3; // OK
TypeScript의 strict 모드를 켜면 더 강력해진다. strictNullChecks나 noImplicitAny 같은 옵션들을 켜면 JavaScript의 "알아서 해줄게" 마법이 대부분 차단된다. 물론 런타임에는 JavaScript가 되지만, 개발 단계에서 이미 대부분의 버그를 잡을 수 있다.
Strong Typing과 Weak Typing의 차이는 단순히 "엄격함의 정도"가 아니다. 누가 타입 안전성에 책임을 지느냐의 문제다. Weak Typing은 컴파일러가 "알아서 해줄게"라며 책임을 회피한다. 그 결과는 [] + []가 ""가 되는 혼돈이다. Strong Typing은 개발자에게 "너가 명확히 말해"라고 요구한다. 번거로워 보이지만, 버그를 미리 막아준다.
JavaScript는 태생적으로 Weak Typing 언어지만, TypeScript와 린터로 무장하면 Strong Typing의 이점을 대부분 가져올 수 있다. 중요한 건 "언어가 알아서 해주겠지"라는 기대를 버리는 것이다. Number(), String(), === 같은 명시적 도구를 쓰면서, 코드를 읽는 사람(미래의 나 포함)에게 의도를 명확히 전달하자. 그게 타입 안전성의 시작이다.