JavaScript 변수와 데이터 타입 | let, const, var 완벽 정리
이 글의 핵심
JavaScript 변수와 데이터 타입에 대한 실전 가이드입니다. let, const, var 완벽 정리 등을 예제와 함께 상세히 설명합니다.
들어가며
변수란?
변수(Variable)는 데이터를 담는 상자와 같습니다. JavaScript는 동적 타입 언어(실행할 때 값에 따라 타입이 정해지는 방식)이므로, 값의 종류는 실행 시점에 정해집니다.
아래에서 다루는 호이스팅(선언이 마치 코드 맨 위로 끌어올려진 것처럼 처리되는 동작)은, 코드가 위에서 아래로만 읽힌다고 생각하기 쉽지만 실제로는 선언이 먼저 정리된 뒤 실행이 이어집니다. 엘리베이터가 먼저 층·호실을 인식한 뒤에 문이 열리듯, 선언이 스코프 상단으로 올라간 뒤에 할당과 실행이 이어진다고 이해하시면 됩니다.
1. 변수 선언: let, const, var
let: 재할당 가능
let age = 25;
console.log(age); // 25
age = 26; // 재할당 가능
console.log(age); // 26
// 선언만 하고 나중에 할당
let name;
console.log(name); // undefined
name = "홍길동";
console.log(name); // 홍길동
const: 재할당 불가
const는 상수를 선언하며, 재할당이 불가능합니다:
const PI = 3.14159;
console.log(PI); // 3.14159
// 재할당 시도 시 에러
// PI = 3.14; // TypeError: Assignment to constant variable
// const로 선언된 변수는 다른 값으로 재할당 불가
// 선언과 동시에 초기화 필수
// const x; // SyntaxError: Missing initializer in const declaration
// const는 선언만 하고 나중에 할당할 수 없음
// 중요: 객체/배열의 내부는 변경 가능
const person = { name: "홍길동", age: 25 };
// person 변수 자체는 재할당 불가
// 하지만 객체의 프로퍼티는 변경 가능
person.age = 26; // ✅ OK (객체 내부 변경)
console.log(person); // { name: '홍길동', age: 26 }
// person = {}; // ❌ TypeError (재할당 불가)
// person 변수가 다른 객체를 가리키도록 할 수 없음
const arr = [1, 2, 3];
// arr 변수 자체는 재할당 불가
// 하지만 배열의 요소는 변경 가능
arr.push(4); // ✅ OK (배열 내부 변경)
console.log(arr); // [1, 2, 3, 4]
arr[0] = 10; // ✅ OK (요소 수정)
console.log(arr); // [10, 2, 3, 4]
// arr = []; // ❌ TypeError (재할당 불가)
// arr 변수가 다른 배열을 가리키도록 할 수 없음
const의 의미:
// const는 "변수가 가리키는 참조"를 고정
// 참조 대상의 내용은 변경 가능
const obj = { x: 1 };
// obj는 특정 객체의 메모리 주소를 가리킴
obj.x = 2; // ✅ 객체 내용 변경 (주소는 그대로)
// obj = { x: 2 }; // ❌ 다른 객체로 교체 (주소 변경 시도)
완전한 불변 객체 만들기:
// Object.freeze(): 객체를 완전히 동결
const person = Object.freeze({ name: "홍길동", age: 25 });
// person.age = 26; // 무시됨 (strict mode에서는 에러)
console.log(person.age); // 25 (변경 안 됨)
// 중첩 객체는 별도로 freeze 필요
const user = Object.freeze({
name: "홍길동",
address: { city: "서울" } // 이 객체는 freeze 안 됨
});
user.address.city = "부산"; // ✅ 변경됨
console.log(user.address.city); // 부산
var: 구식 (사용 권장 안 함)
var x = 10;
var x = 20; // 재선언 가능 (문제!)
console.log(x); // 20
// 함수 스코프 (블록 스코프 무시)
if (true) {
var y = 30;
}
console.log(y); // 30 (블록 밖에서도 접근 가능!)
// 호이스팅 문제
console.log(z); // undefined (에러 아님!)
var z = 40;
let vs const vs var 비교
| 특징 | let | const | var |
|---|---|---|---|
| 재할당 | ✅ | ❌ | ✅ |
| 재선언 | ❌ | ❌ | ✅ |
| 스코프 | 블록 | 블록 | 함수 |
| 호이스팅 | TDZ | TDZ | undefined |
| 권장 | ✅ | ✅ | ❌ |
TDZ (Temporal Dead Zone): 선언 전에 접근하면 에러
// let/const: TDZ
// console.log(x); // ReferenceError
let x = 10;
// var: 호이스팅
console.log(y); // undefined (에러 아님)
var y = 20;
2. 스코프 (Scope)
블록 스코프
let과 const는 블록 스코프를 가집니다:
// 블록 스코프: 중괄호 {} 내부에서만 유효
{
let x = 10;
const y = 20;
console.log(x, y); // 10 20
// 이 블록 안에서만 x, y 사용 가능
}
// 블록 밖에서는 접근 불가
// console.log(x); // ReferenceError: x is not defined
// console.log(y); // ReferenceError: y is not defined
// x, y는 블록이 끝나면 소멸
// if 블록 스코프
if (true) {
let z = 30;
console.log(z); // 30
// z는 이 if 블록 안에서만 유효
}
// console.log(z); // ReferenceError
// if 블록이 끝나면 z는 접근 불가
// for 블록 스코프
for (let i = 0; i < 3; i++) {
// i는 이 for 루프 안에서만 유효
console.log(i); // 0 1 2
}
// console.log(i); // ReferenceError
// 루프가 끝나면 i는 소멸
// 중첩 블록 스코프
{
let x = 10;
{
let x = 20; // 새로운 x (외부 x와 다름)
console.log(x); // 20 (내부 x)
}
console.log(x); // 10 (외부 x)
}
// 실전 예시: 루프에서 클로저 사용
for (let i = 0; i < 3; i++) {
// let은 각 반복마다 새로운 i 생성
setTimeout(() => {
console.log(i); // 0 1 2 (각각 다른 i)
}, 100);
}
// var를 사용하면?
for (var j = 0; j < 3; j++) {
// var는 함수 스코프 (같은 j 공유)
setTimeout(() => {
console.log(j); // 3 3 3 (모두 같은 j)
}, 100);
}
블록 스코프의 장점:
- 변수 충돌 방지: 블록마다 독립적인 변수
- 메모리 효율: 블록이 끝나면 자동 해제
- 예측 가능: 변수의 생명주기가 명확
- 클로저 안전: 각 반복마다 새로운 변수
함수 스코프
function test() {
let x = 10; // 함수 스코프
var y = 20; // 함수 스코프
if (true) {
let x = 30; // 블록 스코프 (새 변수)
var y = 40; // 함수 스코프 (같은 변수)
console.log(x, y); // 30 40
}
console.log(x, y); // 10 40
}
test();
// console.log(x); // ReferenceError
전역 스코프
// 전역 변수
let globalVar = "전역";
function test() {
console.log(globalVar); // 전역 접근 가능
}
test(); // 전역
// 브라우저: window 객체
// var global = "test";
// console.log(window.global); // test
// Node.js: global 객체
// global.myVar = "test";
3. 데이터 타입
원시 타입 (Primitive Types)
JavaScript에는 7가지 원시 타입이 있습니다.
1. Number (숫자)
let integer = 42;
let float = 3.14;
let negative = -10;
// 특수 값
let inf = Infinity;
let negInf = -Infinity;
let notANumber = NaN;
console.log(10 / 0); // Infinity
console.log("abc" / 2); // NaN
// NaN 체크
console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false
// 안전한 정수 범위
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991
2. String (문자열)
let str1 = "작은따옴표";
let str2 = '큰따옴표';
let str3 = `백틱 (템플릿 리터럴)`;
// 템플릿 리터럴
let name = "홍길동";
let age = 25;
let message = `안녕하세요, ${name}님! ${age}살이시군요.`;
console.log(message); // 안녕하세요, 홍길동님! 25살이시군요.
// 여러 줄 문자열
let multiline = `첫 번째 줄
두 번째 줄
세 번째 줄`;
// 이스케이프 문자
let escaped = "He said \"Hello\"";
console.log(escaped); // He said "Hello"
3. Boolean (불리언)
let isTrue = true;
let isFalse = false;
// 비교 연산
console.log(10 > 5); // true
console.log(10 === 5); // false
4. undefined
let x;
console.log(x); // undefined
console.log(typeof x); // undefined
// 함수 반환값 없을 때
function test() {
// return 없음
}
console.log(test()); // undefined
5. null
let empty = null;
console.log(empty); // null
console.log(typeof empty); // object (JavaScript 버그!)
// undefined vs null
let x; // undefined (선언만)
let y = null; // null (명시적 빈 값)
6. Symbol (ES6+)
let sym1 = Symbol("description");
let sym2 = Symbol("description");
console.log(sym1 === sym2); // false (항상 고유)
// 객체 프로퍼티 키로 사용
let obj = {
[sym1]: "value1"
};
console.log(obj[sym1]); // value1
7. BigInt (ES2020+)
// Number의 안전 범위를 넘는 큰 정수
let bigNum = 9007199254740991n; // n 접미사
let bigNum2 = BigInt("9007199254740991");
console.log(bigNum + 1n); // 9007199254740992n
// Number와 혼용 불가
// console.log(bigNum + 1); // TypeError
console.log(bigNum + BigInt(1)); // OK
참조 타입 (Reference Types)
Object (객체)
let person = {
name: "홍길동",
age: 25,
greet: function() {
console.log(`안녕하세요, ${this.name}입니다.`);
}
};
console.log(person.name); // 홍길동
person.greet(); // 안녕하세요, 홍길동입니다.
Array (배열)
let numbers = [1, 2, 3, 4, 5];
let mixed = [1, "hello", true, null, { name: "홍길동" }];
console.log(numbers[0]); // 1
console.log(numbers.length); // 5
Function (함수)
function add(a, b) {
return a + b;
}
// 함수도 객체
console.log(typeof add); // function
4. 타입 확인과 변환
typeof 연산자
console.log(typeof 42); // number
console.log(typeof "hello"); // string
console.log(typeof true); // boolean
console.log(typeof undefined); // undefined
console.log(typeof null); // object (버그!)
console.log(typeof {}); // object
console.log(typeof []); // object (배열도 객체)
console.log(typeof function(){}); // function
// 배열 체크
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
타입 변환
문자열 변환
// 명시적 변환
let num = 42;
let str = String(num);
console.log(str, typeof str); // 42 string
// toString()
let str2 = num.toString();
console.log(str2); // 42
// 암묵적 변환
let result = 42 + " apples";
console.log(result); // 42 apples (문자열)
숫자 변환
// 명시적 변환
let str = "42";
let num = Number(str);
console.log(num, typeof num); // 42 number
// parseInt, parseFloat
console.log(parseInt("42px")); // 42
console.log(parseFloat("3.14abc")); // 3.14
console.log(parseInt("abc")); // NaN
// 암묵적 변환
console.log("42" - 10); // 32 (숫자)
console.log("42" * 2); // 84 (숫자)
console.log(+"42"); // 42 (단항 + 연산자)
불리언 변환
// 명시적 변환
console.log(Boolean(1)); // true
console.log(Boolean(0)); // false
console.log(Boolean("hello")); // true
console.log(Boolean("")); // false
// Falsy 값 (false로 변환)
console.log(Boolean(false)); // false
console.log(Boolean(0)); // false
console.log(Boolean("")); // false
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
console.log(Boolean(NaN)); // false
// Truthy 값 (true로 변환)
console.log(Boolean(1)); // true
console.log(Boolean("hello")); // true
console.log(Boolean([])); // true
console.log(Boolean({})); // true
// 암묵적 변환
if ("hello") {
console.log("Truthy!"); // 실행됨
}
5. 호이스팅 (Hoisting)
호이스팅이란?
호이스팅(Hoisting)은 변수와 함수 선언이 스코프 안에서 맨 위로 끌어올려지는 것처럼 동작하는 현상입니다. 엔진이 실행 전에 이름표를 붙이는 단계를 거친다고 보시면 됩니다.
비유(엘리베이터): 층마다 호실이 정해지기 전에 엘리베이터가 먼저 어느 층에 어떤 호실이 있는지 인식하는 것과 비슷합니다. var는 문이 열리기 전에도 호실 번호는 잡혀 있지만 안은 비어(undefined) 있고, let/const는 문이 열리기 전에는 임시 사각지대(TDZ) 때문에 들어갈 수 없습니다.
var 호이스팅
console.log(x); // undefined (에러 아님!)
var x = 10;
console.log(x); // 10
// 실제 동작 (내부적으로)
var x; // 선언이 최상단으로
console.log(x); // undefined
x = 10; // 할당은 원래 위치
console.log(x); // 10
let/const 호이스팅 (TDZ)
// TDZ (Temporal Dead Zone): 선언 전 접근 불가
// console.log(x); // ReferenceError
let x = 10;
console.log(x); // 10
// const도 동일
// console.log(y); // ReferenceError
const y = 20;
함수 호이스팅
// 함수 선언문: 호이스팅됨
greet(); // 안녕하세요! (선언 전 호출 가능)
function greet() {
console.log("안녕하세요!");
}
// 함수 표현식: 호이스팅 안 됨
// sayHello(); // ReferenceError
const sayHello = function() {
console.log("Hello!");
};
sayHello(); // Hello!
6. 실전 예제
예제 1: 변수 스왑
// 방법 1: 임시 변수
let a = 10;
let b = 20;
let temp = a;
a = b;
b = temp;
console.log(a, b); // 20 10
// 방법 2: 구조 분해 (ES6+)
let x = 10;
let y = 20;
[x, y] = [y, x];
console.log(x, y); // 20 10
예제 2: 타입 체크 함수
function getType(value) {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
return typeof value;
}
console.log(getType(42)); // number
console.log(getType("hello")); // string
console.log(getType(true)); // boolean
console.log(getType(null)); // null
console.log(getType(undefined)); // undefined
console.log(getType([])); // array
console.log(getType({})); // object
console.log(getType(() => {})); // function
예제 3: 안전한 숫자 변환
function safeParseInt(value, defaultValue = 0) {
const parsed = parseInt(value);
return isNaN(parsed) ? defaultValue : parsed;
}
console.log(safeParseInt("42")); // 42
console.log(safeParseInt("abc")); // 0
console.log(safeParseInt("abc", -1)); // -1
console.log(safeParseInt("3.14")); // 3
예제 4: 객체 불변성 유지
// const는 재할당만 막음
const config = {
apiUrl: "https://api.example.com",
timeout: 5000
};
config.timeout = 10000; // OK (내부 변경)
console.log(config); // { apiUrl: '...', timeout: 10000 }
// 완전한 불변 객체: Object.freeze()
const frozenConfig = Object.freeze({
apiUrl: "https://api.example.com",
timeout: 5000
});
frozenConfig.timeout = 10000; // 무시됨 (strict mode에서는 에러)
console.log(frozenConfig.timeout); // 5000 (변경 안 됨)
// 중첩 객체는 freeze 안 됨
const user = Object.freeze({
name: "홍길동",
address: { city: "서울" }
});
// user.name = "김철수"; // 무시됨
user.address.city = "부산"; // OK (중첩 객체는 변경 가능)
console.log(user.address.city); // 부산
// 깊은 freeze (재귀)
function deepFreeze(obj) {
Object.freeze(obj);
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object' && obj[key] !== null) {
deepFreeze(obj[key]);
}
});
return obj;
}
7. 자주 하는 실수와 해결법
실수 1: var 사용
// ❌ 잘못된 방법
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 3 3 3 (의도: 0 1 2)
// ✅ 올바른 방법
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 0 1 2
실수 2: const 오해
// ❌ 잘못된 이해
const arr = [1, 2, 3];
// arr = [4, 5, 6]; // TypeError (재할당 불가)
// ✅ 올바른 이해
const arr = [1, 2, 3];
arr.push(4); // OK (내부 변경)
arr[0] = 100; // OK
console.log(arr); // [100, 2, 3, 4]
실수 3: == vs ===
// ❌ == (타입 변환 후 비교)
console.log(5 == "5"); // true (문제!)
console.log(0 == false); // true
console.log("" == false); // true
// ✅ === (타입까지 비교)
console.log(5 === "5"); // false
console.log(0 === false); // false
console.log("" === false); // false
실수 4: 타입 변환 실수
// ❌ 의도하지 않은 문자열 연결
console.log("10" + 20); // 1020 (문자열)
// ✅ 명시적 변환
console.log(Number("10") + 20); // 30
console.log(parseInt("10") + 20); // 30
실수 5: 전역 변수 오염
// ❌ 전역 변수
count = 0; // var/let/const 없이 선언 (전역 변수!)
function increment() {
count++;
}
// ✅ 명시적 선언
let count = 0;
function increment() {
count++;
}
// 또는 strict mode 사용
"use strict";
// count = 0; // ReferenceError
8. 실전 팁
팁 1: const 우선, let 필요시
// ✅ 기본은 const
const API_URL = "https://api.example.com";
const MAX_RETRY = 3;
// ✅ 재할당 필요시 let
let retryCount = 0;
let result = null;
팁 2: 의미 있는 변수명
// ❌ 나쁜 이름
let x = 25;
let data = [1, 2, 3];
let temp = "홍길동";
// ✅ 좋은 이름
let userAge = 25;
let scores = [1, 2, 3];
let userName = "홍길동";
// 상수는 대문자 + 언더스코어
const MAX_USERS = 100;
const API_BASE_URL = "https://api.example.com";
팁 3: strict mode 사용
"use strict";
// 선언 없는 변수 사용 불가
// x = 10; // ReferenceError
// 읽기 전용 속성 수정 불가
// const obj = {};
// Object.defineProperty(obj, "x", { value: 42, writable: false });
// obj.x = 100; // TypeError
// 중복 매개변수 불가
// function test(a, a) {} // SyntaxError
9. 연습 문제
문제 1: 변수 스왑 (3개)
3개의 변수를 회전시키세요. (a → b, b → c, c → a)
let a = 1;
let b = 2;
let c = 3;
// 풀이
[a, b, c] = [c, a, b];
console.log(a, b, c); // 3 1 2
문제 2: 타입 변환 체크
주어진 값을 숫자로 변환하고, 실패하면 기본값을 반환하세요.
function toNumber(value, defaultValue = 0) {
const num = Number(value);
return isNaN(num) ? defaultValue : num;
}
// 테스트
console.log(toNumber("42")); // 42
console.log(toNumber("abc")); // 0
console.log(toNumber("abc", -1)); // -1
console.log(toNumber(null)); // 0
console.log(toNumber(undefined)); // 0
문제 3: Truthy/Falsy 필터
배열에서 Falsy 값을 제거하세요.
function removeFalsy(arr) {
return arr.filter(item => item); // Truthy만 통과
}
// 또는
function removeFalsy(arr) {
return arr.filter(Boolean);
}
// 테스트
let arr = [0, 1, false, 2, "", 3, null, undefined, NaN, 4];
console.log(removeFalsy(arr)); // [1, 2, 3, 4]
정리
핵심 요약
-
변수 선언:
const: 재할당 불가 (기본 선택)let: 재할당 가능var: 사용 금지 (구식)
-
스코프:
- 블록 스코프:
let,const - 함수 스코프:
var
- 블록 스코프:
-
데이터 타입:
- 원시 타입: Number, String, Boolean, undefined, null, Symbol, BigInt
- 참조 타입: Object, Array, Function
-
타입 변환:
Number(),String(),Boolean()parseInt(),parseFloat()- Truthy/Falsy 값
-
호이스팅:
var: undefined로 초기화let/const: TDZ (접근 불가)
베스트 프랙티스
- ✅
const우선, 재할당 필요시let - ✅
===사용 (==금지) - ✅
"use strict"사용 - ✅ 의미 있는 변수명
- ✅ 전역 변수 최소화
다음 단계
- JavaScript 함수
- JavaScript 배열과 객체
- JavaScript 조건문과 반복문
관련 글
- JavaScript 시작하기 | 웹 개발의 필수 언어 완벽 입문