JavaScript 함수와 객체, import export로 모듈 나누기

2026.09.05·수정 2026.09.12·약 17분·작성: 해비·블로그 소개

이 글에서 정리하는 내용

함수에 작업을 맡기고 객체에 관련 값을 묶은 뒤, 두 JavaScript 파일 사이에서 named export와 default export를 가져와 실행하는 과정을 완성합니다.

먼저 변수·조건·반복문을 읽고 Node.js에서 파일을 실행할 수 있도록 준비하세요.

함수의 입력과 반환값 이해하기

JavaScript 함수와 모듈을 함께 배우면 한 파일에 길게 적던 로직을 역할별로 나눌 수 있습니다. 함수 선언의 매개변수는 호출할 때 들어올 값의 이름이고, return은 계산 결과를 호출한 자리로 돌려줍니다. 반환값이 필요한 함수와 출력만 하는 코드를 구분하면 재사용이 쉬워집니다.

calculatePrice(4500, 2)를 호출하면 price는 4500, quantity는 2가 되고 9000을 반환합니다. 함수 안에서 바로 console.log만 하면 결과를 다른 계산에 쓰기 어렵습니다. 먼저 값을 반환하고, 화면이나 콘솔 출력은 함수를 호출하는 쪽에서 결정하는 구조가 더 유연합니다.

화살표 함수도 함수를 값으로 만드는 문법입니다. const double = (value) => value * 2;는 숫자를 받아 두 배를 반환합니다. 중괄호로 본문을 감싸면 return을 직접 써야 합니다. 함수를 다른 함수의 인수로 넘겨 나중에 호출하도록 맡기는 것이 콜백입니다. 아래 apply는 전달받은 함수를 실제로 호출합니다. 배열 메서드와 이벤트 처리에서도 같은 방식이 쓰입니다.

const double = (value) => value * 2;
function apply(value, operation) {
  return operation(value);
}
console.log(apply(3, double)); // 6
원두·분쇄기·커피잔으로 함수의 입력과 처리 및 결과를 비유한 삽화
함수는 입력을 받아 처리 결과를 반환합니다. 커피 도구는 이 흐름을 설명하는 비유입니다.

객체로 서로 관련된 값 묶기

객체는 이름이 있는 속성으로 관련 값을 묶습니다. { name: "드립 커피", price: 4500 }에서 name과 price가 속성입니다. coffee.name처럼 점 표기법으로 읽고, 속성 이름을 문자열 변수로 다뤄야 할 때는 대괄호 표기법을 씁니다.

예제의 createProduct는 name과 price를 받아 객체를 반환합니다. 함수가 객체 생성 규칙을 한곳에 모으므로 호출부는 필요한 값만 전달합니다. 객체의 구조 분해와 spread 복사는 이 글에서 반복하지 않습니다. 그 문법은 기존 JavaScript 배열 메서드 글의 “배열 생성·복사·구조 분해” 절에서 이어서 확인할 수 있습니다.

named export와 default export 구분하기

ES 모듈은 파일 단위로 공개할 값과 가져올 값을 정합니다. named export는 한 파일에서 여러 개를 만들 수 있으며 가져올 때 내보낸 이름을 중괄호 안에 정확히 씁니다. default export는 모듈당 하나이며 중괄호 없이 가져옵니다. 가져오는 쪽에서 이름을 정할 수 있지만, 원래 역할이 드러나는 이름을 쓰는 편이 좋습니다.

예제 첫 줄은 두 형식을 동시에 보여줍니다. createProduct는 default import이고, calculatePricedescribeProduct는 named import입니다. ./product.mjs처럼 상대 경로와 확장자를 명시해 파일 위치를 분명히 합니다.

분리된 두 폴더와 공통 탭으로 모듈 분리와 재사용을 표현한 삽화
모듈은 파일마다 책임을 나누고 필요한 기능을 공개합니다. 구체적인 연결은 import와 export 코드에서 확인합니다.

두 파일로 주문 계산 실행하기

빈 폴더에 다음 두 파일을 같은 위치로 저장합니다. Node.js가 ES 모듈로 해석하도록 여기서는 package.json 없이도 명확히 실행되도록 실제 검증 파일을 .mjs로 제공합니다.

