JavaScript 변수와 데이터 타입 | let, const, var 완벽 정리

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 비교

특징letconstvar
재할당
재선언
스코프블록블록함수
호이스팅TDZTDZundefined
권장

TDZ (Temporal Dead Zone): 선언 전에 접근하면 에러

// let/const: TDZ
// console.log(x);  // ReferenceError
let x = 10;

// var: 호이스팅
console.log(y);  // undefined (에러 아님)
var y = 20;

2. 스코프 (Scope)

블록 스코프

letconst는 블록 스코프를 가집니다:

// 블록 스코프: 중괄호 {} 내부에서만 유효
{
    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);
}

블록 스코프의 장점:

  1. 변수 충돌 방지: 블록마다 독립적인 변수
  2. 메모리 효율: 블록이 끝나면 자동 해제
  3. 예측 가능: 변수의 생명주기가 명확
  4. 클로저 안전: 각 반복마다 새로운 변수

함수 스코프

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]

정리

핵심 요약

  1. 변수 선언:

    • const: 재할당 불가 (기본 선택)
    • let: 재할당 가능
    • var: 사용 금지 (구식)
  2. 스코프:

    • 블록 스코프: let, const
    • 함수 스코프: var
  3. 데이터 타입:

    • 원시 타입: Number, String, Boolean, undefined, null, Symbol, BigInt
    • 참조 타입: Object, Array, Function
  4. 타입 변환:

    • Number(), String(), Boolean()
    • parseInt(), parseFloat()
    • Truthy/Falsy 값
  5. 호이스팅:

    • var: undefined로 초기화
    • let/const: TDZ (접근 불가)

베스트 프랙티스

  1. const 우선, 재할당 필요시 let
  2. === 사용 (== 금지)
  3. "use strict" 사용
  4. ✅ 의미 있는 변수명
  5. ✅ 전역 변수 최소화

다음 단계

  • JavaScript 함수
  • JavaScript 배열과 객체
  • JavaScript 조건문과 반복문

관련 글

  • JavaScript 시작하기 | 웹 개발의 필수 언어 완벽 입문