본문 바로가기
👩‍💻 Programming/JavaScript

코드 모듈화 기술(modularization techniques) from Secrets of the JavaScript Ninja

by codingBear 2022. 5. 13.
728x90
반응형

이번 글은 'Secrets of the JavaScript Ninja'의 'Chapter 11. Code modularization techniques'를 바탕으로 작성하였습니다.


핵심 Concepts

  • 모듈 패턴 활용하기
  • 모듈러 코드 작성을 위한 현재 기준 활용하기: AMD 및 CommonJS
  • ES6 모듈로 작업하기

사전 지식 체크!

  • ES6 이전 JavaScript에서 모듈을 따라하기 위해 쓰이는 기술은 무엇인가?
  • AMD와 CommonJS 모듈의 사양 차이점은 무엇인가?
  • ES6를 사용하여 guineaPig라는 다른 모듈 내에서 test라는 모듈의 tryThisOut() 함수를 사용하는 데 필요한 두 가지 명령문은 무엇인가?

들어가며

 프로그램의 구조 및 조직을 더욱 향상시키는 방법 중 하나는 해당 프로그램을 더 작고 더 느슨하게 연결된 조각으로 나누는 모듈화 작업을 하는 것이다. 프로그램을 모듈화함으로써 프로그램의 다른 부분에서 모듈화된 부분을 용이하게 재사용 가능해지는 것이다. 

 JavaScript는 전역 변수에 큰 영향을 끼친다. 코드의 주요 부분에 변수를 정의할 때마다 변수는 저절로 전역으로 인식되고 코드의 다른 어느 부분에서든 액세스 가능하다. 이는 프로그램 규모가 작을 경우에는 문제 되지 않지만 프로그램 규모가 커지면서 제3자(third-party) 코드를 추가하거나 하면 변수명이 충돌할 위험성이 생긴다. 다른 언어에서는 네임스페이스(namespaces, C++&C#)로 해결하거나 패키지(packages, Java)로 이러한 문제를 해결한다.

 ES6에서 자체적으로 모듈을 제공하기 이전까지는 JavaScript에 모듈 기능이 없었으므로 이러한 문제를 해결하려면 객체, 즉시 실행 함수(immediate function), 클로저의 특장점들을 활용해야 했다.


Modularizing code in pre-ES6 JavaScript

 ES6 이전의 JavaScript에는 전역 스코프 및 함수 스코프 단 두 가지 형식의 스코프만이 존재했다. 따라서 JavaScript 개발자들은 모듈러 코드를 작성하려면 JavaScript의 언어적 특징을 활용해야 했다. 이러한 특징을 활용하는 데는 다음과 같은 최소 2가지 기능을 구현해야 한다.

 

  • 인터페이스(interface) 정의: 이를 통해 모듈이 제공하는 기능에 액세스 가능.
  • 모듈 내부 숨기기: 이로써 모듈 사용자는 중요하지 않은 구현 세부사항에 대한 부담을 가지지 않는다. 또한 외부 접근에 의한 원치 않는 수정을 막을 수 있다.

 

 이번 글에서는 우선 JavaScript의 객체, 클로저, 즉시 호출 함수 등의 특징을 활용하여 모듈을 만드는 방법과 비동기적 모듈 정의(Asynchronous Module Definition, AMD)와 CommonJS에 대해 순차적으로 살펴보겠다.


Using objects, closures, and immediate functions to specify modules

앞서 언급했듯 JavaScript로 모듈을 작성할 땐 구현 세부 정보 숨기기(hiding implementation details) 및 모듈 인터페이스 정의(defining module interfaces)라는 두 가지 최소 요구 사항이 있다. 그렇다면 JavaScript의 어떠한 언어적 특성을 활용하여 이 두 가지 최소 요구 사항을 충족시킬 수 있을지 생각해보자.

 

  • 구현 세부 정보 숨기기: 알다시피 JavaScript 함수를 호출하면 현재 함수에서만 액세스 가능한 변수를 정의하는 새로운 스코프가 생성된다. 따라서 모듈 내부를 숨기는 하나의 방법은 함수를 모듈처럼 사용하는 것이다. 이 방법을 쓰면 모든 함수 변수는 내부 모듈 변수가 된다.
  • 모듈 인터페이스 정의: 모듈이 다른 코드에서 사용되는 경우 모듈에서 제공하는 기능을 꺼낼 수 있는 명확한 인터페이스를 정의해야 한다. 이를 위해 객체 및 클로저의 특성을 활용하면 된다. 기본 원리는 함수 모듈에서 모듈의 공용 인터페이스를 나타내는 객체를 반환한다는 것이다. 

 

Functions as modules

 다음은 웹 페이지를 클릭한 횟수를 세는 코드이다.

 

 

 위 예제 코드에서 변수 numClicks를 생성하고 전체 document에 클릭 이벤트 핸들러를 등록할 countClicks 함수를 생성한다. 클릭이 일어날 때마다 변수 numClicks의 값은 증가하고 결과값이 alert 상자에 반환된다. 여기서 두 가지 중요한 점을 발견할 수 있다.

 

  •  countClicks 함수 내부의 numClicks 변수는 클릭 핸들러 함수의 클로저를 통해 활성화되어 있다. 변수는 오로지 핸들러 안에서만 참조된다. numClicks 변수를 countClicks 함수 외부로부터 지킴과 동시에 프로그램의 전역 네임스페이스를 변수로 오염시키지 않는다.
  • countClicks 함수는 이 한 곳에서만 호출되므로 함수를 정의한 다음 별도의 명령문에서 호출하는 대신 즉시 실행 함수를 사용하여 countClicks 함수를 정의하고 호출한다. 

 

 내부 함수 (혹은 모듈) 변수가 클로저를 통해 어떻게 활성화 상태를 유지하는지는 아래 그림에 잘 나와 있다.

 

 

The module pattern: augmenting functions as modules with objects as interfaces

 

 모듈 인터페이스는 일반적으로 모듈이 외부에 제공하는 일련의 변수와 기능으로 구성된다. 인터페이스를 만드는 가장 간단한 방법은 JavaScript 객체를 활용하는 것이다.

 

 

 위 예제를 보면 모듈을 실행하기 위해 즉시 실행 함수를 사용했다. 즉시 실행 함수 안에는 내부 모듈 구현 세부 정보를 정의한다.  numClicks라는 지역 변수 및 handleClick이라는 지역 함수를 정의하는데 이는 오로지 모듈 내에서만 액세스 가능하다. 그런 다음 모듈의 공용 인터페이스(public interface)로 쓰이는 객체를 반환한다. 이 인터페이스는 모듈 기능에 액세스하기 위해 모듈 외부에서 사용할 수 이씨는 countClick 메서드를 포함한다.

 동시에 모듈 인터페이스를 내보내기 때문에 인터페이스에 의해 생성되는 클로저를 통해 내부 모듈 세부 사항은 활성화 상태를 유지한다. 인터페이스의 countClicks 메서드는 내부 모듈 변수 numClicks와 handleClick를 활성화 상태로 유지한다. 다음 그림에 이 같은 상태가 잘 나타나 있다.

 

 

 마지막으로 즉시 실행 함수에서 반환된 모듈 인터페이스를 나타내는 객체를 MouseCOunterModule이라는 변수에 저장한다. 이를 통해 다음 코드를 작성하여 모듈 기능을 쉽게 사용할 수 있다. 

 

 

 즉시 실행 함수의 특징을 활용하여 특정한 모듈 구현 세부 사항을 숨길 수 있다. 그리고 객체 및 클로저를 함께 추가함으로써 모듈에 의해 제공된 기능을 외부로 내보내는 모듈 인터페이스 명시할 수 있다.

 이렇게 JavaScript에서 모듈을 만들기 위해 즉시 실행 함수, 객체 및 클로저를 활용하는 방식을 모듈 패턴이라고 부른다. 

 이렇듯 코드를 잘게 나누어 모듈화하면 유지보수도 수월할 뿐만 아니라 소스 코드를 수정하지 않고도 기능을 추가할 수 있다.

 

Augmenting modules

 앞선 예제를 수정하지 않고 마우스 스크롤에 따라 카운팅 횟수가 증가하는 기능을 추가해보자.

 

 

 모듈을 늘릴 때는 새로운 모듈을 만들었던 방식과 비슷한 절차를 따른다. 함수를 호출하는 것은 똑같은데 확장하려는 모듈을 인수로 전달한다는 점이 다르다.

 함수 내에 스크롤 횟수를 세는 데 필요한 모든 프라이빗 변수 및 함수를 생성한다. 즉 다른 객체를 확장하는 것처럼 직접 함수의 모듈 매개 변수를 통해 사용할 수 있는 모듈을 확장하는 것이다. 

 이로써 공용 모듈 인터페이스는 두 개의 메서드를 갖고 아래와 같이 모듈을 쓸 수 있다.

 

 

 앞서 언급했다시피 모듈을 확장하는 즉시 호출되는 함수를 통해 새로운 모듈을 생성하는 것과 비슷한 방식으로 모듈을 확장했다. 이는 클로저와 관련한 몇 가지 부수 효과 중 하나이다. 아래 그림을 통해 모듈을 확장하고 난 이후의 애플리케이션 상태를 살펴보자.

 

 

 위 그림을 자세히 살펴보면 모듈 패턴의 단점 한 가지를 발견할 것이다. 확장된 모듈들과 프라이빗 모듈 변수들을 공유할 수 없다는 점이다. 예를 들어 countClicks 함수는 numClicks와 handleClick 변수 주변에 클로저를 유지해서 이러한 프라이빗 모듈 내부에는 countClicks 메서드를 통해서만 액세스할 수 있는 것이다.

 안타깝게도 countScrolls 함수는 완전히 새로운 프라이빗 변수인 numScrolls 및 handleScroll과 함께 별도의 스코프에서 생성된다. countScrolls 함수는 클로저를 numScrolls 및 handleScroll 변수 주변에만 생성하기 때문에 numClicks와 handleClick 변수에 액세스할 수 없는 것이다. 

 모듈 패턴에서 모듈은 객체여서 새로운 프로퍼티로 모듈 객체를 확장하는 방식 등 적절한 방식으로 새로운 기능을 추가할 수 있다.

 

 

 불행히도 모듈 패턴에는 또 다른 문제가 더 있다. 바로 해당 모듈의 기능을 다른 모듈에 의존한다는 점이다. 이 같은 문제는 애플리케이션의 규모가 커질수록 문제가 된다. 이러한 문제를 다루기 위해 비동기적 모듈 정의(AMD) 및 CommonJS라는 기준이 생겨났다.


Modularizing JavaScript applications with AMD and CommonJS

 AMD와 CommonJS는 JavaScript 모듈을 정의하는 데 쓰이는 모듈 정의 표준이다. 둘의 주된 차이점은 AMD의 경우 브라우저를 염두에 두고 설계되었고 CommonJS는 브라우저에 국한되지 않고 Node.js와 같은 JavaScript 환경에서 일반적으로 쓰일 목적으로 설계되었다는 점이다. 

 

AMD

 AMD는 아래와 같은 기능을 가진 define이라는 함수를 제공한다.

  • 새롭게 생성된 모듈의 ID
  • 현재 모듈이 의존하는 모듈 ID의 목록
  • 모듈을 초기 설정하고 요구된 모듈을 인자로 받는factory 함수

 

 

 위 예제를 보면 JQuery에 의존하는 ID MouseCounterModule을 갖는 모듈을 생성하기 위한 define 함수를 선언했다. 이 같은 의존성 때문에 AMD는 먼저 jQuery 모듈을 요청하며 서버에서 파일을 요청해야 하는 경우 시간이 걸릴 수 있다. 이때 작업은 비동기적으로 이루어진다. 모든 의존성이 다운로드 및 평가되고 난 후 모듈 factory 함수는 요청된 각 모듈에 대한 하나의 인자와 함께 호출된다. 위 예제의 경우 새로운 모듈이 jQuery만을 요구하므로 인자는 단 하나이다. factory 함수 안에서 표준 모듈 패턴이 그러하듯 모듈의 퍼블릭 인터페이스를 노출하는 객체를 반환함으로써 모듈을 생성한다. 

 AMD를 사용할 때 이점은 다음과 같다.

 

  • 의존성을 자동으로 해결하여 모듈을 포함하는 순서에 대해 고려할 필요가 없다.
  • 모듈을 비동기적으로 불러온다.
  • 하나의 파일에 모듈을 여러 개 정의할 수 있다.

 

CommonJS

 CommonJS는 파일 기반 모듈을 사용하여 파일당 하나의 모듈을 정의할 수 있다. 각 모듈에 CommonJS는 추가 속성으로 쉽게 확장할 수 있는 속성 내보내기와 함께 변수 모듈을 노출한다. module.exports의 내용은 모듈의 퍼블릭 인터페이스로 노출된다.

 애플리케이션의 다른 부분의 모듈을 사용하려면 요구를 하면 된다. 파일은 동기적으로 불러와져 해당 퍼블릭 인터페이스에 대한 액세스를 가진다. 이것은 CommonJS가 원격 서버에서 모듈을 다운로드 받아야만 하고 동기적 로딩이 차단을 의미하는 클라이언트보다 파일 시스템 읽기만이 필요하기에 모듈 가져오는 속도가 상대적으로 빠른 서버에서 더 인기 있는 이유이다. 

 이번엔 CommonJS를 활용한 예를 살펴보겠다.

 

 

 다른 파일에 위에서 작성한 모듈을 불러오려면 아래와 같이 작성하면 된다.

 

 

 CommonJS의 철학은 파일당 하나의 모듈만 정의한다는 것이다. 따라서 즉시 실행 함수로 변수들을 감싸줄 필요가 없다. 모듈 안에 정의된 모든 변수들은 현재 모듈의 스코프 안에 안전하게 보관되어 전역 스코프로 새어나가지 않는다. 위 예제의 경우 $, numClicks, handleClick이 해당된다. 여기서 염두에 두어야 할 부분은 module.exports 객체를 통해 노출된 변수와 함수만이 모듈 밖에서 접근 가능하다는 점이다. 

 CommonJS는 다음과 같은 장점이 있다.

 

  • 문법이 간단하다. 
  • Node.js의 기본 모듈 형식이어서 node의 패키지 매니저인 npm 활용 가능

 

 반면 CommonJS의 큰 단점은 브라우저를 염두에 두고 설계되지 않았다는 점이다. 브라우저 내의 JavaScript에서는 module 변수 및 export 프로퍼티를 제공하지 않는다. 따라서 CommonJS 모듈을 Browserify나 RequireJS와 같은 패키지를 활용해 브라우저가 읽을 수 있는 형태로 변경해야 한다. 


ES6 modules

 ES6는 CommonJS의 간단한 문법 및 AMD의 비동기적으로 모듈을 불러온다는 장점을 합하여 만들어졌다. ES6 모듈의 주요 개념은 모듈에서 명시적으로 내보낸 식별자만 해당 모듈의 외부에서 접근 가능하다는 것이다. 식별자를 내보낼 때는 export, 삽입할 때는 import 키워드를 사용한다.


Exporting and importing functionality

 하나의 모듈에서 기능을 내보내어 다른 곳에 어떻게 삽입하는지 예를 통해 살펴보자.

 

 

 위 예제에서는 우선 이 모듈 안에서만 접근할 수 있는 모듈 변수 ninja를 정의한다. 다음 export 키워드를 사용하여 모듈 밖에서도 접근 가능한 message 변수를 정의하고 sayHiToNinja 함수를 만들어 내보낸다. 

 이게 ES6 모듈의 전부이다. 앞선 AMD 및 CommonJS와 비교해서 코드 작성이 매우 간단한 것을 알 수 있다. 

 내보낸 기능을 어떻게 삽입하는지 살펴보기에 앞서 아래 예와 같이 내보내고 싶은 모든 기능을 어떻게 처리하면 되는지 예제를 살펴보자.

 

 

 위 처럼 중괄호({})로 필요한 만큼 기능들을 묶어서 내보낼 수 있다.

 특정 모듈에서 어떤 시긍로 식별자를 내보냈는지 상관없이 import 키워드를 쓰면 내보낸 기능들을 삽입할 수 있다.

 

 

 ninja 모듈에서 변수 message 및 함수 sayHiToNinja를 삽입하기 위해 import 키워드를 사용했다. 여기서 기억해야 할 점은 내보내지 않은 변수와 가져오지 않은 변수에 액세스할 수 없다는 점이다. 위 예제의 경우 ninja 변수는 활용할 수 없다는 것이다. 

 위 예제에서는 모듈로부터 가져와 파일에서 활용할 변수들을 중괄호({})로 감쌌는데 이런 식으로 일일이 사용할 변수를 나열하는 것은 번거롭다. 따라서 아래 예제와 같이 축약어를 사용할 수 있다.

 

 

 위 예제 코드와 같이 import *을 식별자와 함께 사용하면 모듈에서 내보낸 모든 변수에 대해 ninjaModule로써 접근 가능하다. 하지만 내보내지 않은 ninja 변수는 여전히 활용 불가능하다.

 

Default Exports

 때때로 모듈에서 관련된 식별자 모음을 내보내지 않는 대신 하나의 내보내기를 통해 전체 모듈을 나타낼 때가 있다. 이러한 일반적인 상황 중 하나는 모듈에 단일 클래스가 포함되어 있을 때이다. 예제를 통해 자세히 살펴보자.

 

 

 위 예제에서 export 키워드 뒤에 default 키워드를 붙여 이 모듈에 대한 기본 바인딩을 설정한다. 위 예제의 경우 모듈에 대한 기본 바인딩은 Ninja 클래스이다. 기본 바인딩을 지정했으나 compareNinjas 함수에다 했듯 명명된 내보내기를 사용하여 추가적인 식별자를 내보낼 수 있다. 

 이제 아래 예제에서 보듯 단순화한 삽입 문법을 사용할 수 있다.

 

 

 우선 default export를 삽입하면서 시작한다. default export의 경우 중괄호를 빼고 작성하면 된다. 또한 default export에 임의의 이름을 부여할 수 있다. 즉 위 예제의 ImportedNinja는 Ninja.js에서 정의된 Ninja 클래스를 참조한다.

 위의 importing을 더 간단히 작성하면 아래와 같다.

 

 

 

 

 

 위 예제에서 함수 sayHi를 sayHello로 변경하여 내보냈으므로 삽입 시에는 sayHello라고 삽입해야 한다. 또한 두 번째에서 보듯 삽입 시에 변수명을 바꿔 활용할 수도 있다. 

 아래는 ES6 모듈 문법을 정리한 표이다.

 


요약 정리

  • 규모가 크고 단일한 모듈은 관리 및 유지 보수가 어렵기 때문에 프로그램의 구조와 조직을 개선하기 위해서 작고 느슨하게 연결된 부분 및 모듈로 나눌 필요가 있다. 
  • 코드를 모듈화하면 가독성이 좋아지고, 유지 보수성이 향상되며 재사용성이 증가한다.
  • ES6 이전의 JavaScript에서는 모듈이 내장되어 있지 않아서 즉시 실행 함수 및 클로저를 활용하여 모듈 기능을 구현했다.
    • 즉시 실행 함수는 해당 스코프의 바깥에서 보이지 않는 모듈 변수를 정의하기 위한 새로운 스코프를 생성한다.
    • 클로저는 모듈 변수들을 활성화 상태로 유지한다.
    • 가장 널리 쓰이는 패턴은 모듈의 퍼블릭 인터페이스를 나타내는 새로운 객체를 반환하는 즉시 실행 함수를 통합하는​ 모듈 패턴이다.
  • 유명한 모듈 표준이 두 개 존재한다
    • AMD(Asynchronous Module Definition): 브라우저에서 모듈을 사용 가능하게 함. 자동적으로 의존성을 해결하고 모듈이 비동기적으로 불러와짐.
    • CommonJS: 문법이 간단함. 동기적으로 모듈을 불러옴.(서버에 적합함.) npm을 활용하여 패키지가 많음.
  • ES6 모듈은 CommonJS와 AMD의 간단한 문법과 비동기적 모듈 불러오기라는 장점을 합쳐 놓음.
    • ES6 모듈은 파일 기반으로 파일당 하나의 모듈이 존재
    • export 키워드를 사용하여 식별자를 내보냄
    • import 키워드를 사용하여 식별자를 삽임함
    • 모듈은 단일 default 내보내기를 가지며 단일 내보내기를 통해 전체 모듈을 나타내려는 경우 사용함
    • as 키워드로 imports와 exports 이름 변경 가능
728x90
반응형

댓글