Meeta

누구나 한 번쯤은 띄워본 JavaScript 에러 TOP 10.

Meeta - 개발자의 포트폴리오 2018. 3. 22. 18:44



안녕하세요 Meeta 매니저 아몬드🤴입니다.

오늘은 해외의 서비스 기술블로그에서 JavaScript와 관련된 재밌는 글이 있어서 직접 번역해봤습니다.

JavaScript 개발자라면 흥미롭게 읽어보실 수 있을 것 같습니다!!!





* 이 글은 Rollbar의 기술블로그에 포스팅된 글을 번역한 글입니다. (https://rollbar.com/blog/top-10-javascript-errors/)



커뮤니티의 개발자들에게 보답하기 위해 우리는 천여개의 JavaScript 프로젝트에서 가장 자주 발생하는 에러 10가지를 찾아냈습니다. 이 10가지 에러가 무엇때문에 발생하고 이를 막기 위해선 어떻게 해야할지 보여드릴게요. 이것들만 피하더라도 당신은 더 나은 개발자가 될 수 있을 거에요!!!


먼저 데이터는 갱장히 중요하기 때문에 우리는 데이터를 모으고, 분석해서 10가지 JavaScript 에러에 대해 순위를 매겼습니다. <Rollbar>는 프로젝트에서 발생하는 에러들을 모아 각 에러들이 얼마나 많이 발생하는지 요약해주고, fingerprints를 이용해서 수집한 에러들을 분류해요. 반복해서 발생한 에러는 하나의 에러로 분류해서 유저들에게 로그 파일의 어지러운 덤프 목록 대신에 보기 쉬운 요약자료를 제공해줍니다.


우리는 개발자와 그 서비스의 사용자들에게 영향을 끼칠 만한 에러들에 집중합니다. 단순히 에러가 발생한 숫자를 통해 순위를 정한다면 이용자가 많은 서비스의 에러만 상위권에 올라올 것이기 때문에, 회사에 관계없이 같은 에러를 겪고있는 프로젝트의 수에 따라 에러의 순위를 정했습니다.


JavaScript 에러 Top 10!!!

각각 에러는 가독성을 높이기 위해 이름을 줄였습니다. 이제 이 에러들이 왜 발생하고 어떻게 피할 수 있는지 알아볼까요?


1. Uncaught TypeError: Cannot read property

JavaScript 개발자라면 생각보다 이 에러를 많이 봤을텐데요. 이 에러는 크롬에서 정의되지 않은 객체의 property를 읽어내거나 method를 호출했을 때 발생합니다. 간단하게 크롬 검사창의 console에서도 쉽게 확인할 수 있어요.



이 에러가 발생하는 이유는 아주 많은데요. 가장 일반적인 경우는 UI component를 렌더링하는동안 상태 초기화가 제대로 이루어지지 않아서 그렇습니다. 실제로 코드를 보면서 에러를 살펴볼까요? React의 경우를 볼테지만 Angular, Vue 등 다른 프레임워크에서도 같은 방식으로 문제가 발생할 수 있습니다.



class Quiz extends Component {
  componentWillMount() {
    axios.get('/thedata').then(res => {
      this.setState({items: res.data});
    });
  }

  render() {
    return (
      <ul>
        {this.state.items.map(item =>
          <li key={item.id}>{item.name}</li>
        )}
      </ul>
    );
  } 
}

여기엔 알아둬야 할 두가지 중요한 점이 있어요.


1. Component의 state(여기서는 this.state)는 undefined 인 상태로 시작합니다.

2. 비동기적으로 데이터를 가지고 올 때, constructor(여기서는 componentWillMount 또는 componentDidMount)에서 데이터를 불러오는 것과 관계없이 component는 데이터를 불러오기 전에 최소한 한번은 렌더링을 수행합니다. Quiz가 처음 렌더링될 때, this.state.items undefined입니다. 결국 ItemList가 undefined인 items를 가졌다는 뜻이기 때문에 "Uncaught TypeError: Cannot read property 'map' of undefined" 에러가 발생하게 되는거죠!


이 에러는 굉장히 쉬운 방법으로 고칠 수 있습니다. 가장 간단한 방법은 constructor에서 적절한 초기값을 설정해주는 거에요 ;)



class Quiz extends Component {
  // 여기에 추가합니다.
  constructor(props) {
    super(props);

    // Quiz 자체에 state를 할당하고, items에 기본값을 줍니다.
    this.state = {
      items: []
    };
  }

  componentWillMount() {
    axios.get('/thedata').then(res => {
      this.setState({items: res.data});
    });
  }

  render() {
    return (
      <ul>
        {this.state.items.map(item =>
          <li key={item.id}>{item.name}</li>
        )}
      </ul>
    );
  }
}


