본문 바로가기
About IT/기술 및 언어

AI가 내게 Named Export를 쓰라고 한 이유 (feat. Next.js)

by yjin_fe 2025. 12. 31.

들어가며.

최근 '바이브 코딩'을 통해 AI와 페어 프로그래밍을 하던 중 흥미로운 경험을 했습니다. 여느 때처럼 컴포넌트를 만들고 AI에게 코드 리뷰를 받던 중, AI가 대뜸 'Named Export' 방식을 권장하더군요. 평소 유틸리티 함수 등은 Named Export를 쓰더라도, 컴포넌트는 습관적으로 Export Default를 사용해 왔기에 의문이 들었습니다.

 

"이건 단순한 취향 차이일까, 아니면 내가 모르는 기술적인 이유가 있는 걸까?"

 

Named Export의 장점은 어렴풋이 알고 있었지만, 지금까지는 굳이 바꿔야 할 필요성을 크게 느끼지 못했기 때문입니다. 이번 기회에 두 방식의 차이를 기술적 관점에서 살펴보고, Next.js 환경에서의 실전 전략까지 정리해 보려 합니다.

 

 

두 가지 방식, 무엇이 다른가.

자바스크립트 모듈 시스템(ESM)에는 크게 두 가지 내보내기 방식이 존재합니다.

1. Export Default

파일당 단 하나의 메인 모듈을 내보낼 때 사용합니다. 내보내는 쪽에서 이름을 정하지 않고, 가져오는(Import) 쪽에서 이름을 자유롭게 지정할 수 있다는 특징이 있습니다.

// Component.tsx
const Component = () => { ... }
export default Component;

// 사용하는 곳
import MyComponent from './Component'; // 이름을 마음대로 지정 가능 (MyComponent, AnyName 등)

2. Named Export 

한 파일에서 여러 개를 내보낼 수 있으며, import한 이름 (선언된 이름)을 그대로 사용해야 합니다. 즉, 내보내는 쪽에서 정한 이름이 강제됩니다.

// Component.tsx
export const Component = () => { ... }

// 사용하는 곳
import { Component } from './Component'; // 정해진 이름을 써야 함

 

리액트 초기에는 '파일 하나당 컴포넌트 하나'라는 직관적인 구조와 코드가 간결해 보인다는 이유로 export default가 널리 사랑받았습니다. 하지만 웹 개발 환경이 고도화되고 프로젝트 규모가 커지면서 상황이 조금 달라졌습니다.

 

 

Named Export의 장점은.

AI의 제안을 비롯하여 최근 많은 개발자가 Named Export를 선호하는 추세입니다. 그 이유는 단순히 '취향 차이' 때문만은 아닙니다. 실무에서 체감할 수 있는 확실한 기술적 이점들이 있기 때문입니다.

 

1. IDE와 도구 지원의 우수성 (DX)

개발자 경험(DX) 측면에서 Named Export는 압도적인 효율을 자랑합니다.

  • 자동 완성(Auto-import)의 정확도: 컴포넌트 이름의 일부만 입력해도 IDE가 정확한 경로를 찾아 import { ... } 구문을 자동으로 생성해 줍니다. Default Export는 파일명과 컴포넌트명이 다를 경우 자동 완성이 제대로 동작하지 않거나, 원치 않는 이름으로 import 되는 경우가 잦습니다.
  • 이름의 강제성 (Consistency): Default Export는 가져오는 사람이 import MyButton from './Button' 처럼 이름을 마음대로 지을 수 있습니다. 이는 팀원 A는 MyButton, 팀원 B는 Btn으로 부르는 등 코드의 파편화를 야기합니다. 반면 Named Export는 만든 사람이 지은 이름을 그대로 쓰도록 강제하여 프로젝트 전체의 일관성을 유지합니다.

2. 리팩토링의 용이성

유지보수를 하다 보면 함수나 컴포넌트의 이름을 바꿔야 할 때가 반드시 옵니다. 이때 두 방식의 차이는 극명합니다.

  • Named Export: IDE에서 심볼 이름 바꾸기(보통 F2키)를 이용하여 해당 컴포넌트를 import 해서 사용하는 프로젝트 내 모든 파일의 코드까지 한 번에, 그리고 안전하게 변경할 수 있습니다.
  • Default Export: 내보내는 변수명과 가져오는 변수명이 서로 독립적이기 때문에, IDE가 이 둘을 동일한 대상으로 인식하지 못할 때가 많습니다. 결국 수동으로 찾아다니며 수정해야 하는 번거로움이 발생하며, 이는 곧 휴먼 에러로 이어지기 쉽습니다.

3. Tree-shaking 성능 최적화

