Skip to content

Latest commit

 

History

History
367 lines (265 loc) · 11.4 KB

File metadata and controls

367 lines (265 loc) · 11.4 KB

전역변수의 사용을 지양하자

  • 웬만하면 지역변수 사용해야함
  • 이유는 아래와 같다

지역 변수의 생명주기는 함수의 생명주기와 대부분 일치

  • 함수 몸체의 변수(=지역변수)들은 함수가 호출되기 전까지는 생성되지 않음
    • 컴파일 시에 선언된 변수들이 다 등록되는건 전역 변수만 해당
    • 함수는 실행 컨텍스트를 만들고 그 안에서 변수 객체를 생성함
  • 함수를 호출하고 나서야 함수 몸체에 선언된 변수들을 등록하고 코드를 한줄 씩 실행함
    • 즉, 호이스팅은 스코프 단위로 동작함
    • 호이스팅은 변수 선언이 스코프의 선두로 끌어올려진 것처럼 동작하는 자바스크립트 특징
    • 실제로 코드가 옮겨지는 것이 아니라 자바스크립트 엔진의 작동 방식임
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 모듈의 독립적인 스코프

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에서는 모듈에서 명시적으로 내보내지 않은 변수에는 접근할 수 없습니다.

모듈 간 변수 공유 방법

모듈 간에 데이터를 공유하려면 exportimport를 사용해야 합니다. 다음은 몇 가지 방법입니다:

1. 상태를 관리하는 모듈 생성하기

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.jsmoduleD.js)은 이 상태에 접근하고 수정할 수 있습니다. 이렇게 하면 전역 변수 없이도 모듈 간에 상태를 공유할 수 있습니다.

2. 싱글톤 패턴 구현하기

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에서 내보낸 객체를 여러 모듈에서 가져와 사용합니다. 이 객체는 참조로 전달되므로 한 모듈에서 변경하면 다른 모듈에서도 그 변경사항이 반영됩니다.

주의사항

모듈 간에 상태를 공유할 때는 주의해야 할 점이 있습니다:

  1. 예측 불가능한 동작: 여러 모듈이 공유 상태를 수정하면 추적하기 어려운 버그가 발생할 수 있습니다.
  2. 테스트 어려움: 공유 상태가 있는 모듈은 격리하여 테스트하기 어려울 수 있습니다.
  3. 순환 의존성: 모듈 간에 순환 의존성이 생길 수 있으므로 구조를 잘 설계해야 합니다.

이러한 이유로 Redux나 MobX 같은 상태 관리 라이브러리를 사용하는 경우가 많습니다. 이러한 라이브러리는 모듈 간 상태 공유를 더 체계적으로 관리할 수 있게 해줍니다.