- 웬만하면 지역변수 사용해야함
- 이유는 아래와 같다
- 함수 몸체의 변수(=지역변수)들은 함수가 호출되기 전까지는 생성되지 않음
- 컴파일 시에 선언된 변수들이 다 등록되는건 전역 변수만 해당
- 함수는 실행 컨텍스트를 만들고 그 안에서 변수 객체를 생성함
- 함수를 호출하고 나서야 함수 몸체에 선언된 변수들을 등록하고 코드를 한줄 씩 실행함
- 즉, 호이스팅은 스코프 단위로 동작함
- 호이스팅은 변수 선언이 스코프의 선두로 끌어올려진 것처럼 동작하는 자바스크립트 특징
- 실제로 코드가 옮겨지는 것이 아니라 자바스크립트 엔진의 작동 방식임
function foo() {
console.log(x); // undefined (변수 호이스팅으로 인해 선언은 되었지만 아직 초기화되지 않음)
var x = 'local';
console.log(x); // 'local' (변수에 값이 할당된 후)
return x;
}
foo();
console.log(x); // ReferenceError: x is not defined (x는 foo 함수의 지역 변수이므로 전역에서 접근 불가)- 함수 내부에 선언된 변수는 함수가 생성한 스코프에 등록됨
- 함수가 생성한 스코프를 참조하는 대상이 없으면 스코프는 가비지 컬렉터가 정리(= 스코프에 등록된 변수들도 정리)
- 누군가가 스코프를 계속 참조하면 사라지지 않고 남아있음 이를 클로저라고함
- 이는 메모리 효율성 측면에서 중요한 개념임
- 반면 전역코드는 함수와 달리 명시적인 호출 없이 실행됨
- 전역 변수는 전역 객체의 프로퍼티
- 즉 전역 변수의 생명주기는 전역 객체의 생명주기와 같음
- 브라우저에서는 window.변수명 으로도 접근 가능함
- 전역 객체는 어떠한 객체보다도 가장 먼저 생성되는 객체를 뜻함
- 브라우저 → window
- node.js → global
- ES11(ECMAScript 2020)부터는 globalThis로 통일됨
- 이러한 특징 때문에 전역변수는 아래와 같은 단점이 존재
- 긴 생명 주기
- 리소스 낭비
- 재할당이 여러번 일어날 수 있음
- 메모리 누수의 원인이 될 수 있음
- 스코프 체인 종점에 존재
- 전역 변수 검색속도가 가장 느림 (큰 차이는 없다만)
- 변수를 찾을 때 로컬 스코프부터 시작해서 전역까지 올라가야 함
- 네임스페이스 오염
- 자바스크립트는 파일이 분리되어있어도 하나의 전역 스코프를 공유함
- 따라서 다른 파일 내에서 동일한 이름으로 명명된 전역 변수나 전역 함수가 같은 스코프내에 존재해 예상치못한 오류가 발생할 수 있음
- 라이브러리 간의 충돌이 발생하기도 함
- 즉시 실행 함수 사용
- 코드를 즉시 실행 함수로 감싸서 실행하면 그 안의 모든 변수들은 즉시 실행 함수의 지역 변수가 됨
- 한 번만 실행되고 사라지므로 전역 스코프를 오염시키지 않음
(function () {
var localVar = '이건 지역변수임';
console.log(localVar); // '이건 지역변수임'
}());
console.log(localVar); // ReferenceError: localVar is not defined- 네임스페이스 객체 사용
- 네임스페이스 역할을 할 객체를 생성하고 전역 변수처럼 사용하고 싶은 변수를 프로퍼티로 추가하는 방법
- 전역 변수 자체는 줄이면서도 관련 기능을 모아둘 수 있음
var MYAPP = {}; // 전역 네임스페이스 객체
MYAPP.name = 'KIM';
MYAPP.age = 30;
console.log(MYAPP.name); // 'KIM'
// 계층적 네임스페이스
MYAPP.person = {
name: 'LEE',
address: 'Seoul'
};
console.log(MYAPP.person.name); // 'LEE'- 모듈 패턴
- 관련 있는 변수와 함수를 모아서 즉시 실행 함수로 감싸 하나의 모듈로 만드는 방법
- 전역 변수도 억제하고 캡슐화도 가능
- 정보 은닉을 구현할 수 있음
var Counter = (function() {
// private 변수
var num = 0;
// 외부로 공개할 데이터나 메서드를 프로퍼티로 추가한 객체를 반환한다.
return {
increase() {
return ++num;
},
decrease() {
return --num;
},
getNum() {
return num;
}
};
}());
// private 변수는 외부로 노출되지않음
console.log(Counter.num); // undefined
console.log(Counter.increase()); // 1
console.log(Counter.increase()); // 2
console.log(Counter.decrease()); // 1
console.log(Counter.getNum()); // 1- 외부로 노출하고 싶은 변수나 함수는 return에.. 숨기고 싶은애들은 함수 몸체 내부에
- 클로저 개념을 활용한 것임
- 내부 변수 num은 increase와 decrease 메서드에서 접근 가능함
- ES6 모듈
- ES6 모듈을 사용하면 전역 변수 사용 불가능함
- ES6 모듈은 파일 자체의 독자적인 모듈 스코프를 제공하기 때문
- import와 export 키워드로 모듈 간 의존성을 명시적으로 관리할 수 있음
// math.js
export function add(x, y) {
return x + y;
}
// app.js
import { add } from './math.js';
console.log(add(1, 2)); // 3- 브라우저에서는 script 태그에 type="module" 속성을 추가해야 함
- 모듈 시스템은 대규모 애플리케이션의 코드 구조화에 매우 중요함
ES6 모듈이 어떻게 독립적인 스코프를 가지는지, 그리고 필요할 때 모듈 간에 변수를 어떻게 공유할 수 있는지 설명해 드리겠습니다.
ES6 모듈은 각각 자신만의 스코프를 가집니다. 이는 한 모듈에서 선언된 변수가 다른 모듈에서 자동으로 접근할 수 없다는 것을 의미합니다. 이 특성이 전역 네임스페이스 오염을 방지하는 데 도움이 됩니다.
moduleA.js
// 이 변수는 moduleA.js 내에서만 존재합니다
const message = "모듈 A에서 안녕하세요";
console.log(message); // 정상 작동: "모듈 A에서 안녕하세요"
// 공유하고 싶다면 특정 항목을 내보낼 수 있습니다
export function sayHello() {
console.log(message);
}moduleB.js
// 이 변수는 moduleB.js 내에서만 존재합니다
const message = "모듈 B에서 안녕하세요";
console.log(message); // 정상 작동: "모듈 B에서 안녕하세요"
// 필요하다면 이것도 내보낼 수 있습니다
export function greet() {
console.log(`인사: ${message}`);
}main.js
import { sayHello } from './moduleA.js';
import { greet } from './moduleB.js';
sayHello(); // "모듈 A에서 안녕하세요"
greet(); // "인사: 모듈 B에서 안녕하세요"
// 다음 코드는 오류를 발생시킵니다:
console.log(message); // ReferenceError: message is not defined여기서 볼 수 있듯이, 각 모듈은 동일한 이름의 변수(message)를 가지고 있지만 서로 충돌하지 않습니다. 각 모듈은 자신만의 독립적인 스코프를 가지고 있기 때문입니다. 또한 main.js에서는 모듈에서 명시적으로 내보내지 않은 변수에는 접근할 수 없습니다.
모듈 간에 데이터를 공유하려면 export와 import를 사용해야 합니다. 다음은 몇 가지 방법입니다:
store.js
// 공유할 상태를 담는 모듈
let counter = 0;
let users = [];
export function increment() {
counter++;
return counter;
}
export function getCounter() {
return counter;
}
export function addUser(user) {
users.push(user);
}
export function getUsers() {
return [...users]; // 배열의 복사본을 반환하여 직접 수정 방지
}moduleC.js
import { increment, addUser } from './store.js';
export function registerUser(name) {
const id = increment(); // 카운터 증가
const newUser = { id, name };
addUser(newUser);
return newUser;
}
변수를 직접 export 하지않았지만 store의 함수를 통해 간접적으로 변수를 다른 모듈과 공유하게 되는군moduleD.js
import { getCounter, getUsers } from './store.js';
export function displayStats() {
console.log(`총 사용자 수: ${getCounter()}`);
console.log('사용자 목록:');
getUsers().forEach(user => {
console.log(`- ID: ${user.id}, 이름: ${user.name}`);
});
}app.js
import { registerUser } from './moduleC.js';
import { displayStats } from './moduleD.js';
registerUser('김철수');
registerUser('이영희');
displayStats();
// 출력:
// 총 사용자 수: 2
// 사용자 목록:
// - ID: 1, 이름: 김철수
// - ID: 2, 이름: 이영희여기서 store.js 모듈은 상태를 관리하고, 다른 모듈(moduleC.js와 moduleD.js)은 이 상태에 접근하고 수정할 수 있습니다. 이렇게 하면 전역 변수 없이도 모듈 간에 상태를 공유할 수 있습니다.
config.js
// 싱글톤 패턴으로 설정 객체 내보내기
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
language: 'ko'
};
export default config;service1.js
import config from './config.js';
export function fetchData() {
console.log(`${config.apiUrl}에서 데이터를 가져오는 중...`);
// config 객체를 사용한 API 호출 로직
}
// config를 변경하면 다른 모듈에도 영향을 미칩니다
export function setLanguage(lang) {
config.language = lang;
console.log(`언어가 ${lang}(으)로 변경되었습니다.`);
}service2.js
import config from './config.js';
export function getSettings() {
return {
currentApiUrl: config.apiUrl,
currentLanguage: config.language
};
}main.js
import { fetchData, setLanguage } from './service1.js';
import { getSettings } from './service2.js';
console.log('초기 설정:', getSettings());
// 출력: 초기 설정: { currentApiUrl: 'https://api.example.com', currentLanguage: 'ko' }
fetchData();
// 출력: https://api.example.com에서 데이터를 가져오는 중...
setLanguage('en');
// 출력: 언어가 en(으)로 변경되었습니다.
console.log('변경된 설정:', getSettings());
// 출력: 변경된 설정: { currentApiUrl: 'https://api.example.com', currentLanguage: 'en' }이 예제에서는 config.js에서 내보낸 객체를 여러 모듈에서 가져와 사용합니다. 이 객체는 참조로 전달되므로 한 모듈에서 변경하면 다른 모듈에서도 그 변경사항이 반영됩니다.
모듈 간에 상태를 공유할 때는 주의해야 할 점이 있습니다:
- 예측 불가능한 동작: 여러 모듈이 공유 상태를 수정하면 추적하기 어려운 버그가 발생할 수 있습니다.
- 테스트 어려움: 공유 상태가 있는 모듈은 격리하여 테스트하기 어려울 수 있습니다.
- 순환 의존성: 모듈 간에 순환 의존성이 생길 수 있으므로 구조를 잘 설계해야 합니다.
이러한 이유로 Redux나 MobX 같은 상태 관리 라이브러리를 사용하는 경우가 많습니다. 이러한 라이브러리는 모듈 간 상태 공유를 더 체계적으로 관리할 수 있게 해줍니다.