자주 발생하는 자바스크립트 에러

🗓 2021-06-29

Image by Emile Perron from Unsplash

개발을 하다 보면 개발자의 실수로 생긴 코드 상의 문제로 뻘건 에러 메시지를 자주 만나게 됩니다. 개발 중에 버그가 발견된다면 다행이지만 실 서비스에서 에러 메시지가 출력된다면 개발자로써 손발이 오그라들고 식은땀이 솟구칠 겁니다. 자바스크립트는 런타임에서만 에러 메시지를 만날 수 있기 때문에 종종 이렇게 심각한 버그를 놓치기도 합니다. 이 부분에선 요즘 타입스크립트와 그밖에 많은 도구들이 도움을 주고 있죠.

뛰어난 개발자분이라면 에러 메시지를 보자마자 빠바박 문제를 바로 파악할 수 있겠지만 저 같은 평범한 개발자는 에러 메시지를 볼 때마다 당황스럽고 새롭습니다. 이런 분들을 위해 준비해봤습니다. 비교적 경력이 낮은 개발자분들에게 도움이 될 것으로 생각됩니다.

자바스크립트에서 에러는 TypeError, SyntaxError, EvalError, InternalError 등 크게 몇 가지의 타입으로 나뉘는데요. 에러 메시지는 브라우저별로 조금 다를 수 있고 심지어 구분되는 타입도 다를 수 있어요. 하지만 얼추 비슷합니다. 이 글에서는 자주 볼 수 있는 타입 세 가지만 다루겠습니다.

TypeError(타입 에러)

타입 에러는 아마도 개발 도중에 제일 많이 만나게 되는 에러일 것 같습니다. 변수나 인자가 참조하고 있는 인스턴스를 잘 못 사용할 때 발생하는 에러입니다. 코드에서 기대한 값이 변수에 들어있지 않을 때 즉 기대했던 타입의 값이 아닐 때 발생하는 오류입니다. 해당 변수가 참조하고 있는 인스턴스에 없는 멤버에 코드로 접근했다는 오류가 대부분입니다.

정적인 타입을 지원하는 언어였다면 IDE나 컴파일 타임에서 경고를 받을 수 있는 내용들이 주를 이룹니다. 자바스크립트도 타입스크립트를 사용하면 대부분 커버가 가능하죠.

TypeError: X is not a function

아마 이 에러 메시지는 자주 보셨을 거예요. 함수가 아닌 것을 실행하려고 할 때 발생하는 에러입니다. X는 함수가 아닌 다른 값이 들어 있을거에요. undefined인 경우도 많습니다.

TypeError: X.forEach is not a function
TypeError: X.map is not a function

그중에서도 특별히 자주 목격되는 에러인데요. 어디선가 전달받은 X를 배열로 생각하고 배열의 인스턴스 메서드들을 사용했지만 실제 X 변수에 담긴 값은 배열이 아니었던 거죠. X 변수가 참조하는 객체에 기대했던 메서드가 없을때 출력되는 에러 메시지입니다. 해당 객체에 저런 메서드가 없으니 undefined를 실행하려고 했겠죠. 배열이 아닌 유사 배열에서 배열의 메서드를 실행하려고 했을 수도 있구요. 리액트의 JSX 파트에서도 은근 자주 납니다. 리스트를 렌더링할때 말이죠.

nullundefined 같이 프로퍼티를 가질 수 없는 데이터 타입의 프로퍼티에 접근을 시도할 때는 조금 다른 에러 메시지를 출력합니다.

TypeError: Cannot read property ‘A’ of undefined
TypeError: Cannot read property ‘A’ of null

대상이 null이나 undefined 라면 데이터 필드에 접근할때 뿐 아니라 없는 메서드를 실행하려고 할때도 ~ is not function 에러메시지 대신 위 메시지를 출력합니다.

let mouse = null;

mouse.suspect; // TypeError: Cannot read property ‘suspect’ of null
mouse.end(); // TypeError: Cannot read property ‘end’ of null

이런 메시지를 만나게 되면 대상 프로퍼티 명을 검색해서 어떤 변수가 잘못된 값을 갖게 되었는지 찾아보겠죠. 그 변수는 null 이나 undefined 값을 갖고 있을 겁니다. 그 변수가 언제부터 잘못된 값을 갖게 되었는지를 따라가보면 쉽게 해결할 수 있을 겁니다.

그외 다른 TypeError들은 비등비등하게 목격되요. 그나마 아주 쬐끔 자주 보게되는 에러라면 “생성자가 아닙니다” 에러정도 겠네요.

TypeError: X is not a constructor

