자바스크립트 클로저, 모듈 정리 - 기초부터 완성까지, 프런트엔드 5장

khakiD·2022년 7월 12일
0

5장 목차

  1. 프로토타입
  2. 스코프
  3. 호이스팅
  4. 클로저 ⏪
  5. 모듈 ⏪

5.4. 클로저 (closure)

함수의 렉시컬 스코프를 기억하여 함수가 렉시컬 스코프를 벗어난 외부 스코프에서 실행될 때에도 자신의 렉시컬 스코프에 접근할 수 있게 해주는 것

  • 자바스크립트에서 굉장히 중요한 개념
  • 함수를 사용하는 곳이라면 어디서든 적용할 수 있는 개념
  • 특정한 상태를 기억하고 캡슐화하거나 하나의 모듈을 정의하는 패턴으로 확장 가능
  • 스코프 파트에서 배운 렉시컬 스코프와 스코프 체인을 이해하면 쉽게 사용 가능
function foo() {
	var a = 1;
	function bar() {
		console.log(a); // 1
	}
	bar();
}
foo();

위 코드의 bar() 함수는 상위 스코프인 foo() 함수 내에서 실행되며, 렉시컬 스코프 체인을 통해 foo() 함수의 스코프를 기억하고 있다. 하지만 이런 경우를 일반적으로 스코프라고 하지 않는다.

내부 함수가 자신을 감싸고 있는 외부 함수를 벗어나 완전히 독립적인 스코프에서 실행되었을 경우에 우리는 클로저라고 부른다.


//클로저를 이용한 코드
function foo() {
	var a = 1;
	function bar() {
		console.log(a); // 1
	}
	return bar;
}
const baz = foo();
baz(); // 1

📌 위 클로저를 이용한 코드는 아래와 같은 순서대로 실행된다.

  1. bar() 함수는 렉시컬 스코프 체인을 통해 foo() 함수의 스코프를 기억
  2. bar() 함수를 전역 변수 baz에 할당
  3. 전역 변수 baz를 사용하여 bar() 함수를 호출
  4. bar() 함수는 자신의 스코프에서 변수 a를 검색
  5. 자신의 스코프에서 찾을 수 없으므로 스코프 체인을 통해 foo() 함수의 스코프에서 검색
  6. foo() 함수의 스코프에서 변수 a를 찾아 1을 출력


📌 중요한 것은 bar() 함수가 자신을 감싸고 있는 foo() 함수를 벗어나 bar() 함수의 스코프와 상관없는 전역 스코프에서 실행된다는 점이다.

  • bar() 함수를 실행했을 때 자신의 렉시컬 스코프 체인을 통해 foo() 함수의 스코프에서 변수 a를 찾았다.
  • 전역 스코프가 아닌 어느 곳에서 호출되어도 bar() 함수는 기억한 렉시컬 스코프 체인을 통해 변수 a를 찾을 수 있다.
  • 클로저를 사용하면 외부에서도 얼마든지 원래의 렉시컬 스코프에 접근할 수 있다.
function foo() {
	var a = 1;
	function bar() {
		console.log(a); // 1
	}
	return bar;
}

function baz() {
	const fn = foo();
	fn(); // 1
}
baz();

5.4.1. 클로저의 모듈 패턴

클로저를 활용하면 모듈을 정의하여 원하는 프로퍼티나 메소드를 캡슐화할 수 있다. ES2015 이전에 자바스크립트에는 모듈이라는 개념이 없었으므로 클로저를 사용하여 모듈을 정의하여 사용했다.

클로저를 모듈로 생성하지 않고 전역 스코프에 필요한 값들을 정의해도 괜찮다. 하지만 전역 변수에 정의된 값이 많아지면 사용하는 전역 변수가 어디서 선언되었는지 찾기 어렵고 다른 라이브러리와 변수명이 충돌할 수 있는 위험이 생긴다. 최악의 경우 어딘가에서 전역 변수의 값이 덮어씌워져 앱이 제대로 동작하지 않을 수 있다. 전역 스코프를 오염시키지 않는 것이 좋다.

⇒ ES2015 이후 모듈이라는 개념이 생겼으므로, 클로저를 더 자세히 이해하기 위해서 공부한다고 생각하자.

function myModule() {
    let counter = 0;

    function increment() {
        counter += 1;
    }

    function decrement() {
        counter -= 1;
    }

    function getCount() {
        return counter;
    }

    return {
        increment,
        decrement,
        getCount
    }
}