가장 중요한 기술적 차이는 빌드 결과물에 있습니다. 웹팩(Webpack)이나 터보팩(Turbopack) 같은 번들러는 사용하지 않는 코드를 제거하는 Tree-shaking 작업을 수행합니다.

 

[정적 접근 패턴]

현대 최신 번들러(Webpack 5, Vite, Rollup 등)는 매우 똑똑합니다. 정적 접근만 사용하는 경우라면, Default Export는 Named Export와 거의 동일하게 최적화됩니다.

// 정적 접근 - 두 방식 모두 Tree-shaking이 잘 됨
import formatters from './formatters';
formatters.formatCurrency(1000);

import { formatCurrency } from './formatters';
formatCurrency(1000);

 

[동적 접근 패턴]

하지만 동적 접근 패턴을 사용할 때는 상황이 달라집니다. 먼저 "정적 접근"과 "동적 접근"이 무엇인지 다시 살펴보겠습니다.

  • 정적 접근: 코드를 작성할 때 어떤 함수나 속성에 접근할지 명확하게 지정하는 방식
    formatters.formatCurrency(1000); // 코드에 함수 이름이 박혀 있음
    
  • 동적 접근: 변수나 문자열을 통해 런타임에 어떤 함수나 속성에 접근할지 결정하는 방식
    const functionName = 'formatCurrency';
    formatters[functionName](1000); // 런타임에서 결정됨

[동적 접근 패턴 예시: 설정 기반 폼 검증 시스템]

JSON Schema 기반 폼 생성이나 동적 필드 검증에서 흔히 사용되는 패턴입니다. 이 예시를 통해 동적 접근 패턴에서 Tree-Shaking이 어떻게 달라지는 지 알아보겠습니다. 먼저 Default Export 방식을 살펴보겠습니다.

// Default Export 사용 시
import validators from './validators';

const formConfig = {
  email: { validator: 'validateEmail' },
  phone: { validator: 'validatePhoneNumber' },
};

const formData = { ... };

Object.keys(formConfig).forEach((fieldName) => {
  const config = formConfig[fieldName];
  // 동적 접근
  const validator = validators[config.validator]; 
  validator(formData[fieldName]);
});

 

[사람의 관점 vs 번들러의 관점]

  • 사람이 보는 코드: `formConfig`를 보면 `validateEmail`과 `validatePhoneNumber`만 사용될 것을 알 수 있습니다. 코드를 읽는 사람은 "이 두 함수만 필요하구나"라고 유추할 수 있습니다.
  • 번들러가 보는 코드: 하지만 번들러는 다르게 봅니다. 번들러는 빌드 시점에 코드를 분석하므로, 런타임에 실행되는 값은 알 수 없습니다. `validators[config.validator]`에서 `config.validator`는 번들러 입장에서 단지 "어떤 문자열 값"일 뿐입니다. 이 문자열이 실제로 `validators` 객체의 함수명과 일치하는지, 아니면 사용자가 입력한 임의의 문자열인지 빌드 시점에는 알 수 없습니다.

따라서 번들러는 다음과 같이 판단합니다:

  • `config.validator`가 런타임에 'validateEmail'이 될 수도, 'validatePassword'가 될 수도, 심지어 '존재하지 않는 함수명'이 될 수도 있다고 가정합니다.
  • 어떤 함수가 실제로 호출될지는 런타임에 실행되어야만 알 수 있으므로, 안전을 위해 `validators` 객체 전체를 포함시킬 가능성이 높습니다.

이것이 Default Export에서 동적 접근 시 Tree-shaking이 어려운 이유입니다.

 

이번엔 Named Export의 경우를 보겠습니다. (사실 엄밀히 따지면 아래 코드는 동적 접근이 아닙니다. 객체를 이용해 동적 처리처럼 보이게 만들었습니다.)

// Named Export 사용
import { validateEmail, validatePhoneNumber } from './validators';

// ... config 및 data 생략 ...

// '사용할 함수'를 명시
const validatorMap = {
  validateEmail,
  validatePhoneNumber,
};

Object.keys(formConfig).forEach((fieldName) => {
  const config = formConfig[fieldName];
  // 동적 접근처럼 보이지만, validatorMap의 키는 정적으로 분석 가능
  const validator = validatorMap[config.validator]; 
  
  if (validator) {
    validator(formData[fieldName]);
  }
});

 

비슷해 보이는데, 왜 이 방식은 Tree-shaking이 잘 될까요?

 

  • import { ... }를 통해 사용할 함수가 명시적으로 선언되었습니다.
  • validatorMap 객체의 키(key)가 코드에 명시되어 있어, 번들러가 정적 분석을 할 수 있습니다.
  • 번들러는 "validatorMap의 키는 명확하고, 여기에 연결된 함수들은 이미 import 되어 있어 안전하다!"라고 판단합니다.
  • 결국 사용하지 않는(Import 하지 않은) 나머지 함수들은 번들에서 제거됩니다.

