SDK JS 모던 딥 다이브 스터디 5-6주차

민겸·2023년 4월 18일
0

SDK 스터디

목록 보기
5/6

이번 주차에는 저번 주를 쉬게 되어서 함수, 스코프, 전역 변수의 문제점 이렇게 총 3장의 내용을 공부하게 되었다.

스터디에서는 공부해온 것들을 주제로 토론을 하며 심화 학습을 하고, 이렇게 스터디가 끝나면 책을 보며 타이핑하는 시간이 복습에 좋은 것 같다.

12장(함수) 요약

프로그래밍 언어의 함수는 일련의 과정을 문statement으로 구현하고 코드 블록으로 감싸서 하나의 실행 단위로 정의한 것이다.

함수는 입력을 받아서 출력을 내보낸다. 이때 함수 내부로 입력을 전달받는 변수를 매개변수parameter, 입력을 인수argument, 출력을 반환값return value이라 한다. 또한 함수는 값이며, 여러 개 존재할 수 있으므로 특정 함수를 구별하기 위해 식별자인 함수 이름을 사용할 수 있다.

다음은 자바스크립트에서 함수 선언문을 사용하여 함수를 정의한 예다.

function add(x, y) {
  return x + y;
}

function 키워드를 사용한 함수 선언문이며, 함수 이름이 add, 매개변수가 함수 이름옆의 소괄호로 둘러쌓여있는 xy이고, return 키워드를 사용한 반환값이 x + y이다.

함수 정의만으로 함수가 실행되지 않는다. 함수 정의는 그저 일련의 과정을 정의한 것일 뿐이고, 함수를 실행시키려면 함수 호출이 필요하다. 함수를 호출하게 되면 코드 블록{...}에 담긴 문들이 일괄적으로 실행되고 실행 결과를 반환한다.

// sayHi라는 이름의 함수 정의
function sayHi() {
  console.log("Hello, world!");
}

sayHi(); // 함수 호출
// "Hello, World!" 가 콘솔에 찍히게 된다.

이런 귀찮아 보이는 걸 왜 알아야 하죠?

함수는 한 번 정의해놓으면 필요할 때 여러 번 호출할 수 있기 때문이다. 그 말인 즉슨, 반복적인 코드를 함수로 한 번 정의해놓고 개발자가 원하는 시점에 몇 번이든 재사용이 가능하다는 뜻이다. 바로 다음의 예와 같이 말이다.함수_재사용의_사용_예함수를 사용하지 않고 같은 코드를 여러 번 중복으로 작성하면 그 코드를 수정해야 할 때 중복된 횟수만큼 코드를 수정해야한다. 수정하는 시간이 길어지는 건 덤이고, 사람이 코드를 수정하기 때문에 실수할 확률도 높아진다.

코드의 중복을 억제하고 재사용성을 높이는 함수는 유지보수의 편의성을 높이고 실수를 줄여 코드의 신뢰성을 높이는 효과가 있다.

함수 이름은 변수 이름과 마찬가지로 자신의 역할을 잘 설명해야 한다. 대개 함수명은 동사로 짓는다. (ex.getData, setData, updateData)

함수 이름이 조금 길어지더라도 읽었을 때 어떤 역할을 하는지 직관적으로 알려줄 수 있는 것이 좋다.

함수는 객체다.

자바스크립트의 함수는 객체 타입의 값이다. 5장에서도 나오지만 다른 타입들과 마찬가지로 함수도 함수 리터럴로 생성할 수 있다.

// 변수에 함수 리터럴을 할당하는 모습
const func = function add(x, y) {
  return x + y;
}

함수 리터럴의 구성 요소는 다음과 같다.

  • 함수 이름
    • 식별자 네이밍 규칙을 준수해야 한다.
    • 함수 몸체 내에서만 참조할 수 있는 식별자이다.
    • 생략할 수 있다. 이름이 있으면 기명, 이름이 없으면 익명 함수이다.
  • 매개변수 목록
    • 0개 이상의 매개변수를 소괄호로 감싸고 쉼표로 구분한다.
    • 함수를 호출할 때, 지정한 인수가 순서대로 할당된다. 순서에 유의해야 한다.
    • 함수 몸채 내부에서 변수와 동일하게 취급되므로, 식별자 네이밍 규칙을 준수해야 한다.
  • 함수 몸체
    • 함수가 호출되었을 때 일괄적으로 실행될 문들을 하나의 실행 단위로 정의한 코드 블록이다.
    • 함수 호출 시, 실행된다.