여러분의 프로그램에 들어갈 코드는 살짝 다르겠지만 충분히 이 문제를 해결하거나 이 에러가 발생하지 않도록 할 수 있을거에요. 아직 에러가 해결되지 않았다면 아래의 에러들을 더 살펴보세요. 아직 관련된 에러들이 많이 남아있거든요!


2. TypeError: 'undefined'is not an object (evaluating

이 에러는 Safari에서 정의되지 않은 객체의 property를 읽어내거나 method를 호출했을 때 발생합니다. Chrome과 마찬가지로 Safari Developer Console에서 확인할 수 있어요. 이건 1. Uncaught TypeError: Cannot read property와 완전 똑같은 에러지만 단지 Safari에서 다른 에러 메시지를 띄우는 것 뿐이에요.



3. TypeError: null is not an object (evaluating

이 에러는 Safari에서 null의 property를 읽어내거나 method를 호출했을 때 발생합니다. 여어어억시 Safari Developer Console에서 확인할 수 있어요.



흥미롭게도! JavaScript에서 nullundefined는 같지 않아요. 그래서 우리는 앞의 1, 2번 에러와 다른 에러 메시지를 보게되는거죠. undefined의 경우 변수 자체가 할당되지 않은 경우이고, null의 경우 변수는 선언되었지만 비어있는 것을 말합니다. 이 둘이 같지 않다는 것을 증명하기 위해서 strict equality operator(===연산)를 사용해볼까요?



개발시에 이 에러가 발생하는 한가지 경우는 JavaScript에서 아직 element가 로드되기 전에 DOM element를 사용하려 했을 때입니다. 이건 DOM API가 빈 오브젝트를 참조할 때 null값을 반환하기 때문입니다.


DOM elements를 사용하는 JavaScript코드는 DOM element가 생성된 이후에 실행되어야 합니다. HTML에서 JavaScript코드는 위에서 아래로(top to down) 읽어지는데요. DOM element 전에 script 태그가 있는 경우, 브라우저가 HTML 페이지를 파싱하면서 script 태그가 있는 JavaScript코드를 먼저 실행합니다. 그리고 이때 script를 로딩하기 전에 DOM element가 생성되어있지 않다면 에러가 발생하게 됩니다.

아래의 예시에서는 페이지가 준비되었는지 확인할 수 있는 event listener를 추가해서 문제를 해결하는 방법을 보여줍니다. 먼저 addEventListener가 실행되면 문서가 준비되었을 때 init() method가 DOM element를 사용할 수 있도록 만듭니다.


<script>
  function init() {
    var myButton = document.getElementById("myButton");
    var myTextfield = document.getElementById("myTextfield");
    myButton.onclick = function() {
      var userName = myTextfield.value;
    }
  }
  document.addEventListener('readystatechange', function() {
    if (document.readyState === "complete") {
      init();
    }
  });
</script>

<form>
  <input type="text" id="myTextfield" placeholder="Type your name" />
  <input type="button" id="myButton" value="Go" />
</form>

4. (unknown): Script error

Script 에러는 JavaScript의 uncaught error가 cross-origin policy를 어기며 다른 도메인에서부터 넘어오는 경우 발생하게 됩니다. 예를 들어 CDN에서 JavaScript코드를 호스팅하는 경우, try-catch에 잡히지 않고 window.onerror 핸들러에 의해 확인되는 모든 uncaught error는 에러의 세부 정보를 표현하지않고 단순히 "Script error"로 표시됩니다. 브라우저 보안상 다른 도메인으로부터 허용되지 않은 방법으로 데이터가 넘어오는 것을 방지하기 때문에 발생하는 일입니다.


정확한 에러 메시지를 얻어내기 위해서는 다음과 같은 방법이 있습니다.


1) Access-Control-Allow-Origin 헤더를 이용합니다.


Access-Control-Allow-Origin 헤더를 *로 설정하여 다른 도메인으로부터 정보가 적절하게 넘어왔다는 것을 확인합니다. 필요하다면 *을 특정 도메인주소로 바꿀 수 있습니다. (예. Access-Control-Allow-Origin: www.example.com) 하지만, 여러개의 도메인을 전부 관리하는 것은 매우 어려운 일이고, CDN을 캐싱 문제 때문에 사용하고 있다면 추천하지 않는 방법입니다. 더 자세한 내용은 여기를 확인하세요!!


Apache


JavaScript 파일이 있는 폴더에 아래와 같은 내용의  .htaccess파일을 추가합니다.


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


Nginx


JavaScript 파일이 있는 location 블럭에 add_header를 추가합니다.


location ~ ^/assets/ {
    add_header Access-Control-Allow-Origin *;
}


HAProxy


JavaScript 파일이 있는 asset backend에 다음을 추가합니다.


rspadd Access-Control-Allow-Origin:\ *


2) script 태그에 crossorigin="anonymous" 을 설정합니다.


