개요
프로젝트를 세팅하거나 새로운 컴포넌트를 만들 때마다 아주 사소하지만, 매번 마주치는 갈림길이 있다.
"이거 export default로 내보낼까? 아니면 그냥 export로 내보낼까?"
사실 둘 중 뭘 써도 기능상으로는 전혀 문제가 없다. 어차피 import 해서 쓰면 그만이니까. 하지만 프로젝트 규모가 커지고, 팀원들이 늘어나고, 성능 최적화(Tree Shaking)를 고민하기 시작하면 이 사소한 차이가 생각보다 큰 영향력을 불러온다.
결론부터 말하자면 나는 Named Export를 강력하게 선호한다. 그 이유를 이야기하기 전에, 우선 두 방식이 본질적으로 어떻게 다른지부터 깊게 파보자.
Named와 Default export란
두 방식의 차이는 단순히 중괄호 `{}`를 쓰냐 마냐의 문제가 아니다. 모듈이 값을 취급하는 방식 자체가 다르다.
Default Export
Default Export는 모듈당 단 하나만 존재할 수 있다. 말 그대로 그 파일의 기본값이자 주인공을 내보내는 것이다. 값을 내보낼 때 이름을 지정하지 않고 값 그 자체를 내보내기 때문에, 받는 쪽(Import)에서 이름을 마음대로 지어서 받을 수 있다. ▼
// user.js
const User = { name: 'Noguen' };
export default User; // 이름 없이 '값' 덩어리를 내보냄
// app.js
import Member from './user'; // 받는 사람이 'Member'라고 이름 지음 (OK)
Named Export
반면 Named Export는 모듈 하나에서 여러 개를 내보낼 수 있다. 가장 중요한 특징은 export name이 고정된다는 점이다. export할 때 "나는 A입니다"라고 이름표를 달고 나가기 때문에, import 할 때에도 반드시 그 이름표(A)를 확인하고 받아야 한다. ▼
// utils.js
export const A = (a, b) => a + b; // 'A'라는 이름표
export const B = (a, b) => a - b; // 'B'라는 이름표
// app.js
import { A, B } from './utils'; // 이름표 확인 필수
결정적 차이 : 이름 변경 (Aliasing)
Named Export를 쓰면서 이름을 바꾸고 싶다면? as 키워드를 써서 내가 이름을 바꿨다는 것을 명시해야 한다.
- Default: import 내맘대로 from ... (암묵적 변경, 위험함)
- Named: import { 원래이름 as 내맘대로 } from ... (명시적 변경, 안전함)
이 명시성의 차이가 바로 유지보수의 퀄리티를 가르는 핵심이다. 이제 본격적으로 왜 Named Export가 더 유리한지 따져보자.
Tree Shaking : 다이어트는 Named가 더 잘한다
웹 성능 최적화의 핵심 중 하나는 번들 사이즈를 줄이는 것이다. 이때 사용되지 않는 코드를 번들링 단계에서 제거하는 것을 트리 쉐이킹(Tree Shaking)이라고 한다. ▼