자바스크립트에서 함수는 객체다. 일반 객체와는 달리 호출할 수 있는 객체이다.

함수 정의 방법들

함수를 정의하는 방법은 4가지가 있다.

  • 함수 선언문
    function add(x, y) { 
      return x + y; 
    }
  • 함수 표현식
    const add = function (x, y) { 
      return x + y; 
    };
  • Function 생성자 함수
    const add = new Function('x', 'y', 'return x + y');
  • 화살표 함수(ES6)
    const add = (x, y) => x + y;

위 4가지의 방법들은 모두 함수를 정의한다는 면에서 동일하지만, 차이가 있다. 함수 선언문부터 화살표 함수까지 간단하게 알아보자.

함수 선언문

  • 함수 이름을 생략할 수 없다.
  • 표현식이 아닌 문이다.
  • 함수 호이스팅이 일어난다. (런타임 이전에 식별자 생성 및 함수 객체 생성 후 할당이 이루어진다.)
  • 정의하게 되면 함수 이름과 같은 이름을 가진 식별자가 암묵적으로 생성되고 식별자는 함수를 가리킨다.

    암묵적으로 식별자가 생성되는 이유는 함수 이름은 함수 몸체 내부에서만 접근할 수 있기 때문에 함수 외부에서 함수를 호출할 수 없기 때문이다.

  • 변수에 할당하게 되면 함수 표현식으로 취급된다. (여기서는 변수가 식별자가 되며, 함수 이름으로는 외부에서 호출할 수 없다.)

함수 표현식

  • 함수 리터럴로 생성한 함수 객체를 변수에 할당하는 방식이다.
  • 함수 이름을 생략하는 것이 일반적이다.
  • 표현식인 문이다.
  • 변수 호이스팅이 일어난다. (런타임 이전에 식별자 생성, 런타임에 위치한 곳에서 평가되어 함수 객체 생성 후 할당이 이루어진다.)

Function 생성자 함수

  • Function 생성자 함수의 인수로 매개변수 목록과 함수 몸체를 문자열로 전달하면서 new 연산자와 함께 호출하여 함수 객체 생성 후 반환하는 방식이다. (new 연산자는 optional하다.)

    생성자 함수는 객체를 생성하는 함수를 말한다.

  • 클로저closure를 생성하지 않는 등, 함수 선언문이나 표현식으로 생성한 함수와 다르게 동작한다.

화살표 함수

  • ES6에 도입되었다.
  • 항상 익명 함수로 정의한다.
  • 표현 뿐만 아니라 내부 동작 또한 간략화되었다.
    • this 바인딩 방식이 다르다.
    • prototype 프로퍼티가 없다.
    • arguments 객체를 생성하지 않는다.

함수 호출

함수는 함수를 가리키는 식별자와 함수 호출 연산자()로 호출한다. 함수 호출 연산자 내에는 0개 이상의 인수를 쉼표로 구분해서 입력할 수 있다.
함수를 호출하면 현재의 실행 흐름을 중단하고 호출된 함수로 실행 흐름을 옮긴다.

매개변수와 인수

이미지 출처
함수 외부에서 함수 내부로 값을 전달할 필요가 있는 경우, 매개변수parameter를 통해 인수argument를 전달한다. 인수와 매개변수의 특징을 알아보자.

인수는

  • 함수를 호출할 때 지정한다.
  • 개수와 타입에 제한이 없다.

매개변수는

  • 함수를 정의할 때 선언한다.
  • 함수 몸체 내부에서 변수와 동일하게 취급된다.
  • 매개변수의 유효 범위는 함수 내부다.

매개변수와 인수의 수는 다를 수 있다.
매개변수의 수 > 인수의 수 일 경우, 인수가 할당되지 않은 매개변수의 값은 undefined이다.

function add(x, y){
  return x + y;
}
// x에는 첫 번째 인수 2가 할당되고, 
// y는 선언과 동시에 undefined로 초기화 되었지만 두 번째 인수가 없어 할당되지 않았다.

// 2 + undefined 의 결과는 NaN 이다.
console.log(add(2)); // NaN