정리하면 다음과 같습니다:

  • Default Export: 동적 접근이 가능하지만, 번들러가 정적 분석을 하기 어려워 Tree-shaking이 어려워집니다. `config.validator`가 런타임에 결정되므로, 번들러는 안전을 위해 객체 전체를 포함시킬 가능성이 높습니다.
  • Named Export: 필요한 것만 명시적으로 가져오기 때문에, 동적 로직을 구현하더라도 사용하지 않는 코드는 확실하게 제거됩니다. Named Export + 객체 조합은 동적 유연성과 성능 최적화를 모두 잡는 좋은 패턴이 될 수 있습니다.

 

 

그럼에도 Default Export를 선택하는 이유는.

여기까지 읽으셨다면 "이제 모든 코드를 Named Export로 바꿔야지!"라고 생각하실 수도 있습니다. 하지만 현업의 수많은 프로젝트와 오픈소스 라이브러리들이 여전히 Default Export를 이용하는 데에는 그만한 이유가 있습니다. 기술은 언제나 트레이드오프(Trade-off)가 존재하기 때문입니다.

1. React.lazy와 코드 스플리팅의 편리함

리액트의 지연 로딩(React.lazy) 기능은 기본적으로 default export를 불러오도록 설계되어 있습니다.

// ✅ Default Export - 문법이 깔끔함
const LazyComponent = React.lazy(() => import('./MyComponent'));

// ⚠️ Named Export - 추가 변환 작업 필요
const LazyComponent = React.lazy(() =>
  import('./MyComponent').then((module) => ({ default: module.MyComponent })),
);

성능 최적화를 위해 코드 스플리팅이 필수적인 대규모 서비스에서는, export default가 주는 문법적 간결함이 유지보수 측면에서 큰 장점이 됩니다.

2.  단일 책임의 구조적 강제성

개발자의 인지적 관점에서 보통 "하나의 파일은 하나의 명확한 책임을 가진다"는 멘탈 모델이 깊게 자리 잡고 있습니다. 이는 객체지향의 단일 책임 원칙(Single Responsibility Principle)과도 맥락을 같이 합니다.

// Button.tsx
export default function Button() { ... }

파일명과 export가 1:1로 매칭되는 구조는 "이 파일은 오직 Button 컴포넌트를 위해 존재한다"는 의도를 명확히 전달합니다. 반면에, Named Export는 한 파일에 여러 export 있을 수 있어, 파일을 봐야 무엇이 있는지 확실히 알 수 있습니다. 물론 "한 파일 = 한 컴포넌트" 구조로 '약속'을 하고 작성할 수 있습니다. 대신 개발자의 주의력에 의존해야 하여 '휴먼 에러'를 발생시킬 여지가 있습니다. Default Export는 구조적으로 메인 컴포넌트가 무엇인지 강제합니다. 이러한 제약이 오히려 개발자의 인지적 부담을 줄여주는 역할을 할 수 있습니다.

3. 점진적 도입의 현실성

술적으  수해, 실에서는 팀의 합의와 기존 코드베이스의  시할 수 없습니다.

  • 마이그레이션 비용: 수 줄의 코드베이스를 한 번에 변경하는 것은 비적입니다. 각 파일을 수정하, import 문들을 모두 변경하, 테스트를  돌려야 합니다. 이  비 실적 이득이 명확하지 않다  작하기 어렵습니다.
  •  컨벤션의 편화: 일 일만 Named Export로 변경하면 게 될까요? 개발자는  "이 컴포넌트는 default, named인가?"를 인해야 합니다. 이는 오히려 생산성을 떨어뜨립니다.
  • 관성의 : 완벽한 코드보 일관된 코드 팀 생산성에 더 중요할 가 많습니다.  팀원이 Default Export에 익숙하, 이미 렇게 작성된 코드가 부분이라면, Named Export의 적 이점보다 기  유지가 더 합리적일 수 있습니다.

"기 "라 불리기도 하지만, 때로는 "안정성을 위한 선택"  있습니다. 특히 배포 일정이 촉박하거나, 팀 스가 제한적 황에서는 더욱 그렇습니다.

 

 

번외편: Next.js 15 실전 전략

그렇다면 Next.js 프레임워크를 이용하는 경우는 어떨까요? 초기 프로젝트를 세팅한다고 가정했을 때, 모든 파일을 Named Export로 사용하면 될까요? 아쉽게도 그렇지 않습니다.

반드시 Default Export를 써야 하는 경우

