자바스크립트의 모듈

나혜수·2023년 2월 9일
0

자바스크립트 실전

목록 보기
7/19

모듈

일반적으로 웹 사이트는 여러 개의 자바스크립트 파일로 이루어져 있다. 예전에는 자바스크립트 파일 간 통신을 위해 전역 scope에 존재하는 변수와 함수를 사용해야 했다. 즉시 실행 함수 등을 통해 전역 scope가 오염되는 것을 어느정도 막을 수 있었지만, 파일 간 의존도를 확인하기 힘들고 실행 순서를 제어해야 한다는 한계점이 있었다.

이러한 불편함을 해결하기 위해 모듈이 등장했다. 모듈은 프로그램을 구성하는 구성 요소로, 관련된 데이터와 함수를 하나로 묶은 단위를 의미한다. 보통 하나의 소스 파일에 모든 함수를 작성하지 않고 함수의 기능별로 따로 모듈을 구성한다. 이러한 모듈을 합쳐 하나의 파일로 작성하는 방식으로 프로그램을 만들게 된다. 모듈을 사용하면 스크립트 간 의존도를 파악할 수 있고 실행 순서를 쉽게 제어할 수 있다.

  • 스크립트 의존성을 훨씬 간편하게 관리할 수 있다.

  • 사용되는 모듈을 명시적으로 import 해오기 때문에, 사용되거나 사용되지 않는 스크립트를 추적할 수 있다.

  • 스크립트 태그를 불러오는 순서로 인한 스트레스에서 해방될 수 있다. import로 불러오는 경우 순서는 무관하다.

  • <script src = " ">로 불러오는 것과 다르게 전역 오염이 발생하지 않는다.

모듈과 컴포넌트
모듈과 컴포넌트는 자주 혼용된다.
모듈은 개발 단계에서의 식별 단위이며, 컴포넌트는 runtime에서의 식별 단위이다.

  • 모듈은 "구현의 단위이자 배포의 단위 " 이다. 예를 들어 어떤 library, program이 바로 모듈이다. 또한 스크립트 파일 하나도 하나의 구현 단위로서 module이 될 수 있다.

  • 컴포넌트는 "동작의 단위 " 이다. 하나의 모듈이 하나의 컴포넌트에 1:1로 매칭될 수도 있고 그렇지 않을 수도 있다. UI 상에 표시되어 동작하는 button control 하나도 컴포넌트가 될 수 있으며, 하나의 process를 만들고 동작하는 것도 컴포넌트로 정의할 수 있다.

자바스크립트에서 모듈은 단지 파일 하나에 불과하다. 스크립트 하나는 모듈 하나이다.


export & import

export는 모듈에서 객체, 함수, 원시값을 내보낼 때 사용하며, 다른 파일에서 내보낸 값은 import로 가져올 수 있다.

내보내는 모듈, 가져오는 모듈은 "use strict " 유무와 상관없이 무조건 엄격 모드이다.
import, export은 HTML 안에 작성한 스크립트에서는 사용할 수 없다.


1. export

export는 named, default 두 종류가 있다. 모듈 하나에서 named는 여러 개 존재할 수 있지만 default는 하나만 가능하다.

Named export

named는 여러 값을 내보낼 때 유용하다. import 할 때는 내보낸 이름과 동일한 이름을 사용해야 한다.

// 개별로 선언해서 export
export const arrs = [10, 20, 30, 40]; 

// 묶어서 export 
export { arrs2, getName }; 

// 내보내면서 이름 바꾸기
export { variable1 as name1, variable2 as name2 };

Default exports

export default는 모듈 당 하나만 있어야 한다.
export default를 사용할 때 var, let, const는 사용하지 못한다.

// 먼저 선언한 식별자 내보내기
export { myFunction as default };

// 각각의 식별자 내보내기
export default function () { ... };
export default class { ... }

default는 어떤 이름으로도 가져올 수 있다.

// test.js
let k; 
export default k = 12;
// 임의의 다른 파일
import m from './test'; // k가 default이므로, 가져오는 이름으로 k 대신 m을 사용 OK
console.log(m);         // 12

2) import