product.mjs

export function calculatePrice(price, quantity) {
  return price * quantity;
}

export function describeProduct(product) {
  return `${product.name}: ${product.price}원`;
}

export default function createProduct(name, price) {
  return { name, price };
}

app.mjs

import createProduct, { calculatePrice, describeProduct } from "./product.mjs";

const coffee = createProduct("드립 커피", 4500);
const quantity = 2;

console.log(describeProduct(coffee));
console.log(`수량: ${quantity}`);
console.log(`합계: ${calculatePrice(coffee.price, quantity)}원`);

node app.mjs를 실행하면 “드립 커피: 4500원”, “수량: 2”, “합계: 9000원”이 차례로 출력됩니다. 모듈 파일은 직접 열어 각각 실행하기보다 진입점인 app.mjs를 실행합니다.

함수를 나눌 때는 코드 줄 수보다 책임을 기준으로 봅니다. createProduct는 객체 생성, calculatePrice는 숫자 계산, describeProduct는 표시 문자열 생성을 담당합니다. 함수 이름을 읽고 입력과 출력의 관계를 한 문장으로 말할 수 있다면 경계가 비교적 분명한 것입니다. 반대로 한 함수가 객체를 만들고 출력하고 파일까지 저장한다면 역할을 다시 나눌 시점입니다.

객체를 함수에 전달하면 여러 값을 매개변수로 길게 나열하지 않아도 됩니다. 다만 함수가 어떤 속성을 요구하는지는 호출부와 구현부가 함께 지켜야 합니다. 예제에서 describeProduct는 name과 price를 사용하므로 두 속성이 없는 객체를 전달하면 기대한 문장이 나오지 않습니다. 호출 전 객체를 콘솔에 출력해 실제 속성 이름과 값의 종류를 확인하는 습관이 도움이 됩니다.

모듈 경계는 공개 API이기도 합니다. 다른 파일에서 써야 하는 함수만 export하고, 해당 파일 내부에서만 쓰는 보조 함수는 내보내지 않습니다. 공개 항목이 적으면 이름 변경이나 내부 구현 수정이 다른 파일에 미치는 영향도 줄어듭니다. 먼저 한 파일에서 동작을 완성한 뒤 역할이 분명한 부분을 모듈로 옮기면 import 순환도 피하기 쉽습니다.

named export는 여러 기능 중 필요한 것만 이름으로 고를 때 유용합니다. 편집기의 자동 완성도 내보낸 이름을 기준으로 안내할 수 있습니다. default export는 파일의 대표 기능이 하나일 때 자연스럽습니다. 어떤 형식이 더 우수한 것이 아니라, 모듈이 제공하는 값의 성격을 일관되게 표현하는지가 선택 기준입니다.

실행 결과가 undefined라면 import 오류만 의심하지 말고 return 누락도 확인합니다. 함수 본문에서 계산했지만 반환하지 않으면 호출 결과는 undefined입니다. 객체 속성이 undefined라면 생성할 때 쓴 속성 이름과 읽을 때 쓴 이름을 비교합니다. 모듈 로드, 함수 호출, 객체 접근을 단계별로 나누어 출력하면 오류 범위를 빠르게 좁힐 수 있습니다.

파일을 나누기 전에는 의존 방향도 그려 보세요. app.mjs는 product.mjs가 공개한 함수를 사용하지만 product.mjs는 app.mjs를 알 필요가 없습니다. 이런 한 방향 구조에서는 계산 규칙을 독립적으로 시험하고 다른 진입점에서도 재사용하기 쉽습니다. 두 파일이 서로 import하면 초기화 순서와 아직 만들어지지 않은 값 때문에 이해하기 어려워질 수 있습니다. 공통 기능을 세 번째 모듈로 옮기면 순환을 풀 수 있습니다. 또한 모듈 최상단에서는 파일이 로드될 때 즉시 실행되는 코드가 무엇인지 살피고, 실제 작업은 명시적으로 호출하는 함수 안에 두면 import만 했는데 예상치 못한 출력이나 변경이 생기는 일을 줄일 수 있습니다.

모듈 오류를 빠르게 점검하는 순서

