Top 10 JavaScript errors from 1000+ projects (and how to avoid them)

원문 - Jason Skowronski

Trend 파악을 위한 기고문 포스팅 - 1000개 이상의 프로젝트에서 가장 많이 발생한 10가지 자바스크립트 에러, 그리고 어떻게 피하는지

우리 커뮤니티의 개발자들을 위해서 우리는 데이터베이스의 수천개의 프로젝트를 살펴보고 자바스크립트에서 자주 발생하는 10개의 에러를 찾았습니다. 이런 에러들이 어떻게 발생하는지 보여드리고 어떻게 에러 발생을 막는지 알려드리겠습니다. 이런 버그들을 피할 수 있다면 여러분을 더욱 나은 개발자로 만들어 줄 것입니다.

데이터가 매우 중요하기 때문에 우리는 수집하고 분석해서 탑 10 자바스크립트 에러 순위를 선정했습니다. Rollbar는 각 프로젝트의 모든 에러를 수집하고 각 에러가 몇번 발생했는데 요약했습니다. 에러 그룹핑은 에러의 식별자를 통해서 진행했습니다. 기본적으로 우리는 첫번째 에러 다음에 두번째 에러가 반복일 경우 하나로 묶었습니다. 이것은 여러분이 로그파일에서 본적 있는 것처럼 큰 덩어리가 아니라서 보기 편하실 겁니다.

저희는 여러분과 사용자들에게 가장 영향을 주는 에러들에게 집중을 했습니다. 이렇게 하기 위해서 저희는 여러회사를 방문해가며 에러의 순위를 매겼습니다. 만약 단순히 에러의 총 횟수로 순위를 선정했다면 고객의 수가 많은 데이터가 다른 모든 순위를 압도할 것이기 때문입니다.

다음은 자바스크립트에서 자주 발생하는 10개의 에러들입니다. 각 에러는 가독성을 위해 축약되었습니다. 각각을 깊게 살펴보고 어떨 때 발생하는지, 어떻게 에러 발생을 피할 수 있는지 알아봅시다.

1. Uncaught TypeError: Cannot read property

여러분이 자바스크립트 개발자라면 아마 생각보다 이 에러를 많이 보셨을 겁니다. 이 에러는 크롬에서 undefined 객체의 프로퍼티를 읽거나 메소드를 호출할 때 발생합니다. 크롬 개발자 콘솔에서 매우 쉽게 테스트를 해볼 수 있습니다.

이것이 발생하는 이유는 많이 있지만 가장 흔한 것은 UI 컴포넌트가 렌더링하는 단계에서 부적절한 초기화가 있었기 때문입니다. 실제 앱에서 어떻게 이런일이 발생하는 지 알아봅시다. 리액트를 선택할 것이지만 Angular, Vue, 다른 프레임워크에서도 부적절한 초기화는 동일한 원리입니다.

``` class Quiz extends Component { componentWillMount() { axios.get('/thedata').then(res => { this.setState({items: res.data}); }); } render() { return (
    {this.state.items.map(item => <li key={item.id}>{item.name}</li> )}
); } } ```

여기서 알아차리셔야 할 점이 두가지 있습니다.

  1. 컴포넌트 상태값은(this.state) undefined로 시작을 합니다.
  2. 데이터를 비동기적으로 가져오면 컴포넌트는 최소한 한번은 데이터가 로딩되기 전에 렌더링을 합니다 - 생성자, componentWillMount, componentDidMount 에서 데이터 fetch가 일어는 것과 상관없이 말이죠. Quiz가 처음 렌더를 할 때 this.state.items는 undefined 입니다. 그래서 이 경우에 ItemList가 undefined된 아이템을 받고 여러분은 “Uncaught TypeError: Cannot read property ‘map’ of undefined”를 콘솔에서 보게됩니다.

이것은 매우 간단하게 고칠 수 있습니다. 생성자에서 타당한 값으로 state를 초기화 하는 것입니다.

``` class Quiz extends Component { // Added this: constructor(props) { super(props); // state 자신에게 items의 기본값을 할당합니다. this.state = { items: [] }; } componentWillMount() { axios.get('/thedata').then(res => { this.setState({items: res.data}); }); } render() { return (
    {this.state.items.map(item => <li key={item.id}>{item.name}</li> )}
); } } ```