Named Export의 경우
Named Export는 내보내는 대상이 명확하다. 번들러(Webpack, Rollup 등) 입장에서 '아, 이 파일에서 A만 가져다 쓰는구나, B는 안 쓰네?' 라고 판단하기가 매우 쉽다. ▼
// utils.js
export const funcA = () => { ... }; // 사용됨
export const funcB = () => { ... }; // 사용 안 됨 -> 제거 대상 (Tree Shaking)
// app.js
import { funcA } from './utils';
Default Export의 경우
반면 Default Export는 객체 하나를 통째로 내보내는 형태가 많다. 물론 최신 번들러들이 똑똑해져서 정적 분석을 잘하긴 하지만, Named Export에 비해 트리 쉐이킹이 실패할 확률이 높다.
만약 하나의 거대한 객체로 묶어서 default로 내보낸다면, 번들러는 '이 객체의 프로퍼티 중 일부가 동적으로 사용될 수도 있지 않을까?' 라고 보수적으로 판단하여 사용하지 않는 함수까지 번들에 포함시킬 위험이 있다. ▼
// utils.js
const A = () => { ... };
const B = () => { ... };
export default { A, B }; // 객체로 묶임
// app.js
import utils from './utils';
utils.A(); // B는 안 쓰지만 번들에 포함될 가능성 높음
협업의 관점 : 이름 짓기
개발자들이 가장 힘들어하는 것 중 하나가 변수명 짓기다. Default Export는 이 고통을 import 하는 사람에게 떠넘긴다.
내 마음대로 이름 짓기 (Inconsistency)
Default Export로 내보낸 모듈은 가져올 때 이름을 내 마음대로 지을 수 있다. 이게 장점 같지만, 협업에서는 재앙이 될 수 있다. 같은 컴포넌트인데 누구는 `Text`이라 부르고, 누구는 `Label`이라 부른다면? 전체 코드를 검색(Grep)해서 찾기가 매우 어려워진다. ▼
// Text.js
export default function Text() {}
// A 개발자
import Text from './Text';
// B 개발자 (자기 마음대로 작명)
import Label from './Text';
"아니 도대체 누가 파일명에 떡하니 써있는걸 굳이굳이 이름을 바꿔가며 개발을 하나?"

