JavaScript 스코프 관리: 전역 변수 vs IIFE 모듈 패턴
“왜 내 코드가 다른 스크립트랑 충돌하나요?”
프로젝트에 새 라이브러리를 추가했더니 갑자기 카운터가 작동하지 않았습니다.
// 내 코드 (counter.js)
var count = 0;
function increment() {
count++;
console.log('My count:', count);
}
// 새로 추가한 라이브러리 (analytics.js)
var count = 0; // ⚠️ 같은 이름!
function increment() {
count += 5;
}
// 버튼 클릭
increment(); // 예상: 1 증가, 실제: 5 증가!
“전역 변수가 뭐가 문제인가요?”
이 글에서는 전역 스코프의 문제점과 IIFE 모듈 패턴으로 코드를 안전하게 캡슐화하는 방법을 알아봅니다.
패턴 1: 전역 변수 사용
기본 예제: 간단한 카운터
전역 변수는 JavaScript에서 가장 기본적인 변수 선언 방식입니다. var, let, const를 최상위 레벨에서 선언하면 전역 스코프에 속합니다.
// 전역 변수
var count = 0;
var userName = 'Alice';
// 전역 함수
function increment() {
count++;
updateDisplay();
}
function updateDisplay() {
console.log(`${userName}: ${count}`);
}
// 어디서든 접근 가능
increment(); // Alice: 1
increment(); // Alice: 2
특징:
- 파일 어디서든 접근 가능
- 다른 파일에서도 접근 가능
- 브라우저 콘솔에서도 접근 가능
전역 변수의 문제점
문제 1: 이름 충돌 (Name Collision)
여러 스크립트가 같은 변수명을 사용하면 덮어씌워집니다.
// ========== script1.js ==========
var data = { user: 'Alice', score: 100 };
function saveData() {
localStorage.setItem('data', JSON.stringify(data));
}
// ========== script2.js (다른 팀이 작성) ==========
var data = { items: [1, 2, 3] }; // ⚠️ 덮어씌움!
function processData() {
console.log(data.user); // undefined (기대했던 값이 아님)
}
// ========== index.html ==========
<script src="script1.js"></script>
<script src="script2.js"></script> // data가 덮어씌워짐
<script>
saveData(); // 에러! script1의 data가 사라짐
</script>
실제 발생 시나리오:
- 여러 개발자가 협업할 때
- 외부 라이브러리 추가 시
- 레거시 코드와 새 코드 통합 시
문제 2: 외부에서 수정 가능 (No Encapsulation)
전역 변수는 누구나 수정할 수 있습니다.
// 내 코드
var maxAttempts = 3;
var currentAttempts = 0;
function tryLogin() {
if (currentAttempts >= maxAttempts) {
alert('Too many attempts!');
return;
}
currentAttempts++;
// 로그인 로직...
}
// 악의적인 사용자가 개발자 콘솔에서
maxAttempts = 999999; // ⚠️ 보안 우회!
currentAttempts = 0; // ⚠️ 재설정 가능!
// 또는 다른 스크립트가 실수로
currentAttempts = "문자열"; // ⚠️ 타입까지 변경!
보안 위험:
- 브라우저 확장 프로그램이 접근 가능
- XSS 공격 시 조작 가능
- 사용자가 개발자 도구로 수정 가능
문제 3: 네임스페이스 오염 (Namespace Pollution)
변수가 많아질수록 전역 공간이 혼잡해집니다.
// 여러 기능을 추가하다 보면...
var userCount = 0;
var adminCount = 0;
var guestCount = 0;
var totalCount = 0;
var activeCount = 0;
var inactiveCount = 0;
function incrementUserCount() { userCount++; }
function incrementAdminCount() { adminCount++; }
function incrementGuestCount() { guestCount++; }
// ... 수십 개의 함수
// window 객체가 오염됨
console.log(Object.keys(window).length); // 수백 개...
문제점:
- 어떤 변수가 어디서 사용되는지 추적 어려움
- 실수로 기존 변수를 덮어쓸 위험
- 메모리 관리 어려움 (가비지 컬렉션 불가)
문제 4: 테스트 어려움
전역 상태는 테스트 간 독립성을 해칩니다.
// 전역 변수 사용
var items = [];
function addItem(item) {
items.push(item);
}
// ========== 테스트 ==========
// 테스트 1
addItem('apple');
console.assert(items.length === 1); // ✅ 통과
// 테스트 2
addItem('banana');
console.assert(items.length === 1); // ❌ 실패! (length가 2)
// 테스트 1의 영향을 받음
// 매 테스트마다 초기화 필요
items = []; // 수동으로 리셋해야 함
패턴 2: IIFE 모듈 패턴
IIFE란?
IIFE (Immediately Invoked Function Expression): 정의되자마자 즉시 실행되는 함수 표현식입니다.
// 기본 구조
(function() {
// 이 안의 코드는 즉시 실행됨
console.log('Hello!');
})();
// 화살표 함수 버전
(() => {
console.log('Hello with arrow function!');
})();
구조 분해:
()- 함수를 표현식으로 만듦function() { }- 익명 함수 정의()- 즉시 실행
IIFE로 스코프 격리하기
IIFE를 사용하면 변수와 함수를 독립적인 스코프에 가둘 수 있습니다.
// ========== 전역 스코프 ==========
(() => {
// ========== 격리된 스코프 ==========
let count = 0;
function increment() {
count++;
console.log('Count:', count);
}
function decrement() {
count--;
console.log('Count:', count);
}
// 이벤트 연결
document.getElementById('btn-inc')?.addEventListener('click', increment);
document.getElementById('btn-dec')?.addEventListener('click', decrement);
})();
// 외부에서 접근 불가
console.log(count); // ReferenceError: count is not defined
console.log(window.count); // undefined
핵심:
count,increment,decrement가 IIFE 내부에만 존재- 외부에서 접근하거나 수정 불가
- 다른 스크립트와 이름 충돌 없음
실전 예제: 여러 모듈 공존
// ========== 모듈 A: 사용자 카운터 ==========
(() => {
let count = 0; // 모듈 A의 count
function increment() {
count++;
console.log('Users:', count);
}
document.getElementById('user-btn')?.addEventListener('click', increment);
})();
// ========== 모듈 B: 방문자 카운터 ==========
(() => {
let count = 0; // 모듈 B의 count (독립적!)
function increment() {
count += 5;
console.log('Visitors:', count);
}
document.getElementById('visitor-btn')?.addEventListener('click', increment);
})();
// 두 모듈의 count는 서로 영향을 주지 않음!
// - 사용자 버튼 클릭 → "Users: 1"
// - 방문자 버튼 클릭 → "Visitors: 5"
외부 API 노출하기
필요한 경우 선택적으로 일부 기능을 외부에 노출할 수 있습니다.
// ========== 패턴 1: 전역 객체에 할당 ==========
const MyCounter = (() => {
// 비공개 변수
let count = 0;
// 비공개 함수
function validateCount() {
return count >= 0 && count <= 100;
}
// 공개 API
return {
increment: () => {
if (count < 100) {
count++;
}
return count;
},
decrement: () => {
if (count > 0) {
count--;
}
return count;
},
getCount: () => count
};
})();
// 사용
console.log(MyCounter.getCount()); // 0
MyCounter.increment();
console.log(MyCounter.getCount()); // 1
// 비공개 변수는 접근 불가
console.log(MyCounter.count); // undefined
장점:
count는 외부에서 직접 수정 불가 (캡슐화)validateCount는 완전히 비공개- 공개 API만 노출하여 의도한 대로만 사용 가능
// ========== 패턴 2: 이벤트 버스 ==========
const EventBus = (() => {
// 비공개 저장소
const events = {};
return {
// 이벤트 등록
on: (event, callback) => {
if (!events[event]) {
events[event] = [];
}
events[event].push(callback);
},
// 이벤트 발생
emit: (event, data) => {
if (events[event]) {
events[event].forEach(callback => callback(data));
}
}
};
})();
// 사용
EventBus.on('user-login', (user) => {
console.log('User logged in:', user);
});
EventBus.emit('user-login', { name: 'Alice' });
// events 객체는 접근 불가 (보호됨)
console.log(EventBus.events); // undefined
핵심 차이점 비교
1. 스코프와 접근성
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 전역 변수: 어디서든 접근 가능
var apiKey = 'secret-key-123';
var userData = { name: 'Alice' };
console.log(window.apiKey); // 'secret-key-123' (노출됨!)
apiKey = 'hacked!'; // 외부에서 수정 가능
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// IIFE: 격리된 스코프
(() => {
const apiKey = 'secret-key-123';
const userData = { name: 'Alice' };
// 내부에서만 사용
function authenticate() {
// apiKey 사용
}
})();
console.log(window.apiKey); // undefined (보호됨)
apiKey = 'hacked!'; // ReferenceError (접근 불가)
2. 이름 충돌
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 전역 변수: 충돌 발생
// file1.js
var config = { theme: 'dark' };
// file2.js (다른 개발자)
var config = { lang: 'ko' }; // 덮어씌움!
console.log(config.theme); // undefined (사라짐)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// IIFE: 충돌 없음
// file1.js
(() => {
const config = { theme: 'dark' };
// 사용...
})();
// file2.js
(() => {
const config = { lang: 'ko' }; // 독립적!
// 사용...
})();
// 각 모듈의 config는 서로 영향 없음
3. 메모리 관리
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 전역 변수: 페이지가 닫힐 때까지 메모리 점유
var largeData = new Array(1000000).fill('data');
// 사용이 끝나도 메모리에 계속 남아있음
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// IIFE: 함수 실행 후 가비지 컬렉션 가능
(() => {
const largeData = new Array(1000000).fill('data');
processData(largeData);
})();
// IIFE 실행 후 largeData는 메모리에서 해제될 수 있음
4. 보안성
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 전역 변수: 보안에 취약
var password = 'myPassword123';
var isAdmin = false;
// 사용자가 콘솔에서
isAdmin = true; // ⚠️ 권한 상승!
password = ''; // ⚠️ 데이터 조작!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// IIFE: 보호됨
(() => {
let password = 'myPassword123';
let isAdmin = false;
// 검증 로직...
})();
// 외부에서 접근 불가
isAdmin = true; // ReferenceError
실전 예제: 타이머 애플리케이션
전역 변수 사용 (문제점 있음)
// 전역 공간에 모든 것이 노출됨
var seconds = 0;
var minutes = 0;
var intervalId = null;
var isPaused = false;
function start() {
if (intervalId) return;
isPaused = false;
intervalId = setInterval(function() {
seconds++;
if (seconds >= 60) {
seconds = 0;
minutes++;
}
updateDisplay();
}, 1000);
}
function pause() {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
isPaused = true;
}
}
function reset() {
pause();
seconds = 0;
minutes = 0;
updateDisplay();
}
function updateDisplay() {
var display = pad(minutes) + ':' + pad(seconds);
document.getElementById('timer').textContent = display;
}
function pad(num) {
return num < 10 ? '0' + num : num;
}
// 초기화
updateDisplay();
document.getElementById('start').onclick = start;
document.getElementById('pause').onclick = pause;
document.getElementById('reset').onclick = reset;
// 문제점:
// 1. seconds, minutes 등이 전역에 노출
// 2. 다른 타이머를 추가하면 변수명 충돌
// 3. 외부에서 seconds = -999 같은 조작 가능
IIFE 모듈 패턴 사용 (개선됨)
/**
* 타이머 모듈
* IIFE로 캡슐화하여 독립적인 타이머 생성
*/
(() => {
'use strict';
// ==================== 상수 정의 ====================
const CONFIG = {
TICK_INTERVAL: 1000, // 1초
};
const SELECTORS = {
timer: '#timer',
btnStart: '#start',
btnPause: '#pause',
btnReset: '#reset',
};
// ==================== 비공개 상태 ====================
let seconds = 0;
let minutes = 0;
let intervalId = null;
let isPaused = false;
// ==================== 비공개 유틸리티 ====================
const pad = (num) => (num < 10 ? '0' : '') + num;
const formatTime = () => `${pad(minutes)}:${pad(seconds)}`;
// ==================== 비공개 함수 ====================
const updateDisplay = () => {
const timerEl = document.querySelector(SELECTORS.timer);
if (timerEl) {
timerEl.textContent = formatTime();
}
};
const tick = () => {
seconds++;
if (seconds >= 60) {
seconds = 0;
minutes++;
}
updateDisplay();
};
const start = () => {
if (intervalId) return; // 이미 실행 중
isPaused = false;
intervalId = setInterval(tick, CONFIG.TICK_INTERVAL);
console.log('▶ Timer started');
};
const pause = () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
isPaused = true;
console.log('⏸ Timer paused');
}
};
const reset = () => {
pause();
seconds = 0;
minutes = 0;
updateDisplay();
console.log('🔄 Timer reset');
};
// ==================== 초기화 ====================
const attachEvents = () => {
document.querySelector(SELECTORS.btnStart)
?.addEventListener('click', start);
document.querySelector(SELECTORS.btnPause)
?.addEventListener('click', pause);
document.querySelector(SELECTORS.btnReset)
?.addEventListener('click', reset);
};
const init = () => {
updateDisplay();
attachEvents();
console.log('✅ Timer initialized');
};
// DOM 로드 완료 후 실행
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
// 장점:
// ✅ 모든 변수가 외부로 노출되지 않음
// ✅ 여러 타이머를 독립적으로 생성 가능
// ✅ 외부에서 상태 조작 불가 (보안)
// ✅ 깔끔한 구조와 명확한 역할 분담
여러 독립적인 타이머 만들기
// 같은 코드를 약간 수정하여 재사용
const createTimer = (selectors) => {
return (() => {
let seconds = 0;
let intervalId = null;
const start = () => {
if (intervalId) return;
intervalId = setInterval(() => {
seconds++;
document.querySelector(selectors.display).textContent = seconds;
}, 1000);
};
const reset = () => {
clearInterval(intervalId);
intervalId = null;
seconds = 0;
document.querySelector(selectors.display).textContent = seconds;
};
document.querySelector(selectors.start)?.addEventListener('click', start);
document.querySelector(selectors.reset)?.addEventListener('click', reset);
return { start, reset }; // 필요시 API 노출
})();
};
// 두 개의 독립적인 타이머
const timer1 = createTimer({
display: '#timer1',
start: '#start1',
reset: '#reset1'
});
const timer2 = createTimer({
display: '#timer2',
start: '#start2',
reset: '#reset2'
});
// 두 타이머는 서로 영향을 주지 않음!
성능 비교
메모리 사용
// 전역 변수: 계속 메모리 점유
var tempData = new Array(1000000).fill(0);
processData(tempData);
// tempData는 페이지가 닫힐 때까지 메모리에 남음
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// IIFE: 가비지 컬렉션 가능
(() => {
const tempData = new Array(1000000).fill(0);
processData(tempData);
})();
// 실행 후 tempData는 가비지 컬렉션 대상이 됨
실행 속도
// 벤치마크 테스트 결과
// 전역 함수 호출
function add(a, b) {
return a + b;
}
for (let i = 0; i < 1000000; i++) {
add(i, i + 1);
}
// 평균: ~10ms
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// IIFE 내부 함수 호출
(() => {
const add = (a, b) => a + b;
for (let i = 0; i < 1000000; i++) {
add(i, i + 1);
}
})();
// 평균: ~10ms
결론: 실행 속도는 거의 동일
성능보다는 코드 구조와 유지보수성에 집중
언제 어떤 패턴을 사용할까?
전역 변수가 적합한 경우
✅ 초간단 스크립트 (5줄 이하)
// 버튼 하나만 다루는 경우
document.getElementById('btn').onclick = () => {
alert('Clicked!');
};
✅ 전역 설정 (실제로 전역이어야 하는 경우)
// 앱 전체에서 사용하는 설정
const APP_CONFIG = {
API_URL: 'https://api.example.com',
VERSION: '1.0.0'
};
✅ 학습/데모 목적
// 튜토리얼이나 빠른 프로토타입
var count = 0;
function increment() {
count++;
console.log(count);
}
IIFE가 필수인 경우
✅ 여러 스크립트 통합
// 여러 팀이 작업하는 대규모 프로젝트
// 각 모듈을 IIFE로 격리
✅ 라이브러리/플러그인 개발
// jQuery 플러그인처럼 독립적인 모듈
const MyPlugin = (() => {
// 내부 로직...
return { init, destroy };
})();
✅ 보안이 중요한 코드
// 결제, 인증 등 민감한 로직
(() => {
const secretKey = '...';
// 외부 접근 차단
})();
✅ 테스트 가능한 코드
// 단위 테스트가 필요한 경우
const createCounter = () => {
return (() => {
let count = 0;
return {
increment: () => ++count,
getValue: () => count
};
})();
};
마이그레이션 가이드
전역 변수를 IIFE로 단계적으로 마이그레이션하는 방법입니다.
Step 1: IIFE로 감싸기
// Before
var count = 0;
function increment() {
count++;
}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// After
(() => {
var count = 0;
function increment() {
count++;
}
})();
Step 2: ‘use strict’ 추가
(() => {
'use strict'; // 엄격 모드 활성화
var count = 0;
function increment() {
count++;
}
})();
Step 3: 구조화
(() => {
'use strict';
// 상수
const CONFIG = {
INITIAL_VALUE: 0
};
// 상태
let count = CONFIG.INITIAL_VALUE;
// 함수
const increment = () => {
count++;
};
// 초기화
const init = () => {
// 설정...
};
init();
})();
체크리스트
코드 리뷰 시 확인사항
전역 변수 사용 시:
[ ] 정말 전역이어야 하는가?
[ ] 다른 스크립트와 충돌 가능성은?
[ ] 외부에서 수정되면 문제가 생기는가?
[ ] 10줄 이상의 코드인가?
IIFE 사용 시:
[ ] 코드가 () => { })() 로 감싸져 있는가?
[ ] 'use strict'가 선언되어 있는가?
[ ] 외부 API가 필요하면 명시적으로 노출했는가?
[ ] 여러 모듈이 독립적으로 작동하는가?
흔한 실수와 해결책
실수 1: IIFE 문법 오류
❌ 잘못된 문법:
function() {
console.log('Hi');
}();
// SyntaxError: Function statements require a name
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 올바른 문법:
(function() {
console.log('Hi');
})();
// 또는
(() => {
console.log('Hi');
})();
실수 2: IIFE 내부에서 외부 변수 오염
❌ 실수로 전역 변수 생성:
(() => {
count = 0; // var/let/const 없음 → 전역!
})();
console.log(window.count); // 0 (오염됨)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 'use strict'로 방지:
(() => {
'use strict';
count = 0; // ReferenceError!
})();
실수 3: 이벤트 리스너에서 접근 불가
❌ 외부에서 함수에 접근하려고 시도:
(() => {
function handleClick() {
console.log('Clicked');
}
})();
// HTML에서
<button onclick="handleClick()">Click</button>
// ReferenceError: handleClick is not defined
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ IIFE 내부에서 이벤트 연결:
(() => {
function handleClick() {
console.log('Clicked');
}
document.querySelector('#btn')
?.addEventListener('click', handleClick);
})();
// HTML
<button id="btn">Click</button>
마치며
전역 변수 vs IIFE의 핵심:
전역 변수는 간편하지만 위험합니다:
- 빠르게 작성 가능
- 이름 충돌, 보안 문제, 유지보수 어려움
IIFE는 안전하고 확장 가능합니다:
- 초기 설정이 필요
- 스코프 격리, 캡슐화, 테스트 용이
추천:
- 소규모 스크립트: 전역 변수로 시작
- 10줄 이상: IIFE로 전환 고려
- 팀 프로젝트: 처음부터 IIFE 사용
- 라이브러리: 반드시 IIFE 사용
작은 것부터 시작하세요:
- 기존 코드를 IIFE로 감싸기
- ‘use strict’ 추가
- 섹션별로 구조화
- 필요시 외부 API 노출
하나씩 적용하다 보면, 어느새 안전하고 유지보수하기 좋은 코드를 작성하게 될 겁니다!
참고 자료
공식 문서
디자인 패턴
- Module Pattern - JavaScript Design Patterns
- JavaScript Module Systems
스타일 가이드
관련 문서
- 📖 JavaScript 함수 선언 방식 비교: function vs 화살표 함수 - this 바인딩과 문법 차이
- 🔧 ES Modules vs CommonJS - 현대적 모듈 시스템
- 🏗 JavaScript 디자인 패턴 - 실전 패턴 모음
- ⚡ JavaScript 클로저 완전 가이드 - IIFE의 기반 개념
댓글