Module 패턴

코드베이스가 커져갈 수록 코드들을 유지보수하기 좋게 쪼개는 것이 중요해진다. 모듈 패턴이 이 때 코드들을 재사용 가능하면서도 작게 나눌 수 있게 해 준다.

또 모듈을 코드를 나누는 과정에 특정 변수들을 파일 내에 private 하게 할 수 있는데. 모듈 스코프 내에 변수를 선언하고 명시적으로 외부에 export 하지 않으면 바깥에서 해당 변수에 접근할 수 없다. 이를 통해 전역 스코프의 변수들과 이름이 충돌하는 문제를 줄일 수 있다.


ES2015 모듈

ES2015에는 자바스크립트의 빌트인 모듈 기능이 추가되었다. 모듈은 자바스크립트 코드를 포함한 파일이며 일반적인 스크립트와 동작이 약간 다르다.

아래 예제를 확인해 보자. math.js 모듈은 몇가지 계산 함수를 가지고 있다.

전달된 인자를 더하고, 곱하고, 빼고, 제곱하는 함수를 구현한 math.js 가 있다.

이 함수들을 math.js 에서 사용하지 않고 index.js 에서 사용하고 싶다. 함수들을 index.js 에서 직접 사용하려고 하면 당연하게도 함수가 존재하지 않는다는 예외가 발생한다.

함수들을 math.js 외에 다른 파일에서도 사용할 수 있게 하려면 먼저 각 함수들을 export 해야 한다. 그러기 위해서 export 키워드를 사용한다. 이는 named export 라 하며 파일 외부에서 사용하기 원하는 것 앞에 export 키워드를 붙이면 된다. 예제에서는 각 계산 함수의 앞에 붙여준다. index.js 는 이제 이 함수들을 사용할 수 있게 되었다.

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

export function multiply(x) {
  return x * 2
}

export function subtract(x, y) {
  return x - y
}

export function square(x) {
  return x * x
}

이렇게 add, multiply, subtract, square 함수를 export할 수 있게 만들었다. 하지만 외부에서 사용할 수 있도록 export 하는 것 만으로 문제가 해결되지는 않는다. export된 값들을 사용하기 위해서는 쓰는 쪽에서 명시적으로 import해 주어야 한다.

index.js 파일의 맨 위에서 import 구문을 사용하여 함수들을 import해야 한다. 어떤 모듈로부터 기능들을 import하는지 알리기 위해 from 키워드 뒤에 해당 모듈의 상대 경로를 입력한다.

import { add, multiply, subtract, square } from './math.js'

이렇게 index.js 에서 math.js 모듈의 함수들을 import하여 사용할 수 있게 되었다. 아래 예제를 통해 동작을 확인해 보자.

이전에 발생했던 참조 예외는 더 이상 발생하지 않고. export된 값들을 사용할 수 있게 되었다.

모듈의 장점은 명시적으로 export한 값들만 외부에 노출된다는 것이다. 명시적으로 export하지 않으면 모듈 내에서만 사용할 수 있다.

아래는 math.js 모듈 내에서만 사용할 수 있는 변수 privateValue 를 사용하는 예제이다.

const privateValue = 'This is a value private to the module!'

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

export function multiply(x) {
  return x * 2
}

export function subtract(x, y) {
  return x - y
}

export function square(x) {
  return x * x
}

코드에서 보이는 것 처럼 privateValue에는 export키워드를 사용하지 않았다. 따라서 math.js 외부에서는 해당 변수에 접근할 수 없다.

import { add, multiply, subtract, square } from './math.js'

console.log(privateValue)
/* Error: privateValue is not defined */

모듈 내에 private 변수를 둚으로써 전역 스코프에 의도치 않게 변수를 추가하는 불상사가 줄어든다. 개발자는 전역 변수를 덮어쓰게 되거나 하는 등의 걱정 없이 코드를 작성할 수 있다. 또 변수명이 충돌되는 것도 막을 수 있다.


가끔 export된 변수의 이름이 모듈 내 로컬 변수와 이름이 겹칠 수 있다.

import { add, multiply, subtract, square } from './math.js'

function add(...args) {
  return args.reduce((acc, cur) => cur + acc)
} /* Error: add has  already been declared */

function multiply(...args) {
  return args.reduce((acc, cur) => cur * acc)
}
/* Error: multiply has already been declared */

위의 경우 모듈 스코프에 add, multiply 함수가 존재하고 있는데. 동일한 이름의 값을 import하려 할 때 동일한 이름이 이미 존재한다는 에러가 발생한다. 이 경우 as 키워드를 통해 import한 값의 이름을 변경할 수 있다.

아래 예제에서는 add, multiply 를 각각 addValues, multiplyValues로 변경하여 import하고 있다.

import {
  add as addValues,
  multiply as multiplyValues,
  subtract,
  square,
} from './math.js'

function add(...args) {
  return args.reduce((acc, cur) => cur + acc)
}

function multiply(...args) {
  return args.reduce((acc, cur) => cur * acc)
}

/* From math.js module */
addValues(7, 8)
multiplyValues(8, 9)
subtract(10, 3)
square(3)

/* From index.js file */
add(8, 9, 2, 10)
multiply(8, 9, 2, 10)

export 키워드를 사용한 선언들 중에 하나를 default export 할 수 있다.

아래는add함수를 default export하는 예제이다. default export를 원하는 값 앞에 export default 키워드를 사용하면 된다.

export default function add(x, y) {
  return x + y
}

export function multiply(x) {
  return x * 2
}