변수 X 가 생성자가 아닌 값을 갖고 있는데 new X() 이렇게 new 키워드를 사용하게 되면 에러 메시지가 출력됩니다. 추상 팩토리나 비슷한 구현을 할 때와 같이 생성자의 숨기고 대신 인스턴스를 만들어주는 코드에서 발생할 수 있습니다. 주로 밑단의 구조적인 코드일겁니다. 가벼운 실수로는 비슷한 이름의 변수를 잘못 사용할 수도 있고, 생성자가 담겨 변수에 다른 값을 덮어썼을 수도 있겠고요.

타입 에러의 일반적인 해결 방법은 스택 트레이스에서 위치를 찾은 다음에 관련된 변수가 어떤 값을 갖고 있나 확인해보고 언제부터 잘못된 값을 갖게 되었는지 역으로 코드를 읽어 올라가면 됩니다. 혹은 반대로 해당 변수가 초기화되는 지점부터 디버거로 한 스탭씩 진행하면서 값이 어떻게 변경되는지 살펴볼 수도 있겠네요. 사용하는 외부 디펜던시 코드에서 에러가 발생하는 게 아니라면 금방 원인이 되는 위치를 찾을 수 있을 겁니다. 사실 외부 디펜던시라도 디버거로 따라가면 어렵지 않게 찾을 수 있죠. 해결하기는 껄끄럽습니다. :)

SyntaxError

예전에는 타입 에러보다 더 자주 발생했던 에러입니다. 정상적으로 문법을 사용하지 않아서 자바스크립트 엔진이 이해하지 못할 때 발생해요. 정적 분석 도구가 없던 시절에는 에디터에서 코드를 작성할 때 실수로 오타를 내도 미리 알 수 있는 방법이 없었기 때문에 브라우저까지 가서야 구문 에러를 확인할 수 있었죠. 요즘에는 ESLint, Prettier 같은 도구가 코드를 작성하는 시점에 경고하거나 수정해 주기 때문에 브라우저에서 확인하기도 전에 에디터에서 해결됩니다. 정적 분석 도구를 정상적으로 사용했다면 거의 만날 일 없는 에러지만 몇 가지만 소개해볼게요.

SyntaxError: Unexpected token

이 에러는 몇 년 전까지 자주 볼 수 있었던 에러 메시지에요. 코드상에 자바스크립트 엔진이 이해 못 할 문자가 입력되었을 때 발생하는데요. 문법에 맞지 않는 문자가 있는 거죠. 자바스크립트 문법에 익숙하신 분이라도 말 그대로 실수로, 오입력으로 생길 수 있습니다. vim이나 vim을 시뮬레이션하는 모드를 즐겨시는 분은 Escape할 때 입력하는 키셋이 코드로 입력되는 경우에 발생하기도 하구요. (ex. “jk”) 비슷하게 단축키 입력을 잘못하는 경우에도 발생하곤 합니다. 예를 들면 Ctrl+s 를 급하게 몇 번 누르는 바람에 s라는 문자가 코드에 들어간 상태에서 저장을 해버리는 경우 말이죠. 몇 번 있으시죠? :)

IE 8 이전 버전에서는 객체 리터럴 안에서 마지막 콤마(Trailing Comma)가 입력된 경우, 이를 정상적인 코드로 인식하지 않아 에러가 발생했었죠.

var myObj = {
  a: ‘hi’,
  b: ‘hey’,   // error
}

이게 IE6~7만 있던 시절에는 가끔씩 실수로 발생했지만 금방 해결할 수 있었습니다. 오히려 그 후 살 만해지고(?) 빌드 도구를 자주 쓰는 시점부터 몇 번 애를 먹었던 기억이 있습니다. 빌드 도구가 마지막 콤마를 생성하는 이슈가 있었거든요. 코드베이스에는 마지막 콤마가 없는데 빌드 도구에서 넣어주니 IE7 이하 버전에서 문제가 발생했어요. 아마 Uglifyjs로 기억하는데 미니파이쪽 문제였습니다. 지금도 크로스 브라우저 테스트하다가 IE에서 저 에러 나오면 마지막 콤마부터 찾을 것 같습니다. 이제는 IE 저버전들을 테스트해야 할 필요가 갈수록 없어지고 있네요. 참 다행입니다.

문법 에러는 ESLint와 같은 정적 분석 도구의 사용으로 요즘은 웬만해선 브라우저에서 보기 힘든 에러가 되었습니다. ESLint 와 더불어 Prettier를 에디터에 잘 설정해서 사용하시면 됩니다. Prettier도 문법 에러가 발생하면 꽤 정교하게 알려주는 기능이 있습니다.

RangeError

다음은 범위 에러입니다. 어떤 값이나 데이터가 유효한 자원의 범위를 넘어설 때 발생하는 에러들입니다. 암튼 뭐가 너무 많거나 유효하지 않은 거에요.

RangeError: invalid array length

