JavaScript/기초 문법과 자료형

JavaScript 변수 선언: var, let, const 완벽 가이드

코딩하는 패션이과생 2025. 5. 16. 01:17
반응형

변수 선언이란?

변수(Variable)는 데이터를 저장하는 메모리 공간에 이름을 붙인 것입니다. JavaScript에서는 var, let, const 세 가지 키워드로 변수를 선언할 수 있습니다.

🔍 기본 문법

var variableName = value;
let variableName = value;
const variableName = value;

🕰️ 역사적 배경

  • ES5 (2009): var만 존재
  • ES6 (2015): letconst 추가
  • 현재: letconst 사용 권장

var 키워드

📖 var의 기본 특징

// var로 변수 선언
var message = "안녕하세요";
var count = 10;
var isActive = true;

console.log(message); // "안녕하세요"
console.log(count);   // 10
console.log(isActive); // true

🔄 var의 주요 특징

1. 재선언 가능

var name = "김철수";
console.log(name); // "김철수"

var name = "이영희"; // 재선언 가능
console.log(name); // "이영희"

2. 재할당 가능

var score = 80;
console.log(score); // 80

score = 95; // 재할당 가능
console.log(score); // 95

3. 함수 스코프

function example() {
    if (true) {
        var x = 1;
    }
    console.log(x); // 1 (접근 가능)
}

example();
// console.log(x); // ReferenceError: x is not defined

4. 호이스팅

console.log(hoistedVar); // undefined (에러 아님)
var hoistedVar = "값";
console.log(hoistedVar); // "값"

// 위 코드는 실제로 이렇게 동작함:
// var hoistedVar;
// console.log(hoistedVar); // undefined
// hoistedVar = "값";
// console.log(hoistedVar); // "값"

⚠️ var의 문제점

1. 예상치 못한 동작

// 클로저 문제
for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 3, 3, 3 출력
    }, 100);
}

// 예상: 0, 1, 2
// 실제: 3, 3, 3

2. 전역 객체 오염

var globalVar = "전역 변수";
console.log(window.globalVar); // "전역 변수" (브라우저에서)

let 키워드

📝 let의 기본 특징

ES6에서 도입된 letvar의 문제점을 해결합니다.

// let으로 변수 선언
let username = "사용자";
let age = 25;
let isLoggedIn = false;

console.log(username); // "사용자"
console.log(age);      // 25
console.log(isLoggedIn); // false

🔧 let의 주요 특징

1. 재선언 불가능

let city = "서울";
// let city = "부산"; // SyntaxError: Identifier 'city' has already been declared

2. 재할당 가능

let temperature = 20;
console.log(temperature); // 20

temperature = 30; // 재할당 가능
console.log(temperature); // 30

3. 블록 스코프

function example() {
    if (true) {
        let y = 1;
        console.log(y); // 1
    }
    // console.log(y); // ReferenceError: y is not defined
}

example();

4. 개선된 호이스팅

// console.log(letVar); // ReferenceError: Cannot access 'letVar' before initialization
let letVar = "값";
console.log(letVar); // "값"

✅ let의 장점

1. 클로저 문제 해결

// let을 사용한 해결
for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 0, 1, 2 출력
    }, 100);
}

2. 전역 객체 오염 방지

let globalLet = "전역 변수";
console.log(window.globalLet); // undefined (브라우저에서)

const 키워드

📌 const의 기본 특징

const는 상수(constant)를 선언할 때 사용하며, 재할당이 불가능합니다.

// const로 상수 선언
const PI = 3.14159;
const APP_NAME = "MyApp";
const MAX_USERS = 100;

console.log(PI);       // 3.14159
console.log(APP_NAME); // "MyApp"
console.log(MAX_USERS); // 100

🔒 const의 주요 특징

1. 선언 시 초기화 필수

// const name; // SyntaxError: Missing initializer in const declaration
const name = "김철수"; // 올바른 선언

2. 재선언 불가능

const brand = "Apple";
// const brand = "Samsung"; // SyntaxError: Identifier 'brand' has already been declared

3. 재할당 불가능

const version = "1.0.0";
// version = "2.0.0"; // TypeError: Assignment to constant variable.

4. 블록 스코프

if (true) {
    const secret = "비밀";
    console.log(secret); // "비밀"
}
// console.log(secret); // ReferenceError: secret is not defined

🔍 const와 객체/배열

