들어가며
어느 정도 기능 구현이 완성된 프로젝트를 살펴보며 리팩토링을 할 만한 부분이 있나 찾던 중이었습니다. 그때 눈에 띄었던 건, 컴포넌트마다 export를 하는 방식이 다양하다는 사실이었어요. 사실 이 방식을 정하는 건 저에게 일종의 관성 같은 거였습니다. 페이지 역할을 하는 컴포넌트는 default export, 나머지는 named export로 나누어 썼었죠. 처음에 책이나 온라인 강의에서 배울 때에도 그렇게 배우는 경우가 많았고, '지속 가능한 코드'라는 관점에서는 export 방식을 고민해본 경험이 적었기 때문입니다.
이 글에서는 React + TypeScript 프로젝트에서 default export와 named export를 언제, 어떤 기준으로 선택할지 정리해봅니다. 특히 라우팅용 페이지 컴포넌트와 재사용 가능한 UI, hooks, utils 사이에서 어떤 방식을 쓰면 협업과 유지보수에 유리한지 이야기하려고 합니다.
React 공식 문서에서는 App 같은 최상위 엔트리 컴포넌트를 default export로 다루는 예시를 많이 보여줍니다. 하지만 실제 프로젝트에서는 그보다 훨씬 다양한 모듈들이 섞이게 되죠. 페이지 컴포넌트, UI 컴포넌트, hooks, utils, 타입 정의까지 각각의 성격에 맞는 export 전략이 필요합니다.
고민의 시작
위에서 잠깐 언급했듯, 페이지나 화면 단위의 컴포넌트는 default export로 내보내는 게 당연하다고 생각했습니다. 파일의 이름과 페이지(화면) 컴포넌트의 이름을 통일해주기 위해서 말이죠. 하지만 협업의 관점이라면 좀 더 생각해볼 문제가 생겼습니다.
자유도에서 오는 혼란
일단, 자유도가 생기며 혼란이 생길 위험성이 있었습니다. default export를 쓰면 import를 할 때 해당 요소의 이름을 마음대로 바꿀 수 있습니다. 원하는 이름으로 바꿀 수 있기 때문에 해당 파일만 보면 이름도 직관적이게 보이니 장점으로 느껴질 수 있습니다. 하지만 팀 프로젝트에서는 이 특징이 혼란을 가중시킬 수 있겠다는 생각이 들었습니다. 예를 들어, 같은 컴포넌트인데도 누구는 UserProfile로, 누구는 Profile로, 누구는 UserCard로 import를 하는 상황이 생길 수 있는 것입니다.
일관성의 부재
다음은 일관성 문제였습니다. 컴포넌트마다 export의 방식이 다르다 보니 명확한 기준이 필요했습니다. 명확한 기준이 존재하지 않다면 매번 다른 파일을 열어보며 export 컨벤션을 확인해야겠죠.
위 문제가 걱정된다면 무조건 named export로 통일하면 되는 것 아니냐고 생각하시는 분들이 계실지도 모르겠습니다. 하지만 기술적인 제약이나 프레임워크의 규칙 때문에 반드시 Default Export를 써야 하는 예외 상황도 존재합니다.
Default Export와 Named Export의 비교
두 방식의 특징을 표로 정리하면 다음과 같습니다.
| 항목 | Default Export | Named Export |
| 파일당 개수 | 하나만 가능 | 여러개 가능 |
| import 시 이름 | 자유롭게 별칭 사용 가능 | export된 이름과 동일하게 사용 |
| IDE 자동완성 | 상대적으로 덜 명시적 | 이름 기반 자동완성이 잘 동작 |
| 협업 시 일관성 | 같은 컴포넌트를 여러 이름으로 부를 수 있음 | 팀 컨벤션만 정하면 이름이 고정됨 |
| Tree Shaking | 패턴에 따라 불리해질 수 있음 | 정적 분석에 유리하여 안정적 |
Default Export를 써야 하는 경우
프레임워크의 규약이나 특정 라이브러리의 설계 방식에 따라 Default Export가 강제되거나 권장되는 상황이 있습니다.
프레임워크의 규칙 (Next.js)
Next.js의 경우, pages 또는 app 디렉토리 내의 페이지 컴포넌트는 프레임워크가 이를 인식하여 라우팅 포인트로 삼기 위해 반드시 default export를 사용해야 합니다.
// app/dashboard/page.tsx
export default function DashboardPage() {
return <div>대시보드</div>;
}
동적 임포트와 코드 분할 (React.lazy)
React에서 성능 최적화를 위해 사용하는 lazy() 함수는 기본적으로 default export된 컴포넌트를 반환하는 Promise를 기대하도록 설계되어 있습니다.
// App.tsx
import { lazy, Suspense } from 'react';
// 가장 일반적이고 깔끔한 방식
const Dashboard = lazy(() => import('./pages/Dashboard'));
만약 프로젝트의 모든 컴포넌트를 Named Export로 통일하기로 결정했다면 어떨까요? 다행히 해결책이 있습니다. import()가 반환하는 모듈 객체를 조작하여 lazy가 원하는 형태로 맞춰주면 됩니다.
// pages/Dashboard.tsx - Named Export 사용 시
export const Dashboard = () => {
return <div>대시보드</div>;
};
// App.tsx - Named Export를 유지하면서 lazy loading을 구현하는 방법
const Dashboard = lazy(() =>
import('./pages/Dashboard').then(module => ({ default: module.Dashboard }))
);
위의 .then(module => ({ default: module.Dashboard })) 구문은 Named로 내보낸 컴포넌트를 default 속성을 가진 객체로 가공해주는 작업입니다. 약간의 추가 코드가 필요하지만, ‘프로젝트 전반의 Named Export 일관성 유지’와 동시에 ‘코드 분할을 통한 성능 최적화’를 하고싶은 분이라면 해결책이 될 것입니다.
모듈의 명확한 정체성 표현
기술적인 제약 외에도, 한 파일이 오직 하나의 주요 컴포넌트만을 담당하고 파일명과 컴포넌트명이 완벽히 일치할 때는 Default Export가 해당 모듈의 정체성을 가장 직관적으로 나타내기도 합니다. 이 컴포넌트가 이 파일의 '주인공'임을 명시적으로 선언하는 것이죠.
Named Export의 장점
하지만 Named Export도 확실한 장점이 있습니다.
IDE 자동완성의 편리함
아까 짚어본 Default Export의 특징 중 이름을 마음대로 정할 수 있다는 점이 있었는데요. Named Export를 사용하면 import할 때 정확한 이름이 자동으로 제안되기 때문에 import문을 작성할 때 IDE가 정확한 이름을 자동완성해줍니다.
명확한 의존성 파악
Named Export를 사용하면 어떤 것들을 import하는지 한눈에 보여서 코드의 의존성을 파악하기 쉽습니다.
import { useUserData, useAuth, useFriends } from '../hooks';
이렇게 보면 이 컴포넌트가 사용자 데이터, 인증, 친구 목록 관련 기능을 사용한다는 걸 바로 알 수 있겠죠. 반면 Default Export를 사용했다면 여러 파일에서 각각 import해야 하므로 의존성 파악이 상대적으로 조금 어려울 수 있습니다.
// Default Export 사용 시
import UserDataHook from '../hooks/useUserData';
import AuthHook from '../hooks/useAuth';
import FriendsHook from '../hooks/useFriends';
CommonJS와의 호환성
Named Export는 CommonJS 환경에서도 일관되게 동작합니다. Default Export는 과거 Babel이나 TypeScript의 트랜스파일 과정에서 module.exports.default로 변환되어 .default를 통해 접근해야 하는 경우가 있었습니다. 이는 ES 모듈의 default export와 CommonJS의 module.exports가 완벽하게 1:1 매칭되지 않기 때문인데요. 번들러가 이를 호환시키기 위해 임의로 .default라는 속성을 가진 객체를 만들기 때문입니다
// CommonJS에서 default export 사용 시 (과거 또는 일부 환경)
const Component = require('./Component').default; // .default가 필요할 수 있음
// Named export는 직관적
const { Component } = require('./Component');
다만 최신 Node.js(v12 이상)에서는 ES 모듈을 네이티브로 지원하며, package.json에 "type": "module"을 설정하면 .default 없이도 정상적으로 작동합니다. 하지만 레거시 코드베이스나 특정 빌드 도구를 사용하는 환경에서는 여전히 이런 차이가 두드러질 수 있으므로, Named Export를 사용하면 이러한 호환성 문제를 원천적으로 피할 수 있습니다.
Tree Shaking 성능에 대한 오해와 진실
정리하면, Tree Shaking에서 중요한 건 default냐 named냐보다 '여러 기능을 어떻게 구조화해서 export하느냐'입니다.
Named Export가 Tree Shaking에 더 유리하다는 이야기를 많이 들어보셨을 겁니다. 저도 처음에는 단순히 "Default는 안 되고, Named는 된다"라고만 알고 있었어요. 하지만 현대적인 번들러의 동작 방식을 고려할 때, 이는 절반만 맞는 설명입니다.
결론부터 말씀드리면, Default Export 그 자체가 문제라기보다, 이를 사용하는 '패턴'이 Tree Shaking을 방해하는 경우가 많기 때문입니다.
단일 값 Export에서는 기술적인 차이는 거의 없습니다
파일 하나에 하나의 컴포넌트나 함수만 담아 내보낸다면 Named와 Default 사이의 Tree Shaking 성능 차이는 사실상 없습니다. 번들러는 import 구문을 보고 해당 모듈 전체의 사용 여부를 판단할 수 있기 때문입니다. 사용하지 않는 모듈은 통째로 제거(Dead Code Elimination)되므로, 이 경우에는 어떤 방식을 써도 결과는 동일합니다.
진짜 문제는 '객체로 묶인 포장지'에 있습니다
왜 전문가들은 여전히 Default Export가 Tree Shaking에 불리할 수 있다고 경고할까요? 그것은 여러 기능을 한 파일에 모으면서 "하나의 객체(Object)로 묶어서" 내보내는 안티 패턴 때문입니다.
❌ Tree Shaking이 누락될 수 있는 패턴 (Default Export Object)
// utils.ts
const add = (a, b) => a + b;
const sub = (a, b) => a - b;
export default { add, sub }; // 하나의 '객체'라는 박스에 담아 내보냄
이 방식이 위험한 이유는 자바스크립트 객체의 동적인 특성 때문입니다. 번들러 입장에서 export default { ... }는 런타임에 평가되는 하나의 큰 객체입니다. 설령 개발자가 utils.add만 사용하더라도, 번들러는 "나머지 sub 함수가 나중에 동적으로 호출(예: utils['sub'])될지도 몰라"라고 보수적으로 판단할 수 있습니다. 결과적으로 쓰지 않는 코드까지 번들에 포함될 확률이 높아집니다.
✅ 정적 분석이 용이한 패턴 (Named Export)
반면, 아래와 같이 각각 내보내면 상황이 달라집니다.
// utils.ts
export const add = (a, b) => a + b;
export const sub = (a, b) => a - b;
ESM(ES Modules) 명세에서 Named Export는 빌드 타임에 확정되는 정적인 식별자들입니다. 번들러는 "사용자가 add만 가져갔고 sub는 어디에서도 참조되지 않네?"라고 확실히 판단하여 sub를 번들에서 안전하게 제거할 수 있습니다.
즉, 한 모듈에 여러 유틸을 담을 때는 export default { ... } 대신 각 함수를 named export로 내보내는 것이 번들러가 미사용 코드를 제거하기에 가장 안전합니다.
Tree Shaking은 방식의 차이라기보다 구조화의 차이입니다. Default Export를 쓰더라도 파일당 하나씩만 내보낸다면 성능 손해는 없습니다. 하지만 한 모듈에서 여러 기능을 제공해야 한다면, 번들러가 미사용 코드를 100% 확신하고 깎아낼 수 있도록 Named Export를 사용하는 것이 가장 안전하고 효과적인 전략입니다.
React 공식 문서의 관점과 실무 확장
React 공식 문서에서는 보통 예제에서 root App 컴포넌트와 페이지 성격의 컴포넌트를 default export로, 그 안에서만 쓰이는 내부 컴포넌트는 같은 파일 안에 정의하는 식으로 소개합니다. 또한 하나의 파일에서 default export와 named export를 섞어서 쓸 수 있고, 팀에 따라 한 스타일만 쓰거나 둘을 섞지 않는 전략을 택하는 것도 권장합니다.
이 글에서는 그 기본 원칙 위에, Next.js 파일 기반 라우팅, React.lazy, Tree shaking, CommonJS 호환성까지 고려했을 때 어떤 컨벤션이 실무에서 유지보수에 유리한지 한 단계 더 확장해서 정리해 봤습니다.
제가 정한 export 기준
정해진 정답은 없지만, 팀 프로젝트의 일관성을 위해 제가 실무에서 적용하고 있는 기준은 다음과 같습니다. 핵심은 프레임워크와의 규약이 필요한 곳에는 Default를, 그 외의 모든 로직에는 Named를 사용하는 것입니다.
페이지 엔트리 컴포넌트의 경우, Default Export를 씁니다
Next.js의 라우팅 시스템이나 React.lazy를 활용한 코드 분할처럼 프레임워크가 특정 형식을 요구할 때만 사용합니다. 이는 해당 파일이 독립적인 하나의 페이지라는 정체성을 명확히 해줍니다. 라우팅 대상이 되거나 최상위 Provider에서 직접 마운트되는 컴포넌트들이 여기에 해당하죠.
그 외 모든 모듈에는 Named Export를 씁니다
재사용 컴포넌트, 커스텀 훅, 유틸 함수, 타입 등은 모두 Named Export를 기본으로 합니다. 선언부에서 바로 export를 붙여 별도의 구문 없이 간결하게 작성합니다.
팀 컨벤션 체크리스트
팀에서 export 컨벤션을 정할 때 체크해볼 질문들입니다.
- 페이지/엔트리 컴포넌트는 모두 default export로 통일할 것인가?
- 재사용 가능한 컴포넌트·hooks·utils는 원칙적으로 named export로 할 것인가?
- 한 파일에서 default와 named를 섞어 쓸지, 아니면 한 스타일만 허용할지?
- export default { ... } 같은 패턴은 코드 리뷰에서 금지할 것인가?
- CommonJS와 혼재된 레거시 모듈이 있다면, 어떤 쪽을 기준으로 맞출 것인가?
- 프레임워크의 규약(Next.js, React Router 등)으로 인한 예외 케이스는 문서화되어 있는가?
마치며
이렇게 명확한 기준을 정해두니 코드베이스의 일관성이 훨씬 좋아졌습니다. 이전에는 매번 이건 어떻게 export했더라?를 고민하며 파일을 열어보곤 했지만, 지금은 모듈의 성격만 보고도 망설임 없이 코드를 작성할 수 있게 되었습니다. import문만 봐도 해당 모듈의 역할을 직관적으로 파악할 수 있다는 점도 큰 수확이었고요.
개인적으로는 일관성이라는 관점에서 Named Export로 다 통일하고 싶다는 생각도 들었습니다. 하지만 Next.js나 React.lazy처럼 프레임워크 수준에서 규약으로 정해진 Default Export를 무시할 수는 없었죠. 결국 중요한 것은 용도에 맞는 전략적 선택이라고 생각합니다. 규약을 따라야 할 곳과 기술적 최적화 및 유지보수성을 챙겨야 할 곳을 명확히 구분하는 것이죠.
단순히 어떤 방식이 더 좋은지를 따지기보다, 팀 내에서 합의된 기준을 세우는 것만으로도 불필요한 고민의 시간을 줄이고 본질적인 로직 구현에 더 집중할 수 있게 되었습니다. 여러분의 프로젝트에서는 어떤 기준을 가지고 export 방식을 선택하고 계신가요?
ref:
https://dev.to/zysd836/tree-shaking-friendly-importexport-patterns-for-modern-frontend-projects-g68
Tree-Shaking Friendly Import/Export Patterns for Modern Frontend Projects
In modern frontend development, tree shaking is essential for keeping bundles lean. Yet many...
dev.to
https://react.dev/learn/importing-and-exporting-components
Importing and Exporting Components – React
The library for web and native user interfaces
react.dev
https://tech.pxd.co.kr/post/named-export-vs-default-export-27
pxd XE Blog | named export vs default export
어떤 방식을 사용해야 할까?
tech.pxd.co.kr
[JS] Named export와 Default export, 둘 중에 뭘 써야할까?
개요 프로젝트를 세팅하거나 새로운 컴포넌트를 만들 때마다 아주 사소하지만, 매번 마주치는 갈림길이 있다. "이거 export default로 내보낼까? 아니면 그냥 export로 내보낼까?" 사실 둘 중 뭘 써도
noguen.com