매개변수의 수 < 인수의 수 일 경우, 초과된 인수는 무시되지만, 모든 인수는 암묵적으로 arguments 객체의 프로퍼티로 보관된다.

function add(x, y){
  console.log(arguments);
  return x + y;
}

add(2, 5, 10); // Arguments(3) [2, 5, 10, callee: ƒ, Symbol(Symbol.iterator): ƒ]

자바스크립트의 함수는

  • 매개변수와 인수의 개수가 일치하는지 확인하지 않고,
  • 매개변수의 타입을 사전에 지정할 수 없기 때문에

함수를 정의할 때 적절한 인수가 전달되었는지 확인할 필요가 있다.

// add 함수는 숫자를 더하기 위해 정의한 함수이다.
// javascript 특성 상, 숫자가 아니더라도 + 연산자가 문자열 연결 연산자로 쓰일 수 있기 때문에
// 사전에 조건문을 통한 검사가 필요하다.
function add(x, y) {
  if (typeof x !== 'number' || typeof y !== 'number') {
    throw new TypeError('인수는 모두 숫자로 와야 합니다.');
  }
  return x + y;
}

위와 같은 방법으로 적절한 인수가 전달되었는지 확인할 수 있다. 하지만 부적절한 호출을 사전에 방지할 수 없고 에러는 런타임에 발생하게 된다.
타입스크립트와 같은 정적 타입을 선언할 수 있는 자바스크립트의 상위 확장을 도입해서 컴파일 시점에 부적절한 호출을 방지할 수 있게 하는 것도 하나의 방법이다.

나는 타입스크립트 사용이 그저 하나의 방법이 아니라 그냥 필수라고 생각한다..

ES6에 도입된 매개변수 기본값을 사용하면 undefined로 초기화된 매개변수에 기본값을 할당할 수 있어 예기치 못한 결과를 방지할 수 있다.

function add(x = 0, y = 0, z = 0){
  return x + y + z;
}

add(1, 2); // 1 + 2 + 0 = 3

매개변수와 인수에는 최대 개수의 제한이 없지만, 개수가 많아질 수록 함수를 호출할 때 전달해야 할 인수의 순서를 고려해야 한다. 이는 함수의 사용법을 이해하기 어렵게 만들고 실수를 발생시킬 가능성을 높인다.
따라서 매개변수는 최대 3개 이상을 넘지 않는 것을 권장하며, 3개를 넘을 경우에는 객체로 된 매개변수를 선언하고 객체를 인수로 전달하는 것이 바람직하다.
객체의 프로퍼티는 순서를 신경쓰지 않아도 되고, 명시적으로 인수의 의미를 설명할 수 있어서 가독성도 좋아진다.

객체는 참조에 의한 전달 방식으로 동작한다. 인수로 지정하여 전달하고 함수 내부에서 값을 변경한다면, 외부의 원본 객체에도 영향이 가게 된다.

const person = { name: "김민겸" };

function changeValue(obj) {
  obj.name = "아무개";
}

changeValue(person);

console.log(person); // {name: "아무개"}

이렇게 함수가 외부 상태를 변경하게 되면 상태 변화를 추적하기 어려워진다. 이는 코드의 복잡성을 증가시키고 가독성을 해친다. 복잡한 코드에서 객체의 변경을 추적하는 것은 어려운 일이다. 객체의 변경을 추적하려면 옵저버Observer패턴 등을 통해 객체의 참조를 공유하는 모든 이들에게 변경 사실을 통지하고 이에 대처하는 추가 대응이 필요하다.

좀 더 쉽게 해결할 수 있는 방법이 있다.
객체의 상태 변경이 필요한 경우에 객체의 방어적 복사를 통해 원본 객체를 완전히 복사, 즉 깊은 복사를 통해 새로운 객체를 생성하고 재할당을 통해 교체하는 방법이다.

객체의 깊은 복사 방법은 다음 글을 참고해보자.
자바스크립트 객체의 깊은 복사

다양한 함수 형태

함수에는 다양한 형태가 있다. 딥 다이브 책에서는 총 6가지의 함수 형태를 소개한다.

  • 즉시 실행 함수: IIFE Immediately Invoked Function Expression
  • 재귀 함수 recursive function
  • 중첩 함수 nested function
  • 콜백 함수 callback function
  • 순수 함수와 비순수 함수