객체의 속성은 변경 가능

const person = {
    name: "김철수",
    age: 30
};

// 객체 자체는 재할당 불가
// person = {}; // TypeError

// 하지만 속성은 변경 가능
person.name = "이영희";
person.age = 25;
console.log(person); // { name: "이영희", age: 25 }

배열의 요소는 변경 가능

const fruits = ["사과", "바나나"];

// 배열 자체는 재할당 불가
// fruits = []; // TypeError

// 하지만 요소 변경은 가능
fruits.push("오렌지");
fruits[0] = "딸기";
console.log(fruits); // ["딸기", "바나나", "오렌지"]

완전한 불변성을 위한 Object.freeze()

const config = Object.freeze({
    apiUrl: "https://api.example.com",
    timeout: 5000
});

// config.apiUrl = "다른 URL"; // 무시됨 (strict mode에서는 에러)
console.log(config.apiUrl); // "https://api.example.com"

스코프(Scope) 비교

🔍 스코프란?

스코프는 변수에 접근할 수 있는 범위를 의미합니다.

📊 스코프 비교표

특징 var let const
스코프 함수 스코프 블록 스코프 블록 스코프
호이스팅 undefined로 초기화 TDZ TDZ
재선언 ✅ 가능 ❌ 불가능 ❌ 불가능
재할당 ✅ 가능 ✅ 가능 ❌ 불가능

🔬 스코프 실험

function scopeTest() {
    // 함수 스코프
    var varVariable = "var 변수";

    if (true) {
        // 블록 스코프
        var varInBlock = "블록 안의 var";
        let letInBlock = "블록 안의 let";
        const constInBlock = "블록 안의 const";

        console.log(varInBlock);   // "블록 안의 var"
        console.log(letInBlock);   // "블록 안의 let"
        console.log(constInBlock); // "블록 안의 const"
    }

    console.log(varVariable);    // "var 변수"
    console.log(varInBlock);     // "블록 안의 var" (접근 가능)
    // console.log(letInBlock);  // ReferenceError
    // console.log(constInBlock); // ReferenceError
}

scopeTest();

🔄 반복문에서의 동작

// var 사용 시
for (var i = 0; i < 3; i++) {
    // 각 반복마다 같은 i 변수 사용
}
console.log(i); // 3 (접근 가능)

// let 사용 시
for (let j = 0; j < 3; j++) {
    // 각 반복마다 새로운 j 변수 생성
}
// console.log(j); // ReferenceError

호이스팅(Hoisting) 이해하기

🔄 호이스팅이란?

호이스팅은 변수와 함수 선언이 컴파일 타임에 스코프의 최상단으로 끌어올려지는 JavaScript의 특성입니다.

📋 var 호이스팅

// 실제 코드
console.log(hoistedVar); // undefined
var hoistedVar = "값";

// 실제 동작 방식
var hoistedVar; // undefined로 초기화
console.log(hoistedVar); // undefined
hoistedVar = "값";

🚨 let/const 호이스팅

// TDZ (Temporal Dead Zone)
console.log(hoistedLet); // ReferenceError
let hoistedLet = "값";

console.log(hoistedConst); // ReferenceError
const hoistedConst = "값";

🕰️ TDZ(Temporal Dead Zone) 설명

// TDZ 시작
// console.log(temp); // ReferenceError

let temp = "값"; // TDZ 끝
console.log(temp); // "값"

재선언과 재할당

📊 재선언/재할당 비교표

// 재선언 테스트
var name1 = "김철수";
var name1 = "이영희"; // ✅ 가능

let name2 = "김철수";
// let name2 = "이영희"; // ❌ SyntaxError

const name3 = "김철수";
// const name3 = "이영희"; // ❌ SyntaxError

// 재할당 테스트
var age1 = 20;
age1 = 30; // ✅ 가능

let age2 = 20;
age2 = 30; // ✅ 가능

const age3 = 20;
// age3 = 30; // ❌ TypeError

🔍 같은 이름 변수 선언

// 다른 스코프에서는 가능
function example() {
    let x = 1;

    if (true) {
        let x = 2; // 다른 스코프이므로 가능
        console.log(x); // 2
    }

    console.log(x); // 1
}

실무 사용 가이드라인

🎯 언제 어떤 키워드를 사용할까?

1. const 우선 사용