const myCounter = myModule();

myCounter.increment();
console.log(myCounter.getCount()); // 1
myCounter.decrement();
console.log(myCounter.getCount()); // 0

myModule() 함수는 increment, decrement, getValue 함수들을 객체로 만들어 실행 결과로 반환하고, 반한된 함수들은 기억한 렉시컬 스코프 체인에 의해 myModule() 함수 스코프에 접근할 수 있다. 세 함수를 사용하면 외부 스코프에서도 myModule() 함수 내부의 counter 변수에 접근하여 값을 변경하거나 참조하는 것이 가능하다.


📌 중요한 점은 myModule() 함수가 반환한 객체는 함수들에 대한 참조만 가지며 내부 변수 counter에 대한 접근은 불가능하다는 점

즉, counter 변수는 캡슐화되어 외부에서 접근할 수 없으며, 접근하고 싶다면 외부로 반환한 클로저 함수를 통해서만 접근할 수 있다.
이것이 클로저를 활용한 모듈 패턴이다.

ES2015 이후 모듈 명세가 등장하여 현재는 클로저를 사용하여 모듈 패턴을 구현하지 않는다. 하지만 IE와 같은 레거시 브라우저는 이를 지원하지 않아 레거시 브라우저 지원을 위해서 이러한 패턴을 이해하고 사용할 수는 있어야 한다. 또한 모듈 패턴은 클로저를 활용하는 좋은 예시이므로 원리에 대해 꼭 숙지하도록 하자.


5.4.1.2. 즉시 실행 함수 표현식

즉시 실행 함수 표현식(IIFE, Immediately Invoked Function Expression)은 정의되자마자 실행되는 함수이며, 익명 함수를 응용한 패턴이다. 익명 함수를 괄호로 둘러싼 형태로 정의한다.

(function (lang) {
	// ...
})('javascript');

즉시 실행 함수를 모듈 패턴과 함께 사용하면 전역 스코프를 오염시키지 않고 모듈 객체를 만들 수 있다. 아래의 예제를 확인하자.

const cleanModule = (function() {
    let counter = 0;

    function increment() {
        counter += 1;
    }

    function decrement() {
        counter -= 1;
    }

    function getCount() {
        return counter;
    }

    return {
        increment,
        decrement,
        getCount
    }
})();

이전 코드 예제의 myModule() 함수를 즉시 실행 함수로 변경하여 바로 실행하였다. 실행 결과로 반환된 모듈 객체를 cleanModule 변수에 할당하여 즉시 실행 함수와 모듈 패턴을 함께 사용하였다. 이러면 전역 스코프를 오염시키지 않고 모듈 함수를 선언하여 객체를 생성할 수 있다.


5.5. 모듈 (module)

  • 모듈은 외부로 공개한 API를 통해 상태를 변경하고 내부 구현에 대한 캡슐화의 역할
  • 각 모듈은 애플리케이션의 구성 단위
  • 모듈 단위로 재사용하여 불필요한 코드를 줄여 유지보수성을 높임

✨ 자바스크립트 모듈의 진화
자바스크립트에서는 파일마다 독립적인 스코프를 갖는 것이 아니라 전역 스코프를 공유하기에 다른 파일의 변수와 중복되거나 값을 덮어쓰는 문제가 발생하여 클로저를 활용한 다양한 모듈 패턴으로 사용해왔다. CommonJS와 AMD라는 모듈 관련 독자적 명세가 제안되었으나 별도 구현 라이브러리를 사용해야하고 방법이 달라 포맷이 통일되지 않았다.

그런데.. 짜잔~ ES2015부터 정식 모듈 문법이 등장!

  • ES 모듈은 각 파일을 독립적 스코프로 처리
  • 내보내기를 한 식별자가 아니라면 해당 모듈 내에서만 접근 (외부 모듈 접근 불가)
  • 더 이상 전역 스코프를 걱정하며 개발할 필요가 사라짐
  • 모듈은 다른 모듈에 정의된 특정 변수나 함수를 불러올 수 있고, 반대로 자신의 모듈 안에 정의된 식별자들을 내보내기 가능
  • IE와 같은 레거시 브라우저에서는 지원하지 않음 -> Webpack, Babel 사용하여 해결

5.5.1. export, import