HTML 소스에서 Access-Control-Allow-Origin헤더를 설정한 모든 script 태그 안에 crossorigin="anonymous"를 세팅합니다. 

script 태그에 crossorigin을 추가하기 전에 확실히 헤더가 전송되고 있는지 확인해야합니다. 파이어폭스 같은 경우, crossorigin 속성이 있고 Access-Control-Allow-Origin 헤더가 없을 때 script가 실행되지 않습니다.


5. TypeError: Object doesn't support property

이 에러는 IE에서 정의되지 않은 method를 호출했을 때 발생합니다. Chrome, Safari와 마찬가지로 IE Developer Console에서 테스트해볼 수 있어요.



이 에러는 크롬의 "TypeError: 'undefined' is not a function"과 같은 에러입니다. 위에서 이야기한 것 같이 서로 다른 브라우저에서 같은 에러를 표시하는 에러메시지는 다를 수 있어요. 


이건 JavaScript의 namespacing을 사용하는 IE 웹 어플리케이션에서 자주 발생하는 문제입니다. 이 경우 문제의 99.9%는 IE가 현재 namespace상의 method를 this라는 키워드에 바인딩 하지 못하기 때문에 발생합니다. 예를 들어 JavaScript에서 namespace Rollbar가 isAwesome이라는 method를 가지는 경우, 일반적으로 Rollbar안에서 isAwesome을 다음과 같이 불러올 수 있습니다.


this.isAwesome();


Chrome, Firefox, Opera는 이 구문을 받아들이지만, IE의 경우 아래와 같이 항상 실제 namespace를 prefix로 가져야합니다.


Rollbar.isAwesome();


6. Type Error: 'undefined' is not a function

이 에러는 크롬에서 정의되지 않은 함수를 사용했을 때 발생합니다. 역시 크롬이나 파이어폭스 Developer Console에서 테스트해볼 수 있습니다.



JavaScript 코딩 기술과 디자인 패턴이 엄청나게 발전함에 따라 callback과 closure에 사용되는 자기 참조(self-referencing) scope가 함께 증가하게 되었습니다. 그리고 이 callback과 closure는 this가 포함된 구문을 굉장히 헷갈리게 만듭니다.


아래 code snippet을 봅시다.


function clearBoard(){
  alert("Cleared");
}

document.addEventListener("click", function(){
  this.clearBoard(); // this가 뭐지??
});


위 코드를 실행시키고 페이지에서 <클릭> 행동을 취하면 "Uncaught TypeError: this.clearBoard is not a function" 에러가 발생하게 됩니다. 익명함수는 document의 context에서 실행되었지만, clearBoard는 window에 정의되어있기 때문이죠.


고전 브라우저에서 사용되는 전통적인 해결방식은 this에 대한 참조를 클로저에서 접근할 수 있는 새로운 변수에 저장하는 것입니다. 

예를 들면


var self=this;  // this에 대한 참조를 self에 저장시킵니다.
document.addEventListener("click", function(){
  self.clearBoard();
});


최신 브라우저의 경우, bind()method를 사용하여 적절한 참조값을 가지게 할 수 있습니다.


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


7. Uncaught RangeError: Maximum call stack

이 에러는 두가지 이유로 크롬에서 발생합니다. 먼저 종료되지 않는 재귀함수를 호출했을 때 발생하는데, 크롬 Developer Console에서 에러 메시지를 확인할 수 있습니다.



다른 경우는 함수에 범위 이상의 값이 입력 되었을 때 발생합니다. 많은 함수들이 제한된 숫자 범위의 입력값을 받는데요. 예를 들어 아래 코드에서 Number.toExponential(digits)과 Number.toFixed(digits)는 0에서 20사이의 수만 받아들이고, Number.toPrecision(digits)의 경우 1에서 21사이의 숫자만 받아들입니다. 이와 같은 숫자 범위를 초과한 입력 값을 받게되면 "Uncaught RangeError: Maximum call Stack"이 발생하게 됩니다.


var a = new Array(4294967295);  //OK
var b = new Array(-1); //range error

var num = 2.555555;
document.writeln(num.toExponential(4));  //OK
document.writeln(num.toExponential(-2)); //range error!

num = 2.9999;
document.writeln(num.toFixed(2));   //OK
document.writeln(num.toFixed(25));  //range error!

num = 2.3456;
document.writeln(num.toPrecision(1));   //OK
document.writeln(num.toPrecision(22));  //range error!

8. TypeError: Cannot read property 'length'

이 에러는 크롬에서 undefined 변수의 length property에 접근하는 경우 발생합니다. 이 에러 역시 크롬 Developer Console에서 확인할 수 있습니다. 