// 변경되지 않는 값들
const API_URL = "https://api.example.com";
const MAX_RETRY_COUNT = 3;
const DEFAULT_SETTINGS = {
    theme: "dark",
    language: "ko"
};

2. 재할당이 필요하면 let

// 반복문의 카운터
for (let i = 0; i < items.length; i++) {
    // ...
}

// 조건에 따라 변경되는 값
let message = "";
if (isSuccess) {
    message = "성공!";
} else {
    message = "실패!";
}

3. var는 피하기

// ❌ var 사용 지양
var legacyVariable = "구식 방법";

// ✅ let 또는 const 사용
const modernConstant = "현대적 방법";
let modernVariable = "현대적 방법";

📋 명명 규칙

// 상수는 대문자와 언더스코어
const MAX_FILE_SIZE = 1024 * 1024;
const API_BASE_URL = "https://api.example.com";

// 일반 변수는 camelCase
let userName = "김철수";
let isLoggedIn = false;
const userProfile = {
    name: "김철수",
    email: "user@example.com"
};

// 클래스는 PascalCase
const UserManager = class {
    constructor(name) {
        this.name = name;
    }
};

🔒 불변성과 코드 안정성

// 설정 객체는 const + Object.freeze
const CONFIG = Object.freeze({
    database: {
        host: "localhost",
        port: 5432,
        name: "myapp"
    },
    cache: {
        ttl: 3600,
        maxSize: 100
    }
});

// 배열 불변성 유지
const initialTodos = Object.freeze([
    { id: 1, text: "할 일 1", done: false },
    { id: 2, text: "할 일 2", done: true }
]);

// 새 배열 생성으로 변경
const updatedTodos = initialTodos.map(todo => 
    todo.id === 1 ? { ...todo, done: true } : todo
);

완전한 예제

🎉 실제 프로젝트 예제

// 애플리케이션 설정 (const)
const APP_CONFIG = {
    name: "Todo App",
    version: "1.0.0",
    api: {
        baseUrl: "https://api.todoapp.com",
        timeout: 5000
    },
    ui: {
        theme: "light",
        language: "ko"
    }
};

// 상태 관리 클래스
class TodoManager {
    constructor() {
        // private 변수들 (const로 초기화)
        const STORAGE_KEY = "todos";
        const MAX_TODOS = 100;

        // 인스턴스 변수들 (let 사용)
        let todos = this.loadTodos();
        let currentFilter = "all";

        // 메소드에서 클로저로 접근
        this.getTodos = () => todos;
        this.getFilter = () => currentFilter;

        // 공개 메소드들
        this.addTodo = (text) => {
            if (todos.length >= MAX_TODOS) {
                throw new Error(`최대 ${MAX_TODOS}개까지만 추가 가능합니다.`);
            }

            const newTodo = {
                id: Date.now(),
                text: text,
                completed: false,
                createdAt: new Date()
            };

            todos = [...todos, newTodo];
            this.saveTodos();
            return newTodo;
        };

        this.deleteTodo = (id) => {
            const originalLength = todos.length;
            todos = todos.filter(todo => todo.id !== id);

            if (todos.length === originalLength) {
                throw new Error("삭제할 할 일을 찾을 수 없습니다.");
            }

            this.saveTodos();
        };

        this.toggleTodo = (id) => {
            todos = todos.map(todo => 
                todo.id === id 
                    ? { ...todo, completed: !todo.completed }
                    : todo
            );
            this.saveTodos();
        };

        this.setFilter = (filter) => {
            const validFilters = ["all", "active", "completed"];
            if (!validFilters.includes(filter)) {
                throw new Error(`유효하지 않은 필터: ${filter}`);
            }
            currentFilter = filter;
        };

        this.getFilteredTodos = () => {
            switch (currentFilter) {
                case "active":
                    return todos.filter(todo => !todo.completed);
                case "completed":
                    return todos.filter(todo => todo.completed);
                default:
                    return todos;
            }
        };

        // private 메소드들
        this.loadTodos = () => {
            try {
                const saved = localStorage.getItem(STORAGE_KEY);
                return saved ? JSON.parse(saved) : [];
            } catch (error) {
                console.error("할 일 목록 로드 실패:", error);
                return [];
            }
        };

        this.saveTodos = () => {
            try {
                localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
            } catch (error) {
                console.error("할 일 목록 저장 실패:", error);
            }
        };
    }
}

// 사용 예제
const todoManager = new TodoManager();