“요청한 export를 제공하지 않는다”는 오류가 나오면 중괄호 여부와 철자를 먼저 비교합니다. 파일을 찾지 못하면 ./, 확장자, 대소문자를 확인합니다. default로 내보낸 값을 중괄호 안에 넣거나 named export를 중괄호 없이 가져오면 형식이 맞지 않습니다.

함수와 객체의 기본 개념은 MDN 함수 안내MDN 객체 안내, 모듈 문법은 MDN 모듈 안내에서 확인할 수 있습니다. 실행 전에는 export 이름, import 경로, 호출 인수, 반환값 사용 위치를 차례로 점검하세요.

직접 실습: 영수증 객체를 반환하는 함수

기존 두 파일의 역할을 유지하면서 createReceipt(product, quantity)를 named export로 추가합니다. 상품 이름, 수량, 합계를 담은 새 객체를 반환하세요. 단가 계산은 이미 만든 calculatePrice에 맡깁니다. 입력은 숫자 단가와 0 이상의 정수 수량으로 제한합니다.

export function createReceipt(product, quantity) {
  return {
    name: product.name,
    quantity,
    total: calculatePrice(product.price, quantity)
  };
}

return { ... }는 객체를 반환합니다. 같은 값을 담았어도 호출마다 새 객체이며, 원래 상품은 변경하지 않습니다. const coffee는 이름 재대입을 막지만 coffee.price = 5000 같은 속성 변경까지 막지는 않습니다. 그래서 실습에서는 입력을 읽기만 하고 영수증을 새로 만듭니다.

ZIP 폴더에서 node starter/verify.mjs로 반환값을 점검하고 node complete/app.mjs로 출력하세요. node complete/verify.mjs는 정상 수량·0개 주문·새 객체·원본 보존을 확인합니다. Node.js ES 모듈 공식 문서의 규칙에 따라 .mjs와 상대 경로 확장자를 사용합니다.

시작 코드와 완성 코드 ZIP 내려받기

실습 파일

파일 경로를 확인하고 같은 프로젝트 안에 저장하세요. 이미지 등 소스 목록에 없는 파일과 실행 안내는 실습 ZIP에 포함되어 있습니다.

함수의 입력과 반환값을 정리하고 모듈 사이에서 데이터를 전달합니다. 수정 전은 시작 상태, 수정 후는 비교할 완성 상태입니다.

수정 전

app.mjs

import createProduct, { createReceipt, describeProduct } from './product.mjs';

const coffee = createProduct('드립 커피', 4500);
const receipt = createReceipt(coffee, 2);
console.log(describeProduct(coffee));
console.log(`수량: ${receipt.quantity}`);
console.log(`합계: ${receipt.total}원`);

product.mjs

export function calculatePrice(price, quantity) {
  return price * quantity;
}

export function describeProduct(product) {
  return `${product.name}: ${product.price}원`;
}

export default function createProduct(name, price) {
  return { name, price };
}

export function createReceipt(product, quantity) {
  return undefined;
}

verify.mjs

import assert from 'node:assert/strict';
import createProduct, {
  calculatePrice,
  describeProduct,
  createReceipt,
} from './product.mjs';

const coffee = createProduct('드립 커피', 4500);
assert.deepEqual(coffee, { name: '드립 커피', price: 4500 });
assert.equal(calculatePrice(4500, 2), 9000);
assert.equal(describeProduct(coffee), '드립 커피: 4500원');
assert.deepEqual(createReceipt(coffee, 2), {
  name: '드립 커피',
  quantity: 2,
  total: 9000,
});
assert.deepEqual(createReceipt(coffee, 0), {
  name: '드립 커피',
  quantity: 0,
  total: 0,
});
assert.notEqual(createReceipt(coffee, 2), createReceipt(coffee, 2));
assert.deepEqual(coffee, { name: '드립 커피', price: 4500 });
assert.notEqual(coffee, createProduct('드립 커피', 4500));
console.log('함수 반환값, named/default import, 0개 주문, 새 객체 검증 통과');

수정 방법

app.mjs

이 파일은 변경하지 않습니다. 다른 파일을 수정하는 동안 기존 내용을 유지하세요.

product.mjs