여러분 앱의 코드는 달라지겠지만 이 정보가 여러분이 버그를 고치거나 피하는 데 충분한 단서가 되길 바라겠습니다. 만약 그렇지 않다면 계속해서 읽어주세요, 아래에 연관되는 에러가 나옵니다.

2. TypeError: ‘undefined’ is not an object (evaluating

이 에러는 사파리에서 undefined 객체의 메소드를 호출하거나 프로퍼티를 읽을 때 발생합니다. 사파리 개발자 콘솔에서 쉽게 확인할 수 있습니다. 근본적으로 위의 크롬에러와 동일하지만 사파리는 다른 에러 메시지를 사용합니다.

3. TypeError: ‘null’ is not an object (evaluating

이 에러는 사파리에서 null 객체의 프로퍼티를 읽거나 메소드를 호출할 때 발생합니다. 사파리 개발자 콘솔에서 쉽게 테스트 할 수 있습니다.

흥미로운 점은 자바스크립트에서 null과 undefined가 동일하지 않기 때문에 두개의 다른 에러메시지를 보게 되는 것입니다. undefined는 값이 할당되지 않았을 때 사용되며 null의 의미는 값이 비어있다는 것입니다. 같지 않다는 것을 확인하려면 strict equality를 사용해서 비교해보세요.

이 에러는 자바스크립트에서 DOM element 안에 element가 로딩되지 않았을 때 사용하려고 시도하면 발생합니다. 이것은 DOM API가 비어있는 객체를 조회했을 때 null을 리턴하기 때문입니다.

모든 JS 코드는 DOM elements가 생성된 다음에 DOM element를 가지고 작업을 해야합니다. JS 코드는 HTML 밖에서 탑다운 방식으로 해석합니다. 그래서 DOM elements 전에 태그가 있다면 브라우저가 HTML 페이지를 파싱하자마자 스크립트 태그 내에 JS code가 실행될 것입니다. 스크립트가 로딩되전에 DOM elements가 생성되어 있지 않다면 여러분은 이 에러메시지를 보게 될 것입니다.

이 예제에서 우리는 페이지가 로딩되었음을 기다리는 이벤트 리스너를 추가해서 문제를 해결할 수 있습니다. 한번 addEventListener가 발생하면 init()메소드는 DOM elements를 사용할 수 있습니다.

```
```

4. (unknown): Script error

스크립트 에러는 uncaught 자바스크립트 에러가 도메인 영역을 넘어갔을 때 cross-origin 정책을 위반해서 발생하게 됩니다. 예를 들어 자바스크립트 코드를 CDN에서 호스트 하고 있다면 uncaught 에러들은(try-catch에서 잡히는 것 대신에 window.onerror 핸들러까지 올라가는 에러들입니다) 간단히 Script 에러라고만 보고되고 유용한 정보들은 없을 것입니다. 이것은 브라우저 보안 방식이 도메인끼리 데이터를 주고받는 것을 막을려고 하기 때문입니다.

진짜 에러메시지를 얻기위해선 다음과 같이 해야합니다.

  1. Access-Control-Allow-Origin 헤더를 보낸다.

Access-Control-Allow-Origin 헤더를 * sigifies에 설정해서 자원이 다른 도메인으로 부터 접근할 수 있게끔 합니다. 필요한 경우 * 부분을 여러분의 도메인으로 변경할 수 있습니다. Access-Control-Allow-Origin:www.example.com. 그러나 다수의 도메인을 처리하는 것은 좀 어렵고 CDN을 사용하고 있다면 캐싱이슈가 발생하기 때문에 노력을 기울일 가치가 없을 수도 있습니다.

apache

자바스크립트가 동작하는 폴더에 .htaccess 파일을 만들어서 다음의 내용을 추가하세요

Header add Access-Control-Alow-Origin "*"

Nginx

자바스크립트 파일이 돌아가는 location block에 직접적으로 add_header를 추가하세요

location ~ ^/assets/ {
  add-header Access-Control-Origin *;
}

HAProxy

자바스크립트 파일이 serve하는 백엔드 에셋에다가 다음의 코드를 추가하세요

rspadd Accenss-Control-Allow-Origin:\ *

Set crossorigin = “anonymous” on the script tag

HTML 소스에 각 스크립트 마다 Access-Control-Allow-Origin 헤더를 설정하고 crossorigin=”anonymous”를 스크립트 태그에 추가하세요. 스크립트 태그에 crossorigin 프로퍼티를 추가하기 전에 헤더파일이 스크립트 파일이게 전달되는지 확실하게 검증하세요. 파이어 폭스에서는 crossorigin 속성은 present 이지만 Access-Control-Allow-Origin 헤더가 그렇지 않다면 스크립트는 동작되지 않을 것입니다

5. TypeError: Object doesn’t support property

이 에러는 IE에서 undefined 메소드를 호출할 때 발생합니다. IE 개발자 도구에서 테스트할 수 있습니다.

이것은 크롬에서 발생하는 “TypeError: ‘undefined’ is not a function” 에러와 동일합니다. 그래요, 동일한 논리의 오류라도 다른 브라우저에서는 다른 에러메시지가 나올 수 있는 것이죠.

이것은 IE의 웹 응용프로그램에서 흔히있는 문제로 자바스크립트 네임스페이싱이 필요합니다. 이 경우에는 99.9% 문제가 IE에서 현재 네임스페이스에 this 키워드를 통한 메소드 바인딩 기능이 없기 대문입니다. 예를 들어 Rollbar 라는 네임스페이스가 있고 거기에 isAwesome이라는 메소드가 있습니다. 일반적으로 Rollbar 네임스페이스 내부라면 this.isAwesome()으로 호출할 수 있습니다.

크롬이나 파이어폭스, 오페라에서는 이러한 문법을 잘 수용하지만 IE에서는 안됩니다. 그래서 JS 네임스페이스를 쓸때는 this 대신에 실제 네임스페이스를 쓰는 것이 가장 안전합니다.

6. TypeError: ‘undefined’ is not a function

이 에러는 크롬에서 undefined 함수를 호출할 때 발생합니다. 크롬 개발자 콘솔이나 모질라 파이어폭스 개발자 콘솔에서 테스트할 수 있습니다.

자바스크립트 코딩 기술과 디자인 패턴은 시간이 지나면서 매우 세련되졌고 이것은 콜백이나 클로져내에서 자기 영역을 호출하는 것이 증가한 것과 일치합니다. 이것은 일반적으로 this/that confusion의 원인이 됩니다. 다음의 코드를 봅시다.

``` function clearBoard(){ alert("Cleared")'' } document.addEventListener("click", function(){ this.clearBoard(); //what is "this"? }) ```

위의 코드를 실행하고 페이지를 클릭해보면 “Uncaught TypeError:this.clearBoard is not a function” 에러메시지를 받게 될 것입니다. 그 이유는 익명함수는 document 내에서 실행되는 반면에 clearBoard는 window에서 정의되었기 때문입니다.

일반적으로 엣날 브라우저를 고려한 솔루션은 this를 변수로 저장해서 클로저 내에서 상속받을 수 있도록 하는 것입니다.

``` var self=this; document.addEventListener("click", function(){ self.clearBoard(); }); ```

반대로 최신 브라우저에서는 올바르게 조회할 수 있도록 하기위해 bind()메소드를 사용하면 됩니다.

``` document.addEventListener("click",this.clearBoard.bind(this)); ```

7. Uncaught RangeError

이 에러는 크롬브라우저에서 몇가지 상황에서 발생합니다. 하나는 끝나지 않는 재귀함수를 호출했을 때 입니다. 크롬 개발자 콘솔에서 테스트 할 수 있습니다.

또한 범위를 벗어난 값을 함수에 전달했을 때도 발생합니다. 많은 함수는 그들의 입력값으로 특정 범위만 허용합니다. 예를 들어 Number.toExponential(digits), Number.toFixed(digits)는 0 ~ 100을 입력받고 Number.toPrecision(digits)는 1 ~ 100을 입력받습니다.

``` var a = new Array(4282867285); //OK var b = new Array(-1); //range error var num = 2.555555; document.writele(num.toExponential(4)); //OK document.writeln(num.toExponential(-2)); //range error! num = 2.9999; document.writeln(num.toFixed(2)); //OK document.writeln(num.toFixed(105)); //range error! num = 2.3456; document.writeln(num.toPrecision(1)); //OK document.writeln(num.toPrecision(0)); //range error! ```

8. TypeError:Cannot read property ‘length’

이 에러는 크롬에서 undefined 변수의 length 프로퍼티를 읽을 때 발생합니다. 크롬 개발자 콘솔에서 테스트 할 수 있습니다.

여러분은 보통 배열에서 정의된 길이를 찾을 수 있지만 배열이 초기화 되지 않았거나 변수 이름이 다른 컨텍스트에서 숨겨진 경우 이 에러를 받게 됩니다. 다음 예제를 통해 이 에러를 이해해 봅시다.

``` var testArray=["Test"] function testFunction(testArray) { for (var i = 0; i < testArray.length; i++) { console.log(testArray[i]); } } testFunction(); ```

매개변수와 함께 함수를 정의하면 이런 매개변수는 지역변수들이 됩니다. 이 말은 testArray라는 이름의 변수가 있어도 함수내에 매개변수가 같은 이름(testArray)라면 지역변수로 취급된다는 것이죠. 이 문제를 해결하기 위한 방법은 두가지가 있습니다.

1. 함수 선언 구문에서 매개변수들으르 제거합니다(이 경우는 매개변수로 쓰일 변수들이 외부에 정의되어 있어서 매개변수가 필요없는 경우 입니다.)

``` var testArray = ["Test"]; function testFunction(/*No params */) { for (var i = 0; i < testArray.length i++) { console.log(testArray[i]); } } testFunction()'' ```

2. 우리가 정의한 함수를 전달하며 호출합니다.

``` var testArray = ["Test"]; function testFunction(testArray) { for (var i = 0; i < testArray.length; i++) { console.lgo(testArray[i]); } } testFunction(testArray); ```

9. Uncaught TypeError: Cannot set Property

undefined 변수에 접근을 하면 항상 undefined를 리턴받게 되고 undefined 프로퍼티는 get이나 set을 할 수 없습니다. 이 경우에 응용프로그램이 “Uncaught TypeError cannot set property of undefined” 라는 메시지를 던집니다.

test 객체가 존재하지 않는다면 “Uncaught TypeError cannot set property of undefined” 에러가 발생할 것입니다.

10. ReferenceError: event is not defined

이 에러는 undefined 이거나 현재 스코프 외부에 있는 변수에 접근할 때 에러가 발생합니다. 크롬 브라우저에서 쉽게 테스트할 수 있습니다.

이벤트 핸들링 시스템을 사용하다가 이 에러메시지를 받았다면 매개변수로 전달된 이벤트 객체를 사용했는지 확인해보세요. IE와 같은 옛날 브라우저에서는 전역 변수 이벤트를 제공하고 크롬은 자동적으로 핸들러에 이벤트 변수를 붙여줍니다. 파이어폭스는 자동적으로 추가해 주지 않습니다. JQuery 같은 라이브러리들은 이 행동을 정규화하는 것을 시도합니다. 말할 필요도 없이 가장 좋은 사용법은 여러분의 이벤트핸들러 함수에 전달된 것을 사용하는 것입니다.

``` document.addEventListener("mousemove", function (event) { console.log(event)'' }) ```

Conclusion

많은 에러들이 null이나 undefined와 관련된 것으로 나타났습니다. 여러분이 strict 컴파일러 옵션을 사용한다면 타입스크립트와 같은 정적 타입 체킹 시스템이 도움이 도리 것입니다. 그것은 타입이 expected 하지만 정의되지 않으면 경고를 날립니다. 타입스크립트가 아니더라도 guard clauses를 사용해서 객체들을 사용하기 전에 undefined인지 확인하는 것이 도움이 됩니다.

Summary

  • null이나 undefined 객체의 프로퍼티나 메소드를 사용하는 에러가 가장 자주 발생한다.
  • 이 문제는 scope의 문제이거나 (this가 바인딩 되지 않음/매개변수가 scope를 가림), 초기화되지 않은 객체를 사용해서 발생한다.
  • 타입스크립트 같은 시스템을 사용하거나 객체를 사용할 때 undefined인지 체크하는 습관이 관련 오류를 피할 수 있는 방법이 될 것이다.

© 2019. All rights reserved.

Powered by Hydejack v8.1.1