// 이벤트 핸들러들 (let 사용)
let addButton = document.getElementById("addBtn");
let todoInput = document.getElementById("todoInput");

// 상수로 DOM 요소 참조
const todoList = document.getElementById("todoList");
const filterButtons = document.querySelectorAll(".filter-btn");

// 할 일 추가 함수
function addTodo() {
    const text = todoInput.value.trim();

    if (!text) {
        alert("할 일을 입력해주세요.");
        return;
    }

    try {
        todoManager.addTodo(text);
        todoInput.value = "";
        renderTodos();
    } catch (error) {
        alert(error.message);
    }
}

// 할 일 렌더링 함수
function renderTodos() {
    const todos = todoManager.getFilteredTodos();

    // 기존 목록 초기화
    todoList.innerHTML = "";

    // 할 일이 없을 때
    if (todos.length === 0) {
        const emptyMessage = document.createElement("li");
        emptyMessage.textContent = "할 일이 없습니다.";
        emptyMessage.className = "empty-message";
        todoList.appendChild(emptyMessage);
        return;
    }

    // 각 할 일 렌더링
    todos.forEach(todo => {
        const li = document.createElement("li");
        li.className = `todo-item ${todo.completed ? "completed" : ""}`;

        li.innerHTML = `
            <span class="todo-text">${todo.text}</span>
            <div class="todo-actions">
                <button onclick="toggleTodo(${todo.id})" class="toggle-btn">
                    ${todo.completed ? "취소" : "완료"}
                </button>
                <button onclick="deleteTodo(${todo.id})" class="delete-btn">
                    삭제
                </button>
            </div>
        `;

        todoList.appendChild(li);
    });
}

// 전역 함수들 (window 객체 오염 방지를 위해 네임스페이스 사용 고려)
function toggleTodo(id) {
    todoManager.toggleTodo(id);
    renderTodos();
}

function deleteTodo(id) {
    if (confirm("정말 삭제하시겠습니까?")) {
        todoManager.deleteTodo(id);
        renderTodos();
    }
}

// 이벤트 리스너 등록
addButton.addEventListener("click", addTodo);
todoInput.addEventListener("keypress", (e) => {
    if (e.key === "Enter") {
        addTodo();
    }
});

// 필터 버튼 이벤트
filterButtons.forEach(button => {
    button.addEventListener("click", () => {
        // 활성 버튼 변경
        filterButtons.forEach(btn => btn.classList.remove("active"));
        button.classList.add("active");

        // 필터 적용
        const filter = button.dataset.filter;
        todoManager.setFilter(filter);
        renderTodos();
    });
});

// 초기 렌더링
document.addEventListener("DOMContentLoaded", () => {
    renderTodos();
});

// 통계 정보 표시
function updateStats() {
    const allTodos = todoManager.getTodos();
    const completedCount = allTodos.filter(todo => todo.completed).length;
    const totalCount = allTodos.length;

    const statsElement = document.getElementById("stats");
    if (statsElement) {
        statsElement.textContent = 
            `전체: ${totalCount}, 완료: ${completedCount}, 미완료: ${totalCount - completedCount}`;
    }
}

// 정기적으로 통계 업데이트
setInterval(updateStats, 1000);

마무리

JavaScript의 변수 선언 키워드 var, let, const는 각각 고유한 특성을 가지고 있습니다. 현대적인 JavaScript 개발에서는 const를 우선 사용하고, 재할당이 필요한 경우에만 let을 사용하는 것이 권장됩니다.

✅ 핵심 정리

  1. const: 재할당 불가, 블록 스코프, 초기화 필수
  2. let: 재할당 가능, 블록 스코프, 재선언 불가
  3. var: 함수 스코프, 호이스팅 이슈, 사용 지양

🚀 모범 사례

  1. const 우선 사용 → 코드 안정성 향상
  2. let으로 필요시 재할당 → 명확한 의도 표현
  3. var 피하기 → 예상치 못한 동작 방지
  4. 명시적 초기화 → 가독성과 디버깅 용이성

변수 선언은 JavaScript의 기본 중의 기본입니다. 이 개념을 확실히 이해하고 넘어가면 더 복잡한 JavaScript 기능들을 학습할 때 큰 도움이 될 것입니다!

변수 선언에 대한 설명이 명확했나요? 더 궁금한 점이나 어려운 부분이 있다면 댓글로 남겨주세요! 💬