// 모듈 전체 가져오기 
import * as name from "module-name";


// 하나의 멤버 가져오기 
import { export1 } from "module-name";
import { export1 as alias1 } from "module-name"; // 다른 이름으로 가져오기 


// 여러 개의 멤버 가져오기 
import { export1 , export2 } from "module-name";
import { export1 , export2 as alias2 } from "module-name"; // 다른 이름으로 가져오기 


// default export 값 가져오기 ❗이때 { }는 넣지 않는다.
import defaultExport from "module-name";
import defaultExport, { export1, export2 } from "module-name";
// 기본 값을 가져오는 부분이 먼저 선언되야 한다. 


// 모듈만 불러와 실행하기
import "module-name";

Dynamic import

위의 예제에서 다룬 export, import는 ‘정적인’ 방식으로 문법이 단순하고 제약이 있다.

  1. import 문에 동적 매개변수를 사용할 수 없다.
    모듈 경로엔 원시 문자열만 들어갈 수 있기 때문에 함수 호출 결괏값을 경로로 쓰는 것이 불가능하다.
 import ... from getModuleName(); // 에러 발생 / 문자열만 허용
  1. 런타임이나 조건부로 모듈을 불러올 수 없다.
if(...) {
        import ...; // 모듈을 조건부로 불러올 수 없으므로 에러 발생
    }
    {
        import ...; // import문은 블록 안에 올 수 없으므로 에러 발생
    } 

이런 제약이 만들어진 이유는 import/export는 코드 구조의 중심을 잡아주는 역할을 하기 때문이다. 코드 구조를 분석해 모듈을 한데 모아 번들링하고, 사용하지 않는 모듈은 제거(가지치기)해야 하는데, 코드 구조가 간단하고 고정되어있을 때만 이런 작업이 가능하다.

그럼에도 불구하고 모듈을 동적으로 불러와야 한다면 어떻게 해야 할까?
import(module) 표현식은 이 모듈이 내보내는 것을 모두 포함하는 객체를 담은 이행된 프로미스를 반환한다. 호출은 어디서나 가능하다. 코드 내 어디에서 동적으로 사용할 수도 있다.

import(모듈)
  .then((모듈) => {
    // 모듈 가지고 할 것
  })
  .catch((e) => {
    // 모듈을 불러올 때 에러가 났을 때 할 것
  });

3) 모듈 다시 내보내기

export from을 사용하지 않는다면 import, export를 따로 선언해야 하지만 export from을 사용하면 가져온 모듈을 다시 내보낼 수 있다.

// module2.js
export const PI = 3.14;
export const ZERO = 0;
export const age = 25;
// module1.js
import { PI } from './module2.js';
export { PI };

export { ZERO } from './module2.js';

export {default as age} from './module2.js';  // default export
// main.js
import { PI } from './module1.js';
console.log(PI); // 3.14

import { ZERO } from './module1.js';
console.log(ZERO); // 0

import age from './module1.js';  
console.log(age); // 25

named export와 default export가 있는 모듈에서 export * from을 사용하면 named export만 다시 내보내진다. 따라서 default export까지 내보내고 싶다면 아래처럼 따로 내보내야 한다.

export * from './Auth.js';
export {default} from './Auth.js'


모듈 예시

App.js

export default function App () {
    this.render = () => {
        alert('hello!')
    }
    this.render()
}

export const printToday = () => {
    console.log(new Date().toLocaleString()) // 지역에 맞는 날짜 표기법으로 바꿔서 출력
}

constants.js

export const DOMAIN_NAME = 'www.naver.com'
export const PORT = '8000'

export const isProduction = () => {
    return false
}

index.js

// import문은 항상 맨 위 from에 .js 꼭 붙이기
import * as constants from './constants.js' 
import App, {printToday} from './App.js' 
// default와 default가 아닌 것을 동시에 불러올 수 있다. 

const $body = document.querySelector('body')

$body.innerHTML = $body.innerHTML + JSON.stringify(constants)

new App()