즉시 실행 함수

즉시 실행 함수는 함수 정의와 동시에 즉시 호출되는 함수를 말한다. 단 한 번만 호출되며 다시 호출할 수 없고, 일반적으로 익명 함수를 사용한다. 기명 함수를 사용해도 되지만, 식별자로 재호출 할 수 없다. 즉시 실행 함수는 반드시 그룹 연산자(...)로 감싸야 한다.

(function () {
  const x = 10;
  const y = 5;
  return x + y;
}());

즉시 실행 함수 내에 코드를 모아 두면 혹시 있을 수도 있는 변수나 함수 이름의 충돌을 방지할 수 있다.

재귀 함수

재귀 함수는 자기 자신을 호출하는 함수를 말한다. 재귀 함수는 주로 반복되는 처리를 위해 사용한다.

function countDown(n) {
  for (let i = n; i >= 0; i--){
    console.log(i);
  }
}
// 위와 아래의 함수는 같은 결과를 도출한다.
function countDown(n) {
  if(n < 0) return;
  console.log(n);
  countDown(n-1);
}

재귀 함수를 사용하면 반복문 없이 반복되는 처리를 구현할 수 있다. 하지만, 재귀 함수는 호출을 억제할 수 있는 탈출 조건이 반드시 필요(탈출 조건이 없다면, 스택 오버플로우가 발생한다 에러가 발생한다.)하기 때문에 반복문을 사용하는 것보다 가독성이 좋을 때만 한정적으로 사용하는 것을 권장한다.

중첩 함수

중첩 함수는 함수 내부에 정의된 함수를 말한다. 내부 함수라고도 하며, 이 경우 내부 함수를 둘러싼 함수를 외부 함수라 부른다.

function outer() { // 외부 함수 outer
  let x = 1;
  
  function inner() { // 내부 함수 inner
    let y = 2;
    console.log(x+y);
  }
  
  inner();
}

중첩 함수는 스코프와 클로저에 깊은 관련이 있다.

클로저에 대해서는 다음 글을 참고해보자.
클로저 (Clouser)

콜백 함수

콜백 함수는 함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수를 말한다. 이때 매개변수를 통해 함수의 외부에서 콜백 함수를 전달받은 함수를 고차 함수HOF High-Order Function라고 한다.
다음과 같이 어떤 일을 반복 수행하는 repeat함수를 정의해 보자.

function repeat(n) {
  for(let i = 0; i < n; i++){
    console.log(i);
  }
}

repeat함수는 매개변수를 통해 전달받은 숫자만큼 반복하여 console.log(i)를 호출한다. repeat함수의 반복문 내에서 다른 일을 하고 싶다면 함수를 새롭게 정의해야 한다.

function repeatEven(n) {
  for(let i = 0; i < n; i++){
    if(i % 2 === 0) console.log(i);
  }
}

위의 두 함수들은 어떤 명령들을 반복시키는 것을 공통적으로 수행하지만 반복하면서 수행하는 명령들이 다르다. 이렇게 함수의 일부분만이 다를 때마다 매번 함수를 새롭게 정의하는 것은 실수를 줄이기 위해 함수를 재사용하는 맥락에서 같은 문제점을 유발할 가능성이 있다. 이를 방지하기 위해 함수를 합성하는 방법이 있다.
함수의 변하지 않는 공통 로직은 미리 정의해 두고, 경우에 따라 변경되는 로직은 추상화해서 함수 외부에서 내부로 전달하는 것이다.

// 변하지 않는 공통 로직(반복 수행) 정의
function repeat(n, f){ // 경우에 따라 변경되는 로직은 f로 추상화
  for(let i = 0; i < n; i++){
    f(i);
  }
}

function logAll(i){ // 모든 숫자를 로그로 출력하는 로직
  console.log(i);
}

function logEven(i){ // 짝수만 로그로 출력하는 로직
  if(i % 2 === 0) console.log(i);
}

repeat(5, logAll); // 0 1 2 3 4
repeat(5, logEven); // 0 2 4

중첩 함수가 외부 함수를 돕는 헬퍼 함수의 역할을 하는 것처럼 콜백 함수도 고차 함수에 전달되어 헬퍼 함수의 역할을 한다. 단, 중첩 함수는 함수 내부에 고정되어 있어서 교체가 힘들지만 콜백 함수는 함수 외부에서 내부로 주입하기 때문에 자유롭게 교체할 수 있다.
즉, 고차 함수는 콜백 함수를 자신의 일부분으로 합성한다.

