코드베이스가 커질수록 코드들을 유지보수 하기 좋게 쪼개는 것이 중요하다.
이 때 모듈 패턴이 코드들을 재사용 가능하면서도 작게 나눌 수 있게 해준다.
또한 모듈 패턴으로 코드를 나누는 과정에서 특정 변수들을 파일 내에 private하게 할 수 있는데, 모듈 스코프 내에서 변수를 선언하고 명시적으로 외부에 export하지 않으면 바깥에서 해당 변수에 접근할 수 없다.
이를 통해 전역 스코프의 변수들과 이름이 충돌하는 문제를 줄일 수 있다.
ES2015에는 자바스크립트의 빌트인 모듈 기능이 추가되었다. 모듈은 자바스크립트 코드를 포함한 파일이며, 일반적인 스크립트와 동작이 약간 다르다.
// math.js
function add(x, y) {
return x + y;
}
function multiply(x) {
return x * 2;
}
function subtract(x, y) {
return x - y;
}
function square(x) {
return x * x;
}
// index.js
console.log(add(2, 3));
console.log(multiply(2));
console.log(subtract(2, 3));
console.log(square(2));
전달된 인자를 더하고, 곱하고, 빼고, 제곱하는 함수를 구현한 math.js
가 있다.
이 함수들은 math.js
에서 사용하지 않고, index.js
에서 사용하고 싶다. 함수들을 index.js
에서 직접 사용하려고 하면 당연하게도 함수가 존재하지 않는다는 예외가 발생한다.
함수들을 math.js
외에 다른 파일에서도 사용할 수 있게 하려면 먼저 각 함수들을 export 해야 한다. 그러기 위해서 export
키워드를 사용한다. 이는 named export
라 하며, 파일 외부에서 사용하기 원하는 것 앞에 export
키워드를 붙이면 된다.
예제에서는 각 계산 함수의 앞에 붙여줌으로써 index.js
에서 함수들을 사용할 수 있게 되었다.
// math.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
키워드 뒤에 해당 모듈의 상대 경로를 입력한다.
// index.js
import { add, multiply, subtract, square } from './math.js'
이렇게 index.js
에서 math.js
모듈의 함수들을 import하여 사용할 수 있게 되었다.
모듈의 장점은 명시적으로 export한 값들만 외부에 노출된다는 것이다. 명시적으로 export하지 않으면 모듈 내에서만 사용할 수 있다.
아래는 math.js
모듈 내에서만 사용할 수 있는 변수 privateValue
를 사용하는 예제이다.
// math.js
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
외부에서는 해당 변수에 접근할 수 없다.
// index.js
import { add, multiply, subtract, square } from './math.js'
console.log(privateValue)
/* Error: privateValue is not defined */
모듈 내에 private 변수를 둚으로써 전역 스코프에 의도치 않게 변수를 추가하는 불상하가 줄어든다. 개발자는 전역 변수를 덮어쓰게 되거나 하는 등의 걱정없이 코드를 작성할 수 있다. 또한, 변수명이 충돌되는 것도 막을 수 있다.
가끔 export된 변수의 이름이 모듈 내 로컬 변수와 이름이 겹칠 수 있다.
// index.js
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하고 있다.
// index.js
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
키워드를 사용하면 된다.
// math.js
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'
)
// index.js
import add, { multiply, subtract, square } from './math.js'
add(7, 8)
multiply(8, 9)
subtract(10, 3)
square(3)
default export
된 값을 사용할 때에는 이름을 자유롭게 변경할 수 있다.
// index.js
import addValues, { multiply, subtract, square } from './math.js'
addValues(7, 8)
multiply(8, 9)
subtract(10, 3)
square(3)
default export
된 함수의 이름은 add
로 명시되어 있지만 자바스크립트가 이를 처리해주기 때문에 addValues
로 import하여 호출할 수 있다.
또한, *
와 이름을 사용함으로써 모듈 내 defalut export
를 포함하여 export
하는 모든 것들을 한 번에 import할 수 있다. export된 모든 것을 포함하는 객체 형태로 사용할 수 있다.
// index.js
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 앱을 개발할 때 앱의 규모가 커지면 많은 컴포넌트를 다루게 된다. 컴포넌트들을 한 파일에 모두 선언하지 않고 각 모듈에 하나의 컴포넌트를 선언하게 된다.
아래는 목록 컴포넌트 list
, listItem
그리고 할 일 입력을 위한 input field
를 가진 예제이다.
import React from 'react'
import { render } from 'react-dom'
import { TodoList } from './components/TodoList'
import './styles.css'
render (
<div className="App">
<TodoList />
</div>
document.getElementById('root')
);
TodoList.js
에는 List
컴포넌트가 있다.Button.js
에는 커스텀된 Button
컴포넌트가 있다.Input.js
에는 커스텀된 Input
컴포넌트가 위치한다.앱 전체에서 material-ui
가 제공하는 Button
과 Input
을 직접 사용하지 않고 각각 파일에서 커스텀하여 사용하고 있다. 각 파일 안에는 style 객체를 이용해 버튼과 텍스트박스의 스타일을 정의하고 있다. 사용하는 쪽에서는 이 모듈만 사용하면 직접 처리할 필요없이 디자인된 컴포넌트를 사용할 수 있는 것이다.
각 파일에는 스타일 지정을 위해 모듈 스코프 변수를 선언하고 있지만, 모듈 구문으로 인해 이름 충돌은 일어나지 않으므로 style
이라는 일반적인 이름을 사용하고 있다.
파일의 맨 위에서 모듈들을 import하면 파일 내 다른 코드들이 실행되기 전에 해당 모듈이 로드된다. 어떤 상황에서는 특정 조건에서만 특정 모듈을 로드해야 할 때가 있는데, Dynamic iport를 사용하면 필요할 때만 로드할 수 있다.
import('module').then(module => {
module.default()
module.namedExport()
})
// Or with async/await
(async () => {
const module = await import('module')
module.default()
module.namedExport()
})()
아래 예제는 사용자가 버튼을 클릭했을 때 모듈을 불러와 기능을 사용하고 있다.
const button = document.getElementById("btn");
button.addEventListener("click", () => {
import("./math.js").then((module) => {
console.log("Add: ", module.add(1, 2));
console.log("Multiply: ", module.multiply(3, 2));
const button = document.getElementById("btn");
button.innerHTML = "Check the console";
});
});
/**** Or with async/await ****/
// button.addEventListener("click", async () => {
// const module = await import("./math.js");
// console.log("Add: ", module.add(1, 2));
// console.log("Multiply: ", module.multiply(3, 2));
// });
모듈을 동적으로 로딩하여 페이지 로딩 타임을 줄일 수 있다. 기능이 필요할 때에만 로드하고 파싱하고 컴파일하여 코드를 사용하게 되는 것이다.
또 필요할 때 모듈을 로딩하는 것 외에도 import()
함수는 인자로 표현식을 받는다. 템플릿 리터럴도 사용 가능하기 때문에 필요에 따라 변수로 필요 모듈을 받아오도록 할 수도 있다.
목록의 각 버튼을 클릭했을 때 로컬에 있는 png 파일을 동적으로 불러오려면 아래처럼 순서를 템플릿 스트링으로 넘기는 것도 가능하다.
const res = await import(`../assets/dog${num}.png`)
사용자의 입력이나 어떤 데이터의 결과에 따라 유연하게 모듈을 로드하여 사용할 수 있다.
모듈 패턴을 사용하면 코드의 일부분을 캡슐화할 수 있다. 이는 의도치않은 전역 변수 할당을 예방할 수 있어 여러 의존 모듈을 사용하거나 네임스페이스를 사용할 때 안전하다.
모든 자바스크립트 런타임에서 ES2015이 모듈을 사용하려면 바벨과 같은 트랜스파일러가 필요하다.
< 출처 : https://patterns-dev-kr.github.io/design-patterns/module-pattern/ >