Next.js 순환참조 에러 해결하기

Dev
2025-03-20

Column is not defined
Header 컴포넌트에 언어 선택 드롭다운을 붙이려던 그때, 오류가 발생했다!
Column이 정의되지 않은 상태에서 사용되었다는 오류였지만 분명 import를 했는데도 이런 문제가 생겼다.

결론적으로는 모듈의 export 방식에 대한 이해 부족이 문제였다.
지금까지 아무 생각 없이 export default ~~~를 사용해왔는데, 이 방식이 순환 참조를 유발하고 있었다.
그렇다면 Named ExportDefault Export의 차이는 무엇이고, 어떤 상황에서 각각을 사용해야 할까?


Named Export vs Default Export

Named Export는 한 파일에서 여러 모듈을 한꺼번에 export할 수 있다.
이 경우 import할 때는 반드시 중괄호 {} 를 사용해야 하며, export한 이름과 동일한 이름으로 가져와야 한다.

반면, Default Export는 한 파일당 하나의 모듈만 export할 수 있다. 중괄호 없이 import할 수 있으며, 가져오는 쪽에서 원하는 이름으로 사용할 수 있다는 유연함이 있다.

// Named Export 예시
export function Button() {}
import { Button } from './Button'; // 이름이 정확히 일치해야 함
 
// Default Export 예시
import Banana from './withAuth'; // 실제 값은 withAuth지만 이름을 Banana로 사용 가능

📌 주의할 점
Default export한 값을 named import 방식({})으로 가져오려 하면 에러가 발생한다.

예시 화면 hoc 캡쳐본 hoc 폴더 내의 index.tsx 캡쳐본

언제 어떤 export 방식을 쓰면 좋을까?

Default Export는 보편적으로 한 파일에서 하나의 컴포넌트만 export 할 때 적합하다. import할 때 이름을 자유롭게 정할 수 있다는 장점이 있다.

Named Export는 여러 유틸 함수, HOC 등 여러 개의 모듈을 하나의 파일에서 묶어 export할 때 유용하다. 특히 index.ts에 공통 모듈을 정리해 모듈 접근성을 높일 수 있다는 장점이 있다.

사실 Default Export를 여러 파일에서 사용할 경우, import 문이 늘어나서 가독성이 떨어질 수 있다는 단점이 있다. 아래와 같이 한 줄에 import가 안 되고, 중복된 경로가 반복되면 가독성이 떨어지고 유지보수가 어려워진다. Default Export 캡쳐본

구분Default ExportNamed Export
정의한 파일당 하나의 값을 export여러 값을 동시에 export 가능
import 방식중괄호 없이 자유롭게 이름 지정 가능중괄호 사용, export한 이름과 동일하게 import해야 함
사용 적합 상황핵심 기능 하나만 제공하는 파일여러 유틸, 컴포넌트를 묶어 export할 때
장점import 이름을 자유롭게 지정 가능하나의 파일에서 여러 값을 한꺼번에 가져올 수 있음
단점import 경로가 많아질 경우 가독성 저하이름이 일치해야 하므로 실수할 가능성 있음
예시 사용 시점export default Componentexport { A, B }

순환 참조는 어떻게 발생했을까?

문제의 원인은 다음과 같은 구조에서 발생했다:

📌 순환 참조 흐름
Dropdown → Column (import { Column } from '@/styles/layouts/Layout')
Layout.tsx → withDefaultProps (import { withDefaultProps } from '@/hoc')
hoc/index.tsx → withLayout (import { withLayout } from '@/hoc/withLayout')
withLayout.tsx → BaseLayout (import { BaseLayout } from '@/components/layouts')
BaseLayout.tsx → Column (import { Column } from '@/styles/layouts/Layout')
➡️ 결국 Column이 BaseLayout을 참조하고, BaseLayout이 다시 Column을 참조하는 순환 참조 발생!

현재 hoc/index.tsx에서 withDefaultProps, withAuth, withLayout을 한꺼번에 export하고 있기 때문에, 어느 한 곳에서 withDefaultProps만 import하더라도 hoc 폴더 내부 전체가 로드된다. 즉, withDefaultProps를 가져오는 것만으로 withLayout도 같이 불려오면서, 결과적으로 withLayout.tsx 내부의 BaseLayout이 불려와 순환 참조가 발생한 것이었다!
이처럼 하나의 모듈이 다른 모듈을 참조하고, 다시 자신이 참조되는 구조가 형성되면 런타임 시 참조 에러가 발생할 수 있다.

🔧 해결책
hoc/index.tsx를 통해 한꺼번에 export하지 않고, 필요한 곳에서 개별적으로 import하도록 변경하여 순환 참조 에러를 해결할 수 있었다.

리액트 공식문서에서도 설명하듯, export 방식에 따라 import 방식이 달라지며, 컴포넌트 명명 규칙과 구조 관리가 중요하다는 점을 꼭 기억하자! 나중에 디버깅과 유지보수를 편하게 하기 위해서는 export 방식을 의도적으로 잘 설계하는 것이 중요하다.
이번 경험을 통해 export 방식의 선택과 순환 참조를 예방하는 설계의 중요성을 제대로 체감했다✨