콜백 함수는 함수형 프로그래밍 패러다임뿐만 아니라 비동기 처리(이벤트 처리, Ajax 통신, 타이머 함수 등)에 활용되는 중요한 패턴이다. 또한 배열 고차 함수 Array.prototype.map, Array.prototye.filter, Array.prototye.reduce 등 에서도 사용된다.

순수 함수와 비순수 함수

순수 함수는 동일한 인수가 전달되면 언제나 동일한 값을 반환하는 함수를 말하고,
비순수 함수는 외부 상태에 따라 반환값이 달라지는 함수를 말한다.

위에서 객체를 인수로 지정하는 경우를 살펴본 적이 있는데 이 경우가 비순수 함수에 해당된다. 함수가 외부 상태를 변경하면 상태 변화를 추적하기 어려워지고, 이를 방지하기 위한 방법도 알아보았다.
이렇게 비순수 함수의 사용을 억제하여 부수 효과를 최대한 방지하고 로직 내에 존재하는 조건문과 반복문을 제거해서 복잡성을 해결하며, 변수 사용을 억제하거나 생명주기를 최소화해서 상태 변경을 피해 오류를 최소화하는 목표를 가진 프로그래밍 패러다임이 함수형 프로그래밍이다.

자바스크립트는 멀티 패러다임 언어이므로 객체지향 프로그래밍뿐만 아니라 함수형 프로그래맹 또한 적극적으로 활용하고 있다.

13장(스코프) 요약

정의

스코프는 식별자가 유요한 범위를 말한다. 모든 식별자(변수 이름, 함수 이름, 클래스 이름 등)는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효 범위가 결정된다.

var x = "global";

function func() {
  var x = "local";
  console.log(x);
}

func();

console.log(x);

위 예제에서 자바스크립트 엔진은 이름이 같은(x) 두 개의 변수 중에서 어떤 변수를 참조해야 할 것인지를 결정해야 한다. 이를 식별자 결정이라 한다. 자바스크립트 엔진은 스코프를 통해 어떤 변수를 참조해야 할 것인지를 결정한다. 따라서 스코프는 자바스크립트 엔진이 식별자를 검색할 때 사용하는 규칙이라고도 할 수 있다.

자바스크립트 엔진은 코드를 실행할 때 코드가 어디서 실행되며 주변에 어떤 코드가 있는지와 같은 문맥context을 고려한다.

코드의 문맥과 환경
코드가 어디서 실행되며 주변에 어떤 코드가 있는지렉시컬 환경 lexical environment이라고 부른다. 즉, 코드의 문맥은 렉시컬 환경으로 이루어진다. 이를 구현한 것이 실행 컨텍스트 execution context이며, 모든 코드는 실행 컨텍스트에서 평가되고 실행된다. 스코프는 실행 컨텍스트와 깊은 연관이 있다.

위 예제에서 두 개의 x 변수는 식별자 이름이 동일하지만 자신이 유효한 범위, 즉 스코프가 다른 별개의 변수다. 다음과 같이 되어 있다고 생각하면 될 것 같다.

전역스코프: {
  x: 'global',
  func함수스코프: {
  		x: 'local'
	}
}

스코프라는 개념이 없다면 같은 이름을 갖는 변수는 충돌을 일으키므로 전체 프로그램에서 단 하나만 사용할 수 있을 것이다.
프로그래밍 언어에서는 스코프(유효 범위)를 통해 식별자인 변수 이름의 충돌을 방지하여 같은 이름의 변수를 사용할 수 있게 한다. 즉, 스코프는 네임 스페이스라고도 할 수 있다.

종류

코드는 전역과 지역으로 구분되는데, 변수는 선언된 위치에 의해 자신이 유효한 범위인 스코프가 결정된다. 전역에 선언된 변수는 전역 스코프를 갖는 전역 변수이고, 지역에 선언된 변수는 지역 스코프를 갖는 지역 변수이다.

구분설명스코프변수
전역코드의 가장 바깥 영역전역 스코프전역 변수
지역함수 몸체 내부지역 스코프지역 변수