일반적으로 array에는 길이가 정의되어 있지만, array가 초기화되어있지 않거나 변수이름이 다른 context 상에 숨겨져 있는채로 코드를 실행하면 array에서도 이 에러가 발생할 수 있습니다. 아래 예를 통해 확인해보죠.


var testArray= ["Test"];

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

testFunction();


파라미터를 가진 함수를 선언했을 때, 이 파라미터는 지역변수가 됩니다. 기존에 testArray라는 변수를 가지고 있더라도 같은 이름을 가진 파라미터가 있다면 이 변수는 지역변수로 취급받게 되는 것입니다.


이 문제를 해결하기 위해서는 두가지 방법이 있습니다.


1) 함수 선언부에서 파라미터를 없애버립니다! (사용하고자하는 변수를 함수 밖에서 선언하면, 함수에 파라미터는 필요없으니까요!)


var testArray = ["Test"];

/* testArray를 함수 밖에 선언한다. */
function testFunction(/* 매개변수 없음 */) {
    for (var i = 0; i < testArray.length; i++) {
      console.log(testArray[i]);
    }
}

testFunction();


2) 앞에서 선언한 array를 매개변수로 전달하여 함수를 실행시킵니다.


var testArray = ["Test"];

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

testFunction(testArray);


9. Uncaught TypeError: Cannot set property

값이 정의되지 않은 변수를 사용하고자 할 때 항상 undefined라는 메시지를 받게되고, undefined 변수의 어떤 property도 값을 읽거나 쓸 수 없습니다. 이 경우 "Uncaught TypeError: Cannot set property of undefined" 에러가 발생합니다.


예를 들어, 크롬 브라우저의 경우



test object가 존재하지 않는 경우, "Uncaught Type Error cannot set property of undefined" 라는 에러 메시지를 띄우게 됩니다.


10. ReferenceError: event is not defined

이 에러는 값이 정의되지 않은 변수를 사용하거나 현재 scope의 밖에 있는 변수를 사용했을 때 발생합니다. 역시 크롬 브라우저에서 확인할 수 있습니다.



만약 event handler에서 이 에러가 발생한다면, event object를 파라미터로 확실히 넘겼는지 확인하면 됩니다. IE와 같은 오래된 브라우저들은 전역 event 변수를 제공하고, 크롬은 자동으로 event변수를 handler에 붙여줍니다. 파이어폭스의 경우 아직 자동으로 추가하지 못하고, JQuery와 같은 라이브러리들은 이 행동을 정규화시키려고하고 있습니다. 

그렇지만 역시 가장 좋은 방법은 아래와 같이 event handler 함수에 event 변수를 넘겨주는 것이겠죠!


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


결론!!!

살펴본 10가지의 에러 중에 대부분이 null 이나 undefined에 의한 에러 였습니다. Typescript와 같은 정적 타입 체크 시스템은 컴파일러 옵션을 엄격하게 설정해서 이런 문제들을 잡을 수 있도록 도와줍니다. type을 예상할 순 있지만 정확히 정의되지 않은 경우 경고를 표시해주기도하고요. Typescript가 없더라도 실행되기 전에 object가 혹시 undefined 상태에 있지 않은지 검사하는 구문을 작성해 볼 수 있습니다.


이 글을 통해 새로운 것을 배우고, 미래에 발생할 에러를 피할 수 있었으면 좋겠습니다. 하지만 예상치 못한 에러는 항상 발생하게 됩니다. 그렇기 때문에 사용자와 프로덕트에 영향을 끼치는 에러들을 시각화하고 빠르게 해결하는 도구를 갖추는 것이 매우 중요합니다.


Rollbar는 JavaScript 에러를 시각화하고 빠르게 해결할 수 있는 정보를 제공합니다. 예를 들어 사용자의 브라우저에서 어떤 일이 발생하여  에러를 유발하는지 알려주는 telemetry와 같은 추가 디버깅 도구를 제공하고 있죠. 이는 개발자에게 기본적인 브라우저 개발자 콘솔에서 얻을 수 없는 시각을 제공합니다. Javascript를 지원하는 Rollbar의 더 많은 기능에 대해서 지금 확인해보세요!!!






의역이 많습니다!!!! 확인해보시고 문제가 있는 부분은 댓글로 남겨주세요 ;)

저희 팀도 아직 Rollbar를 사용해보진 않았지만 유용하게 사용할 수 있는 기능이 많은 것 같습니다!!!!



에러 없는 하루 보내세요😎









개발자 포트폴리오를 만드는 단 하나의 솔루션 Meeta.

https://meeta.io


2018/02/20 - [채용소식] - [라온버드] 머신러닝 엔지니어를 채용합니다.

2018/02/20 - [채용소식] - [부동산다이어트] back-end 개발자를 채용합니다.

2018/02/20 - [채용소식] - [화해 - 버드뷰] web 개발자를 채용합니다.