export function subtract(x, y) {
  return x - y
}

export function square(x) {
  return x * x
}

일반 export와 default export의 차이점은 값을 가져다 쓰는 방법에 있다.

이전에 named export를 사용할 땐 대괄호를 사용하였지만 import { module } from 'module' . 반대로 default export된 값을 import할 땐 대괄호 없이 import하면 된다. import module from 'module'

import add, { multiply, subtract, square } from './math.js'

add(7, 8)
multiply(8, 9)
subtract(10, 3)
square(3)

값이 대괄호 없이 import되었다면 해당 값은 default export된 것이다.

default export된 값을 사용할 때에는 이름을 자유롭게 변경할 수 있다. 위의 예제는 add 함수를 가져오기 위해 add 란 이름을 쓰고 있는데 아래처럼 이름을 바꿀 수 있다.

import addValues, { multiply, subtract, square } from './math.js'

addValues(7, 8)
multiply(8, 9)
subtract(10, 3)
square(3)

default export 된 함수의 이름은 add로 명시되어 있지만 자바스크립트가 이를 처리해주기 때문에 이렇게 import하여 호출할 수 있다.

* 와 이름을 사용하는 하는것으로 모듈 내 default export롤 포함하여 export하는 모든 것들을 한번에 import할 수 있다. export된 모든 것을 포함하는 객체 형태로 사용할 수 있다. 아래는 math.js 의 모든 export들을 math 라는 이름으로 가져오는 예제이다.

import * as math from './math.js'

math 객체에 math.js 모듈이 export하는 것들이 객체 형태로 담겨 있다,

import * as math from './math.js'

math.default(7, 8)
math.multiply(8, 9)
math.subtract(10, 3)
math.square(3)

이 경우 해당 모듈이 export하는 모든 것을 가져오기 때문에 불필요한 것들이 딸려오지 않도록 주의가 필요하다.

* 을 사용하여 import하여도 모듈 내 private 변수들은 명시적으로 export하지 않는 한 가져올 수 없다.


React

React앱을 개발할 때 앱의 규모가 커 지면 많은 컴포넌트를 다루게 된다. 컴포넌트들을 한 파일에 모두 선언하지 않고 각 모듈에 하나의 컴포넌트를 선언하게 된다.

아래는 목록 컴포넌트 list listItem, 그리고 할 일 입력을 위한 input field를 가진 예제이다.

예제에서 컴포넌트들은 각각의 파일로 분리되어 있다.

  • TodoList.js 에는 List 컴포넌트가 있다
  • Button.js 에는 커스텀 된 Button 컴포넌트가 있다
  • Input.js 에는 커스텀 된 Input 컴포넌트가 위치한다

앱 전체에서 material-ui가 제공하는 ButtonInput을 직접 사용하지 않고 각각 파일에서 커스텀하여 사용하고 있다. 각 파일 안에는 style객체를 이용해 버튼과 텍스트박스의 스타일을 정의하고 있다. 사용하는 쪽에서는 이 모듈만 사용하면 직접 처리할 필요 없이 디자인된 컴포넌트를 사용할 수 있는 것이다.

각 파일에는 스타일 지정을 위해 모듈 스코프 변수를 선언하고 있지만 모듈 구문으로 인해 이름 충돌은 일어나지 않으므로 style 이라는 일반적인 이름을 사용하고 있다.


Dynamic import

파일의 맨 위에서 모듈들을 import하면 파일 내 다른 코드들이 실행되기 전에 해당 모듈이 로드된다. 어떤 상황에서는 특정 조건에서만 특정 모듈을 로드해야 할 때가 있는데. Dynmic import를 사용하면 필요할 때만 로드할 수 있다.

import('module').then(module => {
  module.default()
  module.namedExport()
})

// Or with async/await
;(async () => {
  const module = await import('module')
  module.default()
  module.namedExport()
})()

아래 예제에서 사용자가 버튼을 클릭했을 때 모듈을 불러와 기능을 사용하고 있다.

모듈을 동적으로 로딩하여 페이지 로딩 타임을 줄일 수 있다. 기능이 필요할 때에만 로드하고 파싱하고 컴파일하여 코드를 사용하게 되는것이다.

또 필요할 때 모듈을 로딩하는것 외에도 import() 함수는 인자로 표현식을 받는다. 템플릿 리터럴도 사용 가능하기 때문에 필요에 따라 변수로 필요 모듈을 받아오도록 할 수도 있다.

위의 예제에서 date.js 모듈은 사용자가 버튼을 클릭할 때 동적으로 moment 모듈을 불러온다. 사용자가 날짜를 확인하지 않아도 되면 서드파티 모듈 자체를 다운로드하지 않도록 할 수 있는 것이다.

목록의 각 버튼을 클릭했을 때 로컬에 있는 png파일을 동적으로 불러오려면 아래처럼 순서를 템플릿 스트링으로 넘기는것도 가능하다.

const res = await import(`../assets/dog${num}.png`)

사용자의 입력이나 어떤 데이터의 결과에 따라 유연하게 모듈을 로드하여 사용할 수 있다.


모듈 패턴을 사용하면 코드의 일부분을 캡슐화 할 수 있다. 이는 의도치 않은 전역 변수 할당을 예방할 수 있어 여러 의존 모듈을 사용하거나 네임스페이스를 사용할 때 안전하다. 모든 자바스크립트 런타임에서 ES2015의 모듈을 사용하려면 바벨과 같은 트랜스파일러가 필요하다.