전역 변수는 어디서든지 참조할 수 있다. 하지만, 지역 변수는 해당 지역 스코프와 하위 지역 스코프에서 유효하다.

var x = "global x";
var y = "global y";

function outer() {
  var z = "outer's local z";
  
  function inner() {
    var x = "inner's local x";
  }
  
  inner();
}

outer();

전역 변수가 어디서든지 참조 가능한 이유는 스코프 체인을 통해 변수를 검색할 수 있기 때문이다.
위 그림과 같이 모든 스코프는 하나의 계층적 구조로 연결되며, 모든 지역 스코프의 최상위 스코프는 전역 스코프다. 이처럼 스코프가 계층적으로 연결된 것을 스코프 체인scope chain이라고 한다.

변수를 참조할 때 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다.

스코프 체인은 물리적인 실체로 존재한다. 자바스크립트 엔진은 코드를 실행하기 전에 위 그림과 유사한 자료구조인 렉시컬 환경lexical environment를 실제로 생성한다. 변수 선언이 실행되면 식별자가 이 자료구조에 키key로 등록되고, 변수 할당이 일어나면 이 자료구조의 변수 식별자에 해당하는 값을 변경한다. 변수의 검색도 이 자료구조 상에서 일어난다.

렉시컬 스코프

프로그래밍 언어에서 스코프는 함수의 상위 스코프를 결정할 때 함수를 어디서 호출 또는 정의했는지에 따라 동적 스코프렉시컬 스코프로 나뉜다.

자바스크립트는 함수를 어디서 정의했는지에 따라 상위 함수를 결정하는 렉시컬 스코프를 따른다.

var x = 1;

function func1() {
  var x = 10;
  
  func2();
}

function func2() {
  console.log(x);
}
func1(); // 결과는 1

위 예제에서는 func1func2전역에 정의하고 func1을 호출한다. fun1안에서 x를 재선언하고 func2를 호출했다. func2x를 참조하지만 재선언된 x는 무시되고 전역에 선언된 x의 값이 출력된다.
이유는 func2전역에 정의되었기 때문에 func2의 상위 스코프는 전역 스코프가 되었기 때문이다. 그렇기에 func2 내부에는 없는 x를 스코프 체인을 통해 찾기 위해 상위 스코프인 전역 스코프x를 참조하여 콘솔에 출력하게 되는 것이다.

14장(전역 변수의 문제점) 요약

변수는 선언에 의해 생성되고 할당을 통해 값을 가지게 되며 변수가 선언된 스코프가 종료되면 변수 또한 소멸한다. 이 말은 전역 변수는 프로그램 종료가 되지 않는 한 영원히 메모리 공간을 점유하며 소멸하지 않는다는 말과 같다.

함수와 달리 전역 코드는 명시적인 호출 없이 실행된다. 즉, 함수 호출과 같은 진입점 없이 코드가 로드되자마자 곧바로 해석되고 실행된다. 함수는 몸체의 문이 종료되거나 반환문이 실행되면 종료되지만, 전역 코드는 반환문을 사용할 수 없으므로 전역의 마지막 코드가 실행되어 더 이상 실행할 문이 없을 때 종료된다.

var 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 된다. 이는 전역 변수의 생명 주기가 전역 객체의 생명 주기와 일치한다는 것을 말한다.

전역 객체
전역 객체는 코드가 실행되기 이전 단계에 자바스크립트 엔진에 의해 어떤 객체보다도 먼저 생성되는 특수한 객체다. 전역 객체는 클라이언트 환경(브라우저)에서는 window, 서버 사이드 환경(Node.js)에서는 global 객체를 의미한다. 환경에 따라 전역 객체를 가리키는 다양한 식별자(window, self, this, frames, global)가 존재했으나 ES11(ECMAScript 11)에서 globalThis로 통일되었다.

전역 객체는 표준 빌트인 객체(Object, String, Number, Function, Array, ...)와 환경에 따른 호스트 객체(클라이언트 Web API 또는 Node.js의 호스트 API), 그리고 var 키워드로 선언한 전역 변수와 전역 함수를 프로퍼티로 갖는다.

문제점

전역 변수를 사용하면 크게 4가지의 문제점들이 발생할 수 있다.

  • 암묵적 결합
  • 긴 생명 주기
  • 스코프 체인 상에서 종점에 존재
  • 네임스페이스 오염