모듈 안에서 선언한 식별자를 다른 모듈에서 접근할 수 있도록 export 키워드로 내보내기
📌 이는 반드시 모듈의 최상위 위치(top-level)에 존재해야 한다.

export const a = 1;
export function foo() {
  //... 
}

const b = 1;
function bar() { 
  //... 
}

export { b, bar };

function bax() {
  export let c = 'oh'; // Uncaught SyntaxError: Unexpected token 'export'
}

변수 afoo 함수처럼 개별적으로 내보낼 수도 있고, 변수 bbar 함수처럼 객체 형태로 한번에 내보낼 수 있다. bax 함수의 c와 같이 모듈의 최상위 위치가 아닌 곳에서는 내보내기할 수 없다.

📌 export 키워드를 사용하여 개별 식별자를 내보내는 것을 named exports라고 한다.

다른 모듈에서 내보낸 식별자들은 import 키워드로 가져올 수 있으며, 반드시 모듈의 최상단에 위치해야 한다. named exports로 내보낸 식별자들은 import 키워드와 중괄호로 감싸 가져올 수 있다.

/* a.js */
export const a = 1;
export function foo() { 
  //...
}
/* b.js */
import { a, foo } from './a.js';
console.log(a); // 1

모듈이 내보낸 식별자들을 하나의 이름으로 한 번에 가져오는 방법

import * as all from './a.js';
console.log(all.a); // 1

as 키워드 뒤에 지정한 객체의 프로퍼티로 할당된다. all 이라는 이름의 객체의 프로퍼티로 할당하여 가져왔다.

5.5.2. default export

모듈을 내보내는 다른 방식인 default export는 named exports와 다르게 모듈에서 하나만 정의할 수 있다.
export 키워드와 default 키워드를 함께 사용하여 식별자를 내보낸다.

/* a.js */
function foo() {
  // ...
}
export default foo;

default export로 내보낸 모듈을 다른 모듈에서 사용할 경우에는 중괄호 없이 임의의 이름으로 가져와서 사용한다.

/* b.js */
import anyName from './a.js';

여러 식별자를 내보낼 때는 named exports를 사용하는 것이 좋다. named exports를 사용하면 가져오는 측에서 정적 타입 체크나 IDE 자동 완성 기능과 같은 이점을 누릴 수 있어 오타나 잘못된 식별자에 접근하는 문제를 방지할 수 있다.

// a.js
export const a = 1;
export const b = 2;
export default { a, b };

// b.js
import { a, b } from '.a.js';
import defaultModuleA from './a.js';

// named exports로 내보낸 모듈은 명확하게 체크하여 가져울 수 있다.
console.log(a, b);

// default export로 내보낼 경우 가져올 때 잘못된 식별자에 접근하는 문제 발생 가능성이 있다.
console.log(defaultModuleA.a, defaultModuleA.c); // 1, undefined

5.5.3.식별자 충돌 피하기

여러 모듈에서 필요한 식별자를 가져오는 경우 이름 충돌 가능성이 있다. 이런 경우 as 키워드를 사용하여 가져오는 식별자의 이름을 변경할 수 있다. 하지만 내보낼 때도 식별자의 이름을 변경하여 해결할 수 있다.

// a.js
export const a = 1;
export const b = 1;

// b.js
const a = 2;
const b = 2;

export { a as a1, b as b1 };

// c.js
import { a } from './a.js';
import { a1 } from './b.js';

5.5.4. 〈script type="moudle"〉

브라우저 환경에서 모듈을 사용하기 위해서는 script 태그에 type="module" 속성을 설정하여 모듈임을 명시할 수 있다. 이런 파일은 모듈로 인식되어 전역 스코프를 공유하는 것이 아니라 모듈 스코프로 동작한다. 그렇기에 내보낸 식별자가 아니면 외부에서 모듈 내 식별자에 접근할 수 없게 된다.

<!DOCTYPE html>
<html>
  <body>
    <script type="module" src="a.mjs"></script>
    <script type="module" src="b.mjs"></script>
  </body>
</html>

📌 type="module" 속성을 명시하지 않으면 모든 코드가 전역 스코프로 동작하므로 반드시 추가해야 한다.

'.mjs' 확장자는 해당 자바스크립트 파일이 모듈임을 명시하기 위해 사용하는 확장자이다.

profile
(이해 못했음) (개인 블로그로 이전)

0개의 댓글