범위 에러를 가장 쉽게 설명할 수 있는 에러입니다만 보통은 만나기 힘든 에러입니다. 배열이 가용한 최대 크기를 벗어났을 때 발생하는 에러입니다. 배열의 유효한 최대크기는 ECMA-262 스펙상 4,294,967,295개라고 하는데 이게 브라우저나 플랫폼마다 다른 것으로 알고 있습니다. 배열이 크기가 허용하는 범위보다 더 커야 한다면 다수의 배열을 한 개의 배열로 다룰 수 있는 유사 배열 형태의 데이터 구조를 만들어 사용해야합니다. 검색해보지 않았지만 이미 유용한 라이브러리가 있을 수도 있겠네요 node.js 생태계에는 없는 게 없죠.

RangeError: invalid date (Edge)
RangeError: invalid date (Firefox)
RangeError: invalid time value (Chrome)
RangeError: Provided date is not in valid range (Chrome)

이렇게 데이트(Date) 객체를 만들 때 유효하지 않은 값을 사용하면 발생하는 에러도 있고요.

범위 에러 중에서 제일 유명한 에러는 바로 콜 스택 초과 에러일겁니다.

Error: Out of stack space (Edge)
InternalError: too much recursion (Firefox)
RangeError: Maximum call stack size exceeded (Chrome)

함수의 콜 스택이 너무 많이 쌓여서 발생하는 에러입니다. 브라우저별로 에러메시지와 심지어 타입가지 제 각각이네요. IE에서는 크롬과 동일한 메시지를 출력하곤 했습니다. 아무래도 개발할때는 크롬이 편하니까 에러 타입은 크롬을 기준을 따랐습니다. IE6를 지원해야하는 시절에는 자주 만났던 에러메시지죠. 심지어 그 시절에는 실제 서비스에서도 발생하는 경우가 많았습니다. 알고도 못잡은건지 몰랐던건지 모르겠지만 말이죠.

콜 스택은 자바스크립트 엔진 안에 서 사용되는 구성요소중에 하나라고 생각하시면 됩니다. 함수가 실행되면 실행중인 함수는 콜 스택 목록에 들어가게 됩니다. 함수안에서 또 함수가 실행이 되면 그 함수도 콜 스택에 들어가게 되죠. 이때 동일한 함수가 반복해서 실행되면 재귀호출이겠죠. 스택의 자료 구조상 최근에 실행된 함수가 제일 위에 있고 제일 처음 실행된 함수가 제일 밑에 있습니다. 함수의 실행이 종료되면 위에서부터 하나씩 종료된 함수가 콜 스택에서 제거되면서 이전 함수의 컨텍스트로 돌아갑니다. 콜 스택이 초과했다는 건 함수안에서 함수가 연속적으로 실행되어 콜 스택에 많이 쌓였는데 함수는 적절히 종료되지 않고 계속 쌓이기만 해서 콜 스택이 시스템이 허용하는 크기를 초과했다는 뜻입니다. 이 문제는 재귀호출에서 많이 발생하는 문제인데 이를 해결해기위해 꼬리 호출 최적화가 나왔죠. 자바스크립트에서도 ES6 지원 환경에서 결괏값 대신 인자를 사용하는 방법으로 구현할 수 있습니다(Tail call optimization in ECMAScript 6)

이 에러는 주로 재귀 호출 코드에서 탈출 로직의 버그로 발생하는 경우가 많아요. FE개발 중에 재귀 호출을 만들 일은 생각보다 적습니다만 알고리즘 수준의 코드인 경우이거나 텍스트나 데이터 구조를 탐색하는 등의 코드가 있을 수 있겠네요. 아무튼 기본적으로 이런 문제가 발생하면 재귀 호출 코드를 찾고 탈출 조건을 살펴보는 것으로 해결됩니다. 요즘 많이 쓰는 잘 만들어진 프레임웍 환경에도 가끔 만나는 케이스가 있는데 실제 코드는 제각각이겠지만 세터 안에서 동일한 데이터로 값을 세팅했을 때 발생하기도 합니다. 바닐라 자바스크립트도 set과 get 키워드로 프로퍼티를 만들 때 조심해야겠죠?

마무리

TypeError, SyntaxError, RangeError 타입에 대해서 알아봤어요. 이외에도 MDN 기준 EvalError, ReferenceError, URIError, AppregateError, InternalError가 있어요. 이 중에서 자주 발생하는 에러에 대해서 정리해봤습니다.

혹시라도 특이한 에러메시지를 만나서 당황하셨다면 MDN에 에러메시지들에 대한 설명이 잘 나와있으니까요. 참고하시면 좋을 것 같습니다.

♥ Support writer ♥
with kakaopay

크리에이티브 커먼즈 라이선스이 저작물은 크리에이티브 커먼즈 저작자표시-비영리-변경금지 4.0 국제 라이선스에 따라 이용할 수 있습니다.

© Sungho Kim2023