솔직히 맞는 말이다. 파일 이름부터 어떤 컴포넌트인지가 떡하니 써있는데, 상식적으로 모두가 그게 컴포넌트 이름이란걸 알기 때문에 그걸 굳이 바꿔가면서까지 개발할 사람은 없다. 허나 변경될 수 있다는 점이 문제인 것이다. 강제성이 없는 순간, 생각치도 못한 이유로 이름은 바뀌게 된다. 해당 변수명이 좋지 않다고 생각해서 임시로 바꿨다거나, 이름을 가져올 때 단수형에서 복수형으로 바꿔서 `s`를 붙였는데 깜빡했다거나 하는 식으로 말이다. 그렇기에 뭐든간에 가능성을 만들어두지 않는게 좋다.
자동완성과 리팩토링 (IDE Support)
Named Export를 사용하면 IDE의 지원을 빵빵하게 받을 수 있다. `{`를 치는 순간 해당 모듈에서 내보낸 변수명들이 자동완성으로 뜬다.
또한, 컴포넌트 이름을 바꿀 때도 Named Export는 VS Code의 F2 (Rename Symbol) 기능을 쓰면 참조하고 있는 모든 파일의 import 구문까지 안전하게 바뀐다.
하지만 Default Export는 파일 내부 이름만 바뀌고, 이를 import 해서 쓰고 있는 다른 파일들의 변수명은 그대로 남아있는 경우가 많아 일일이 찾아다녀야 한다.
라이브러리 배포 관점 : CommonJS와의 호환성
만약 사내 라이브러리나 오픈소스 패키지를 만들어 배포한다면 더더욱 Named Export를 권장한다. Node.js 환경이나 구형 시스템에서는 여전히 CommonJS (require)를 사용한다. 이때 Default Export를 사용한 라이브러리를 가져다 쓰려고 하면 아주 못생긴 코드를 마주하게 된다. ▼
// Default Export로 배포된 패키지를 require로 쓸 때
const MyLibrary = require('my-library').default; // .default를 붙여야 함
Named Export를 사용하면 require를 쓰든 import를 쓰든 명확하게 해당 모듈을 가져올 수 있어 호환성 이슈가 적다.
... 물론 요즘은 require을 거의 안쓰긴 한다. 이 부분은 나의 Named Export 사랑의 뒷받침 정도에 해당하는 단락이긴 하다. 이유를 위한 이유 정도. 약간은 억지일 수 있는 부분이나 엄연히 불편한 부분 중 하나다.
예외 : Default Export를 써야 할 때
"아니 이정도면 Default Export는 왜 존재하는거?
처음부터 Named Export만 쓰게 하면 되잖아?"
맞다. 필자도 동의한다. 하지만 Default Export가 반드시 필요하거나, 사용하는 것이 훨씬 유리한 상황들이 분명 존재한다. 무조건적인 배척보다는 적재적소에 사용하는 것이 중요하다.
1. Code Splitting & Lazy Loading (동적 임포트)
React.lazy나 Next.js의 dynamic을 사용하여 코드 스플리팅을 구현할 때, Default Export는 선택이 아닌 필수다. React.lazy는 Promise를 반환하는 함수를 인자로 받는데, 이 Promise는 반드시 default 프로퍼티를 가진 모듈을 resolve 해야 한다는 규칙이 있다. ▼
// React.lazy는 기본적으로 default export를 찾도록 설계되어 있다.
const MyComponent = React.lazy(() => import('./MyComponent'));
만약 Named Export로 된 컴포넌트를 Lazy Load 하려면 아래처럼 억지로 변환해 주는 코드가 추가되어야 한다. 꽤나 번거롭다. ▼
// Named Export를 Lazy Load 하려면 이렇게 비틀어야 한다.
const MyComponent = React.lazy(() =>
import('./MyComponent').then(module => ({ default: module.MyComponent }))
);
2. 파일 기반 라우팅 (Next.js, Remix 등)
Next.js의 Pages Router나 App Router 같은 파일 기반 라우팅 시스템에서는 '이 파일이 렌더링 할 페이지의 진입점은 이것이다' 라고 프레임워크와 약속을 해야 한다. 프레임워크는 빌드 타임에 해당 파일을 읽고, export default로 내보내진 컴포넌트를 페이지의 메인 콘텐츠로 인식하여 라우팅을 연결한다. 여기서 Named Export를 쓰면 프레임워크가 진입점을 찾지 못해 에러가 발생한다. ▼
// pages/index.tsx (Next.js)
export default function HomePage() {
return <h1>여기가 메인 페이지입니다</h1>;
}
3. 단일 책임 원칙 (Single Responsibility)과 가독성
어떤 모듈이 명확하게 단 하나의 클래스나 단 하나의 컴포넌트만을 위해 존재할 때, Default Export는 이 파일의 핵심은 나라는 것을 명확히 드러낼 수 있다. 예를 들어 Styled-Components 같은 라이브러리를 보자. 우리는 보통 `import styled from 'styled-components';`라고 쓴다. 여기서 styled가 메인이고 나머지는 부가적인 것이라는 느낌을 직관적으로 준다. 만약 라이브러리의 진입점이 되는 메인 기능을 가져올 때도 {}를 써야 한다면 사용성 측면에서 피로감을 줄 수도 있다.
마치며
"일관성(Consistency)이 곧 생산성이다."
나는 기본적으로 모든 컴포넌트와 유틸리티 함수를 Named Export로 작성한다. 트리 쉐이킹 이점도 있지만, 팀원 모두가 같은 것을 같은 이름으로 부르는 것이 커뮤니케이션 비용을 줄이는 가장 확실한 방법이기 때문이다.
export default가 주는 이름을 바꿀 땐 별 생각이 없지만, 그렇게 하면 나중에 생각을 몇 배로 많이 하게 된다. 특별한 이유가 없다면 { } 안에 명확하게 이름을 담아 export하는 습관을 들여보자.
'Develop > Web' 카테고리의 다른 글
| [Web] 시맨틱 태그 적용 작업(작성중...) (0) | 2025.12.04 |
|---|---|
| [React][Issue] 간단한 알고리즘과 함께 말줄임 디테일 챙기기 (0) | 2025.12.01 |
| [React] Spread Attributes를 조심해야 하는 이유 (0) | 2025.11.26 |
| [Issue] Firefox에서 새로고침 시 Input 값이 초기화되지 않는 문제 해결하기 (1) | 2025.11.20 |
| [Web] div 수프를 끓이지 말아야 하는 이유 (0) | 2025.11.02 |