개발을 하다 보면 오류가 발생하는 건 당연지사인 일이고, 오류 메시지는 꽤나 친절하게 오류가 발생한 원인을 알려주기 때문에 오류 메시지와 출력되는 스택 트레이스를 따라가면 어느 정도 오류는 원인을 찾기 마련입니다 🧐
그러나 가끔 오류 메세지를 보고도 대체 어디서 발생한 녀석인고? 하게 되는 메시지들이 존재하는데..
아래 오류 코드가 바로 그런 녀석들 중 하나입니다.
Uncaught TypeError: Super expression must either be null or a function
Super expression이...null이라고...? 이게 뭔 소리람
이라는 반응이 이 오류 코드에 대한 저의 첫 반응이라는 건 비밀 아닌 비밀입니다..
이 녀석은 class 키워드를 사용하여 ES6 클래스 기반으로 컴포넌트를 생성하거나 객체를 상속받을 때, 부모 클래스(super)가 null이거나 함수가 아닐 경우에 발생합니다.
주원인으로는
1. 부모 클래스가 제대로 import 되지 않음
2. 모듈 간 순환참조 발생
3. react 버전 불일치
등이 있는데, 오늘 저희가 주로 볼 것은 바로 2번 순환참조입니다.
1. ESM의 기본 동작 방식
어, 순환참조 이야기하는데 갑자기 기본 동작 방식이요? 하실 수 있지만, 모듈이 어떻게 동작하는지 알아야 순환 참조가 무엇인지, 어떻게 발생하는 건지 알 수 있다고 생각하기 때문에 천천히 동작 방식부터 둘러보겠습니다.
아, 참고로 모듈이 뭔데요? 하시는 분이 계시다면 슬쩍 이 포스트를 추천해 봅니다.
2025.03.10 - [Language/JavaScript] - 모듈 시스템 [CommonJS, AMD, UMD, ESM]
모듈 시스템 [CommonJS, AMD, UMD, ESM]
1. 모듈이 필요한 이유소프트웨어가 커지면 발생하는 문제들소프트웨어는 시간이 지나면 지날수록 눈덩이처럼 커지고 복잡해집니다.초기에는 단순히 HTML, CSS, JavaScript 파일 한 두 개로 시작한
ciaom.tistory.com
아무튼, 다시 동작 방식 이야기로 돌아가 보겠습니다.
// index.html
<html>
<head>
<script type="module" src="/index.js"></script>
</head>
</html>
// index.js
import './a.js';
// a.js
import { printB } from './b.js';
import { sayHello2 } from './c.js';
console.log('module_a');
sayHello();
sayHello2();
// b.js
console.log('module_b');
export const printB = () => {
console.log('HI B');
}
// c.js
import { printB } from './b.js';
console.log('module_c');
export const printC = () => {
printB();
printB();
}
자, 파일이 좀 많지만 천천히 저와 함께 둘러봅시다.
만약 브라우저가 index.html을 실행할 때 콘솔에 어떤 순서로 출력될지 한 번 생각해 보세요.
.
.
.
한번 생각해 보셨나요?
콘솔에는 바로바로
module_b
module_c
module_a
HI B
HI B
HI B
의 결괏값이 출력이 됩니다.
아래에서 어떻게 위의 값이 출력이 되는지 순서를 작성해 보겠습니다.
※ 참고로 모듈을 평가한다 : 해당 파일의 코드를 실행한다의 의미입니다 ※
1. 브라우저가 index.html 실행 시 모듈로 불러온 index.js 실행
2. index.js 모듈 실행 시 a.js 모듈 평가
3. a.js에서 b.js 모듈 평가
4. b.js 모듈의 console.log('module_b'); 로그 출력
5. b.js에서 printB 함수를 내보내고 평가 종료
6. a.js에서 c.js 모듈 평가
(6). c.js에서 b.js 모듈을 가져올 때는 b.js 모듈이 다시 평가되지 않음
7. c.js 모듈의 console.log('module_c'); 로그 출력
8. c.js에서 printC 함수를 내보내고 평가 종료
9. a.js 모듈의 console.log('module_a'); 로그 출력
10. a.js 모듈에서 printB, printC 함수 호출 후 평가 종료
이렇게 총 10단계를 거쳐서 실행이 됩니다.
여기서 조금 주의 깊게 볼 것은 각 모듈은 최초 한 번만 평가된다는 점입니다. 그 예시로 b.js 모듈이 두 곳 (a.js, c.js)에서 import 하지만 한 번만 평가된다는 걸 보실 수 있습니다.
2. 순환 참조
순환 참조란, 이름 그대로 모듈 간에 원을 그리면서 서로 참조하는 경우를 말합니다. 영어로는 Circular dependancy.
A, B, C 순으로 모듈 의존성이 있는 와중에
A → B → C
이처럼 하나의 선, 혹은 트리 형태를 띄우기도 하지만
A → B → C → A
위와 같이 마지막 C모듈이 A 모듈을 참조하면 각 모듈의 참조가 서클을 생성하게 됩니다.
그러나 사실.. 자바스크립트 모듈시스템에서는 순환참조를 허용합니다.
그 예시로 아래 코드를 한 번 살펴보겠습니다.
// index.js
import './a.js';
// a.js
import { introduce } from './b.js';
export const NAME = 'Ciaom';
console.log('module_a');
introduce();
// b.js
import { NAME } from './a.js';
console.log('module_b');
export const introduce = () => {
console.log('Hi My name is', NAME);
};
a.js와 b.js 모듈은 서로를 참조하지만 실제로 돌려보면 아무런 문제 없이 출력됩니다.
위에서 설명했던 동작 방식을 떠올려본다면
module_b
module_a
Hi My name is Ciaom
으로 출력된다는 것까지 이어서 확인하실 수 있습니다.
Q. b.js에서 a.js 모듈을 가져올 때는 NAME 변수를 내보내지 않았는데, 왜 undefined가 아닌 이름이 제대로 출력되나요?
모든 모듈은 모듈 객체를 가지고 있습니다. 그렇기에 모듈이 내보내는 변수와 함수는 바로 이 모듈 객체에 저장이 됩니다.
export const introduce = () => {
console.log('Hi My name is', NAME);
};
해당 코드에서 NAME이라는 건, 사실 모듈 객체에서 값을 가져오는 과정입니다.
그렇기에 NAME이라는 값을 가져오는 과정은
1. index.js 모듈 실행 시 a.js 모듈 평가
2. a.js에서 b.js 모듈 평가
3. b.js에서 a.js 모듈 평가
(3) b.js에서 a.js 모듈을 가져올 때는 a.js 모듈이 다시 평가되지 않음
4. b.js 모듈의 console.log('module_b'); 로그 출력
5. b.js에서 introduce함수를 bModuleObject에 추가하고 평가 종료
6. a.js는 NAME 변수를 aModuleObject에 추가
7. a.js 모듈의 console.log('module_a'); 로그 출력
8. a.js 모듈에서 bModuleObject.introduce함수 호출 후 평가 종료
의 순서로 작동되기 때문에, NAME 변수의 값이 무사히 출력되는 것입니다.
그러나.... 물론 모든 코드가 위의 코드처럼 착착 맞게 작성되면 좋겠지만, 코드라는 게 내 마음처럼 돌아가지는 않습니다.
3. 문제 발생
위 순환참고 예시 코드에서 살짝만 어긋나 잘못 사용하게 되면 바로 문제가 발생하게 됩니다.
문제 발생 1
// index.js
import './a.js';
// a.js
import { introduce } from './b.js';
console.log('module_a');
introduce();
export const NAME = 'Ciaom'; ①
// b.js
import { NAME } from './a.js';
console.log('module_b');
export const introduce = () => {
console.log('Hi My name is', NAME);
};
혹시 위 코드랑 달라진 점을 발견하셨나요?
제가 표시를 해두기도 했지만, 이 코드는 ① 줄의 NAME 변수 선언을 introduce() 이후로 변경했을 뿐인 코드입니다.
다만 이렇게 되면, introduce 함수를 호출할 때 aModuleObject에 NAME 변수 값이 저장되어 있지 않아 오류가 발생하게 됩니다.
만약 ESM를 사용하신다면 위와 같은 경우를 크게 걱정하실 필요는 없습니다.
ESM에서는 순환참조 발생 시 Reference Error를 발생시키기 때문에 비교적 발견하기 쉽습니다.
다만 CommonJS는 Error가 아닌 빈 객체(undefined)를 발생시키기 때문에 순환 참조 오류를 발견하기 어려운 편입니다.
이는 웹팩도 동일하기 때문에 만약 CommonJS나 웹팩을 사용하는 프로젝트의 경우는 순환 참조 오류임을 빠르게 알아채는 게 중요 포인트라고 볼 수 있겠습니다.
문제 발생 2
// index.js
import './b.js'; ②
// a.js
import { introduce } from './b.js';
export const NAME = 'Ciaom';
console.log('module_a');
introduce();
// b.js
import { NAME } from './a.js';
console.log('module_b');
export const introduce = () => {
console.log('Hi My name is', NAME);
};
두 번째 경우입니다.
이번에는 index.js파일에서 ② a.js를 가져오지 않고 b.js를 가져오는 것 외에는 동일한 코드입니다.
1. index.js 모듈 실행 시 b.js 모듈 평가
2. b.js에서 a.js 모듈 평가
3. a.js에서 b.js 모듈 평가
(3) a.js에서 b.js 모듈을 가져올 때는 b.js 모듈이 다시 평가되지 않음
4. a.js는 NAME 변수를 aModuleObject에 추가
5. a.js 모듈의 console.log('module_a'); 로그 출력
6. a.js 모듈에서 bModuleObject.introduce 함수 호출 시도
→ 해당 함수가 존재하지 않아 오류 발생
이 문제 상황들에서 볼 수 있듯이, 순환 참조 오류를 발생시키지 않기 위해서는 어디서 모듈을 가져오는지, 어떤 순서로 모듈을 가져올 건지 등 모듈의 평가 순서가 중요한 요소입니다.
프로젝트를 하면서 코드 순서 좀 바꿨다고 오류가 해결된 경험이 문득 생각이 나네요..🙄
그 당시에는 이게 왜 되지..? 대체 왜?? 뭘 했다고?? 하며 두려움에 떨었는데, 앞으로는 이런 일 없이 순서를 잘 파악하고.. 오류를 없애봅시다..
4. 문제 해결 방법
순환참조를 해결하는 방법은 사실 대부분 위의 경우처럼 평가 순서만 잘 짜였다면 해결되곤 합니다.
하지만 Michel Weststrate의 글을 기반으로 한 번 해결 방안을 작성하도록 하겠습니다.
내부 모듈 패턴 (Internal module pattern)
간단하게 정의해 보자면 모듈의 평가 순서를 정의하는 파일을 만들고 모듈을 가져올 때는 항상 그 파일에서 가져온다 라는 방식이라 보면 되겠습니다.
이 패턴의 핵심은 index.js와 internal.js 파일입니다.
- internal.js : 프로젝트 전체의 모듈을 불러 모은 다음 전부 내보내는 역할
- 각 파일에서 다른 모듈을 직접적으로 불러오기 X. 다른 모듈은 반드시 internal.js 파일만 불러와서 사용
- index.js : 주요 시작점
- internal.js 파일에서 내보낸 모든 모듈을 불러온 뒤, 외부로 노출하고자 하는 것만 내보내기
위 예시에서 각 파일에서 a.js, b.js 등 다른 모듈을 직접적으로 불러오는 대신
// index.js
import { NAME, introduce } from './internal.js';
// internal.js
export * from './b.js';
export * from './a.js';
이렇게 한 단계 거쳐가는 파일을 생성해서 관리한다고 생각하시면 되겠습니다.
마무리
자바스크립트는 기본적으로 순환 참조를 허용하는 만큼 모든 순환 참조를 없애기 위해 수정할 필요는 없습니다.
오류가 발생했을 때 이게 순환 참조로 인한 오류임을 빠르게 파악하고, 대처할 수 있는 정도면 충분하다고 생각합니다.
참조해야 할 모듈이 한 두 개가 아닌 상황에서 모든 모듈을 internal.js 파일을 만들어서 집어넣을 수는 없으니까요.
그러나 공통적으로 자주 사용되는 모듈이라면 이를 관리할 수 있는 internal.js 파일을 만들어 모듈의 평가 순서를 정해주는 것만으로도 순환 참조를 해결할 수 있으니 적절히 사용하는 능력을 키우는 게 최선일 것 같습니다 🤔
출처
'Language > JavaScript' 카테고리의 다른 글
자바스크립트의 비동기와 콜백 (1) | 2025.04.27 |
---|---|
호이스팅(Hoisting) 이해하기 (0) | 2025.04.08 |
모듈 시스템 [CommonJS, AMD, UMD, ESM] (0) | 2025.03.10 |
나는 DOM이 무엇인지 알고 사용하고 있었을까? (0) | 2025.02.02 |