[Design Patterns] Module 패턴

·2024년 2월 6일
0

patterns

목록 보기
7/11
post-thumbnail

Module 패턴

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

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

ES2015 모듈

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
}

일반 exportdefault 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

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

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

Dynamic import

파일의 맨 위에서 모듈들을 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/ >

profile
개발을 개발새발 열심히➰🐶

0개의 댓글