
누구나 프로젝트를 하다 보면, 처음에는 작은 기능만을 구현했던 코드가 점점 기능을 덧붙이며 몸집이 커져서 한 파일 안에 수백 줄이 넘어갔던 경험이 있을 거다.
처음에는 (나름) 깔끔했던 코드도 시간이 지나면서 다른 사람들과 협업을 한다거나 기능을 추가할 때마다 점점 더 복잡해지고, 또 비슷한 로직이 여러 번 반복되기도 한다. 하나의 파일에서 모든 걸 관리하려니 점점 불편했던 적이 한 번 쯤은 있을 거다.
하나라도 수정을 하려면 그 수백 줄의 코드 중에 어디에 있는 지 찾아야 하고, 기능을 추가할 때마다 다른 부분에 영향을 미치기 때문에 잡아야 할 에러도 늘어나게 돼 불편함이 이만저만이 아니다. 😱이럴 때 필요한 게 '모듈'이다. 모듈화는 반복되는 코드를 하나의 파일로 묶어두고, 필요한 곳에서 가져다 쓸 수 있게 해준다. 이렇게 하면 코드 중복을 줄일 수 있고 코드가 간결해지면서 관리가 쉬워진다.
나는 아직 친숙하지 않은 프로그래밍의 작은 개념들을 늘 일상에 빗대어 이해하려는 습관이 있다.
오늘 소개할 모듈은 요리의 식재료로 비유해보려고 한다.
집에 있다보니 출출해져서 냉장고와 찬장을 열어봤다. 파스타 면, 각종 소스, 올리브유, 마늘, 치즈, 파슬리, 푸른 잎 채소 등이 있다. 이 재료로 만들 수 있는 요리들이 뭐가 있을까?
우선 알리오올리오를 만들 수 있을 것 같다. 소스가 있으니 토마토 파스타나 치즈를 갈아서 올린 샐러드도 만들 수 있겠지.
오늘은 간단히 알리오올리오로 먹겠다고 생각을 했다. 그런데 알리오올리오를 하려고 위의 재료를 모두 꺼내는 사람은 없을 거다. 요리는 필요한 재료만 꺼내서 하는 거니까!
알리오올리오에서 마늘은 향을 내는 역할을 하고, 올리브유는 면수와 만나며 유화를 담당할 거다. 치즈는 짭조름한 간을 더해줄 것이고 파슬리는 마지막 데코레이션의 역할을 하겠지.
모듈도 마찬가지다. 각 모듈은 독립적인 '역할' 또는 '기능'을 담당하는 코드 덩어리로, 필요할 때마다 꺼내서 사용할 수 있다. 모든 코드를 한 파일에 몰아넣는 대신, 필요한 기능만 가져와서 사용할 수 있는 점이 바로 모듈화의 장점이다.
: 자바스크립트에는 두 가지 주요 모듈 시스템이 있다. 식재료를 준비하는 방식이라고 생각하면 쉽겠다.
: CommonJS는 마치 요리 재료를 도매상에서 한 번에 사오는 방식과 비슷하다. 도매상에서는 필요한 재료들을 한 번에 사오고, 요리할 때마다 그 재료를 꺼내서 사용한다. require는 도매상에서 재료를 한 번에 사오는 방식에 해당하고, module.exports는 그 재료를 바로 가져가서 쓸 수 있게 준비해 두는 거다.
// pasta.js (파스타 재료)
function cookPasta() {
return "Pasta is cooked!";
}
function makeSauce() {
return "Sauce is ready!";
}
module.exports = { cookPasta, makeSauce };
// main.js (요리하는 곳)
const pasta = require('./pasta.js');
console.log(pasta.cookPasta()); // Pasta is cooked!
console.log(pasta.makeSauce()); // Sauce is ready!
pasta.js에 있는 함수들을 module.exports = { cookPasta, makeSauce } 로 내보낸다. 마치 미리 연락을 받은 도매상이 손님이 가게에 도착하자마자 구매해 가실 수 있게끔 재료를 미리 포장해두는 거다.main.js 에서 가져온다. 바로 require('./pasta.js) 부분에서 말이다. 저 구문을 통해 pasta.js에서 준비된 모든 재료들을 가져올 수 있게 되었다. 그치만 ./pasta.js에 있는 모든 모듈을 가져오는 것이기 때문에 도매상이 준비한 상자안에 main.js에서 사용하지 않을 재료도 함께 들어있을 수 있다. : ES6 모듈은 마치 요리 재료를 필요한 만큼 준비하는 방식과 비슷하다. 예를 들어 토마토 파스타를 만들 때는 파스타 면과 토마토 소스를 미리 준비해두고, 요리할 때마다 필요한 만큼 꺼내서 사용하는 거다.
// pasta.js (파스타 재료)
export function cookPasta() {
return "Pasta is cooked!";
}
// sauce.js (소스 재료)
export function makeSauce() {
return "Sauce is ready!";
}
// main.js (요리하는 곳)
import { cookPasta } from './pasta.js';
import { makeSauce } from './sauce.js';
console.log(cookPasta()); // Pasta is cooked!
console.log(makeSauce()); // Sauce is ready!
pasta.js와 sauce.js는 각각 파스타 재료와 소스 재료를 따로 관리한다. 이렇게 구좌를 나누면, 필요한 재료만 정확히 가져올 수 있다.export는 재료를 꺼내두는 행위에 해당하고, import는 그 재료를 실제로 가져오는 행위다. 이 방식은 필요한 재료만 골라서 사용할 수 있도록 한다는 점에서 매우 효율적이다.필요한 것만 가져오기
: CommonJS 방식과는 달리 ES6 모듈은 필요한 재료만 골라서 가져오는 방식이다. 위 코드에서는 cookPasta와 makeSauce만 import했기 때문에 불필요한 코드가 포함되지 않는다.
코드 가독성과 유지보수성 향상
: 파일별로 명확히 역할을 나눌 수 있기 때문에 어떤 파일이 어떤 역할을 하는지 쉽게 파악할 수 있다. 이는 대규모 프로젝트에서 진가를 발휘한다. 예를 들어 소스 관련 기능은 모두 sauce.js에서 관리하고, 파스타 관련 기능은 pasta.js에서 관리하는 방식으로 파일 구조를 명확히 나눌 수 있게 된다.
브라우저 및 최신 환경 지원
: ES6 모듈은 최신 브라우저와 표준을 따르는 환경에서 기본적으로 지원된다. 따라서 추가적인 번들링 없이도 모듈을 사용할 수 있는 경우가 많다.
의존성 관리의 용이함
: 특정 파일에서만 필요한 모듈을 improt 하기 때문에, 불필요한 의존성이 생기는 것을 방지할 수 있다. 이는 성능 최적화와 코드 유지보수에 매우 유리하다.
: 정리하자면, CommonJS 방식은 그 요리의 재료 보관 방법이 어떠한 지와 무관하게 한 상자에 넣어서 보관하고, 요리를 할 때는 그 상자를 통째로 가져와서 사용하는 방식에 해당한다.
실온 보관해야 하는 재료나 냉장 보관해야 하는 재료 구분 없이 요리마다 구분해 필요한 재료를 상자에 담아서 그 상자 전체를 가지고 요리를 하는 거다. 이 방식은 간단하고 빠르게 재료를 준비할 수 있지만, 상자 안에 사용하지 않을 재료까지 함께 포함될 수 있다는 단점이 있다.
ES6 모듈 방식은 각 재료를 적절한 보관 장소에 보관한다. 재료의 특성에 맞게 냉장고에, 혹은 실온에 나누어 보관하고 각 장소마다 필요한 재료를 꺼내어 쓴다는 거다. 이 방식은 필요한 재료만 가져올 수 있어 효율적이며, 대규모 요리(프로젝트)에서도 재료를 체계적으로 관리할 수 있다는 장점이 있다.
모듈을 사용하면 필요한 코드만 꺼내서 효율적인 코딩을 할 수 있는 장점이 있지만, 너무 과도하게 나눠진 모듈은 오히려 로직을 복잡하게 할 수 있다.
그래서 적절한 모듈화를 하는 것이 중요하다. 그 적절함을 찾는 건 요리 고수가 "고추장 적당히 눈대중으로" 라고 하는 말을 이해하는 것만큼 어렵겠지만 😅