index.html
모듈을 쓰는 경우 가장 처음 불러오는 스크립트는 type = "module" 을 넣어줘야 한다. index.jsindex.html로 불러오고, index.js 내에서 import, export 키워드로 다른 모듈을 불러온다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src = "./src/index.js" type="module"></script> 
                               <!--type="module" 추가-->
</body>
</html>

결과


module을 활용하여 Todo List 수정하기

컴포넌트의 경우 export default를 사용하기로 약속한다. 외부 의존성이 없는 Header, TodoForm, TodoList 컴포넌트를 먼저 처리한다.

index.html

main.js를 불러오는 스크립트에 type = "module" 추가하고, 나머지 스크립트는 전부 삭제한다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>simple Todo List</title>
</head>
<body>
    <main class="app"></main>
    <script src="./src/main.js" type="module"></script> 
</body>
</html>

Header.js

function 앞에 export default를 추가한다.

export default function Header ({$target, text}) {
    const $header = document.createElement('h1')

    $target.appendChild($header)

    this.render = () => {
        $header.textContent = text
    }
    this.render()
}

TodoList.js

function 앞에 export default를 추가한다.

export default function TodoList({$target, initialState}) {
    const $todoList = document.createElement('div'); // 컴포넌트의 DOM
    $target.appendChild($todoList);

    this.state = initialState;

    // 현재의 상태를 변경하고 변경한 상태를 다시 랜더링
    this.setState = nextState => {
        this.state = nextState;
        this.render();
    }

    this.render = () => {
        $todoList.innerHTML = `
            <ul>
                ${this.state.map(({text}) =>`<li>${text}</li>`).join('')}
            </ul>
        `
    }

    this.render();
}

TodoForm.js

function 앞에 export default를 추가한다.

export default function TodoForm({$target, onSubmit}) {
    const $form = document.createElement('form')
    $target.appendChild($form)

    let isInit = false;

    this.render = () => {
        $form.innerHTML = `
            <input type ="text" name = "todo" />
            <button>add</button>
        `

        if(!isInit) {
            $form.addEventListener('submit', e => {
                e.preventDefault();
            
                const $todo = $form.querySelector('input[name=todo]')
                const text = $todo.value
                
                if(text.length > 1) { 
                // 한 글자 이상 들어왔을 때만 (없으면 빈 문자열만 들어갈 수 있음)
                    $todo.value = '' // 입력 후 input 비워주기

                    onSubmit(text) // App.js의 TodoForm 내의 onSubmit이 호출
                }
            })
            isInit = true
        }
    }
    
    this.render()
}

storage.js

이전에는 전역 오염을 최소화시키고 storage라는 namespace로만 로컬 스토리지를 노출시키기 위해서 즉시 실행 함수를 이용했었다. 이제는 그럴 필요가 없다.

const storage = window.localStorage

export const setItem = (key, value) => {
    try {
        storage.setItem(key,value)
    } catch(e) {
        console.log(e)
    }
}

export const getItem = (key, defaultValue) => {
    try {
        const storedValue = storage.getItem(key)

        if(storedValue) {
            return JSON.parse(storedValue)
        }
        return defaultValue
    } catch(e) {
        console.log(e)
        return defaultValue
    }
}

App.js

App에서 사용하는 컴포넌트, 함수를 import 키워드로 불러오도록 선언한다.

import Header from './Header.js'
import TodoForm from './TodoForm.js'
import TodoList from './TodoList.js'
import { setItem } from './storage.js'

export default function App({$target, initialState}) {
    new Header({
        $target, 
        text: 'simple Todo List'
    })
    new TodoForm({
        $target,
        onSubmit: (text) => {
            const nextState = [...todoList.state, { text }]
            todoList.setState(nextState)
            setItem('todos', JSON.stringify(nextState)) 
            // storage.setItem → setItem으로 바뀜 
        }
    })

    const todoList = new TodoList ({
        $target,
        initialState
    })
}

main.js

마지막으로 main.jsimport를 선언한다.

import App from './App.js'
import { getItem } from './storage.js'

const initialState = getItem('todos', [])
// // storage.getItem → getItem으로 바뀜

const $app = document.querySelector('.app')

new App({
    $target: $app,
    initialState
})
profile
오늘도 신나개 🐶

0개의 댓글