Next.js의 App Router 시스템은 파일 시스템 기반 라우팅을 사용합니다. 이때 프레임워크가 약속한 특정 파일들은 반드시 Default Export를 사용해야 합니다. (공식 문서: 라우팅 및 페이지)

  • page.tsx: 각 라우트의 UI를 정의하는 파일입니다. 반드시 default export가 필요합니다. Named Export 사용 시 TypeScript 타입 에러가 발생합니다. (공식 문서: page)
  • layout.tsx: 여러 페이지 간에 공유되는 UI를 정의하는 파일입니다. 반드시 default export가 필요합니다. Named Export 사용 시 TypeScript 타입 에러가 발생합니다. (공식 문서: layout)
  • template.tsx: layout과 유사하지만, 페이지 전환 시마다 새 인스턴스를 생성합니다. 반드시 default export가 필요합니다. Named Export 사용 시 빌드의 prerendering 단계에서 에러가 발생합니다. (공식 문서: template)
  • error.tsx: 에러 바운더리를 정의하는 파일입니다. 반드시 default export가 필요합니다. Named Export 사용 시 빌드 시점에서는 에러가 발생하지 않지만, 런타임 실행 시점에 "The default export is not a React Component" 에러가 발생합니다. (공식 문서: error)

Next.js는 빌드 시 해당 파일들을 라우트의 진입점으로 인식하고 렌더링하기 위해, "약속된 기본 내보내기(default export)"를 찾도록 설계되어 있습니다. 의도대로 사용하지 않게 되면, page.tsx와 layout.tsx는 TypeScript 타입 검증 단계에서, template.tsx는 prerendering 단계에서 빌드가 실패하며, error.tsx는 빌드는 성공하지만 런타임 실행 시점에 에러가 발생합니다. (공식 문서: 라우트 세그먼트)

 

실제 에러 예시

1. page.tsx 에러:

page.tsx에서 Named Export를 사용하면 다음과 같은 빌드 에러가 발생합니다.

Failed to compile.

app/page.tsx
Type error: Page "app/page.tsx" does not match the required types of a Next.js Page.
  "HomePage" is not a valid Page export field.

Next.js build worker exited with code: 1

이 에러는 Next.js가 page.tsx 파일에서 Default Export를 찾지 못했기 때문에 발생합니다.

 

2. template.tsx 에러:

template.tsx에서 Named Export를 사용하면 다음과 같은 빌드 에러가 발생합니다.

Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use client".

 

이 에러는 Named Export로 인해 Next.js가 올바른 컴포넌트를 찾지 못했기 때문입니다.

 

3. error.tsx 에러:

error.tsx에서 Named Export를 사용하면 다음과 같은 런타임 에러가 발생합니다.

 
The default export is not a React Component in "/error"

이 에러 메시지는 "/error 경로(즉, error.tsx 파일)에서 Default Export가 없거나 React 컴포넌트가 아니다"라는 의미입니다. Named Export만 있고 Default Export가 없기 때문에 Next.js가 에러 페이지 컴포넌트를 찾지 못해 발생합니다.

추천하는 Next.js 컨벤션

따라서 Next.js 프로젝트에서는 다음과 같은 컨벤션을 사용하는 편이 좋습니다.

  • 라우팅 파일 (page, layout 등): 프레임워크 규칙에 따라 Export Default를 사용합니다.
  • 일반 컴포넌트, Hooks, Utils: 트리쉐이킹 효율과 리팩토링 안정성을 위해 Named Export를 사용합니다.

 

마무리.

결국 중요한 것은 '무조건적인 정답'을 찾는 것이 아니라 '팀의 합의'라고 생각합니다.

 

AI가 제안했던 Named Export는 기술적으로 분명 매력적입니다. 하지만 기존의 관습인 Export Default가 주는 직관성과 편리함 또한 무시할 수 없는 가치입니다.

 

결국, 완전한 정답은 없는 것 같습니다. "우리 프로젝트는 Tree-shaking이 중요한가?", "우리 팀은 어떤 방식이 더 직관적인가?"를 먼저 고민하는 것이 첫 걸음이라 생각합니다. 기술적 이점과 팀의 생산성 사이에서 적절한 균형점을 찾는 것, 그것이 좀 더 팀 플레이어로 일하는 좋은 개발자의 모습이 아닐까 싶습니다.

p.s.

이 글에서 언급한 `template.tsx`와 `error.tsx`의 Named Export 제한 사항은 실제 빌드 및 실행 시 에러가 발생하는 것을 확인했지만, Next.js 공식 문서에서 명시적으로 언급한 내용을 찾지 못했습니다. 혹시 공식 문서나 다른 자료에서 관련 내용을 발견하셨다면 댓글로 알려주시면 감사하겠습니다.