product.mjs에서 아래 차이가 있는 위치를 수정하세요. -는 삭제할 줄, +는 추가할 줄이며 표시는 실제 코드에 넣지 않습니다.

함수의 입력과 반환값을 정리하고 모듈 사이에서 데이터를 전달합니다.

--- 수정 전/product.mjs
+++ 수정 후/product.mjs
@@ -11,5 +11,9 @@
 }
 
 export function createReceipt(product, quantity) {
-  return undefined;
+  return {
+    name: product.name,
+    quantity,
+    total: calculatePrice(product.price, quantity),
+  };
 }

수정 후 탭의 전체 파일과 비교한 뒤 본문의 확인 절차를 실행하세요.

verify.mjs

이 파일은 변경하지 않습니다. 다른 파일을 수정하는 동안 기존 내용을 유지하세요.

수정 후

app.mjs

import createProduct, { createReceipt, describeProduct } from './product.mjs';

const coffee = createProduct('드립 커피', 4500);
const receipt = createReceipt(coffee, 2);
console.log(describeProduct(coffee));
console.log(`수량: ${receipt.quantity}`);
console.log(`합계: ${receipt.total}원`);

product.mjs

export function calculatePrice(price, quantity) {
  return price * quantity;
}

export function describeProduct(product) {
  return `${product.name}: ${product.price}원`;
}

export default function createProduct(name, price) {
  return { name, price };
}

export function createReceipt(product, quantity) {
  return {
    name: product.name,
    quantity,
    total: calculatePrice(product.price, quantity),
  };
}

verify.mjs

import assert from 'node:assert/strict';
import createProduct, {
  calculatePrice,
  describeProduct,
  createReceipt,
} from './product.mjs';

const coffee = createProduct('드립 커피', 4500);
assert.deepEqual(coffee, { name: '드립 커피', price: 4500 });
assert.equal(calculatePrice(4500, 2), 9000);
assert.equal(describeProduct(coffee), '드립 커피: 4500원');
assert.deepEqual(createReceipt(coffee, 2), {
  name: '드립 커피',
  quantity: 2,
  total: 9000,
});
assert.deepEqual(createReceipt(coffee, 0), {
  name: '드립 커피',
  quantity: 0,
  total: 0,
});
assert.notEqual(createReceipt(coffee, 2), createReceipt(coffee, 2));
assert.deepEqual(coffee, { name: '드립 커피', price: 4500 });
assert.notEqual(coffee, createProduct('드립 커피', 4500));
console.log('함수 반환값, named/default import, 0개 주문, 새 객체 검증 통과');

이 글이 도움이 되었나요?

조회 중

JavaScript 학습 순서

필수 13개 · 전체 14개

읽음 기록 관리

전체 과정 목차 (14개)
  1. 필수 학습 · JavaScript 조건문과 반복문: 변수 값의 흐름부터 추적하기
  2. 필수 학습 · JavaScript 함수와 객체, import export로 모듈 나누기 현재 글
  3. 필수 학습 · JavaScript 배열 메서드: map filter forEach reduce 차이
  4. 필수 학습 · JavaScript 객체 참조와 불변 갱신: 중첩 객체를 안전하게 바꾸기
  5. 필수 학습 · JavaScript reduce 사용법: 배열 누적 계산을 이해하는 기준
  6. 필수 학습 · JavaScript DOM 폼 만들기: 입력 검증과 접근성 처리
  7. 필수 학습 · JavaScript 투두리스트 만들기: 상태와 이벤트 위임으로 완성하기
  8. 필수 학습 · JavaScript URLSearchParams 사용법: URL 파라미터 읽고 수정하기
  9. 필수 학습 · JavaScript Date UTC KST 차이: 시간대 변환 기준 잡기
  10. 필수 학습 · JavaScript 실행 컨텍스트 기준: 스코프 호이스팅 클로저 연결하기
  11. 필수 학습 · JavaScript fetch 오류 처리: Promise부터 404까지
  12. 필수 학습 · JavaScript 이벤트 루프: Promise와 await 실행 순서 추적하기
  13. 필수 학습 · JavaScript 고급 비동기: AbortController와 최신 요청 경쟁 제어
  14. 선택 참고 · GSAP이 처음일 때 기초 사용법: 설치부터 기본 애니메이션까지

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기