암묵적 결합
전역 변수를 선언하는 것은 코드 어디에서나 전역 변수를 참조하고 변경할 수 있는 암묵적 결합을 허용하는 것이다. 변수의 유효 범위가 크면 클수록 코드의 가독성은 나빠지고 의도치 않게 상태가 변경될 수 있는 위험성도 높아진다.

긴 생명 주기
전역 변수는 생명 주기가 길다. 생명 주기가 긴 만큼 메모리 리소스도 오랜 시간 소비한다. 전역 변수의 상태를 변경할 수 있는 시간도 길고 기회도 많다.

스코프 체인 상에서 종점에 존재
변수를 검색할 때 전역 변수는 항상 가장 마지막에 검색된다. 즉, 전역 변수의 검색 속도가 가장 느리다. 검색 속도의 차이는 그다지 크지 않지만 속도의 차이는 분명히 있다.

네임스페이스 오염
자바스크립트의 가장 큰 문제점 중 하나는 파일이 분리되어 있다 해도 하나의 전역 스코프를 공유한다는 것이다. 따라서 다른 파일 내에서 동일한 이름으로 명명된 전역 변수나 전역 함수가 같은 스코프 내에 존재할 경우 예상치 못한 결과를 가져올 수 있다.

해결책

책에서는 전역 변수 사용을 억제하는 방법이 4가지 정도로 분류되어 기술되어 있다.

  • 즉시 실행 함수
  • 네임스페이스 객체
  • 모듈 패턴
  • ES6 모듈

즉시 실행 함수
함수 정의와 동시에 호출되는 즉시 실행 함수는 단 한 번만 호출된다. 모든 코드를 즉시 실행 함수로 감싸면 변수는 즉시 실행 함수의 지역 변수가 된다.
이 방법은 전역 변수를 생성하지 않아 라이브러리 등에 자주 사용된다.

네임스페이스 객체
전역에 네임스페이스 역할을 할 객체를 생성하고 전역 변수처럼 사용하고 싶은 변수를 프로퍼티로 추가하는 방법이다.

var NAME_SPACE = {
  "전역변수1": '안변하는값1",
  "전역변수2": '안변하는값2"
}

식별자 충돌을 방지하는 효과는 있으나 네임스페이스 객체 자체가 전역 변수에 할당되므로 그다지 유용하지 않다.

모듈 패턴
모듈 패턴은 클래스를 모방해서 관련이 있는 변수와 함수를 모아 즉시 실행 함수로 감싸 하나의 모듈을 만드는 것이다. 모듈 패턴의 특징은 전역 변수의 억제는 물론 캡슐화까지 구현할 수 있다는 것이다.

var Counter = (function() {
  var num = 0;
  
  // 외부에 공개할 데이터나 메서드를 프로퍼티로 추가한 객체를 반환한다.
  return {
    increase() {
      return ++num;
    },
    decrease() {
      return --num;
    }
  };
}());

// private 변수는 노출되지 않는다.
console.log(Counter.num); // undefined

console.log(Counter.increase()); // 1
console.log(Counter.increase()); // 2

ES6 모듈
ES6 모듈을 사용하면 더는 전역 변수를 사용할 수 없다. ES6 모듈은 파일 자체의 독자적인 모듈 스코프를 제공하기 때문에 모듈 내에서 var 키워드로 변수를 선언해도 전역 변수가 아니고 전역 객체의 프로퍼티가 되지도 않는다.

5-6주차 스터디 회고

저번 주를 건너뛰게 되면서 양을 늘렸더니.. 정리할 내용도 많아져서 회고가 조금 늦어졌다. 함수 파트가 생각보다 양도 많고 난이도도 예전 파트에 비해 높았던 부분이어서 이번 스터디에서는 함수 관련한 이야기만 계속 했던 것 같다. 추상화의 개념에 대해서 간단하게나마 토론하는 시간도 가졌고, 개인 프로젝트를 시작하자는 말이 나와서 개인 프로젝트의 기술 선정에 대해서도 토론했다.
3시간을 정말 알차게 써서 좋았는데 프로젝트를 할 생각에 신나서 얘기하다가 다음 주 내용을 미리 같이 훑어보지 못해서 조금 아쉬웠다.

profile
기술부채상환중...

0개의 댓글