JavaScript Debugging (from FreeCodeCamp)

이 포스트는 FreeCodeCamp의 컨텐츠를 공부하며 작성한 번역임을 알립니다. (직역이 어색한 부분을 의역한 결과 표현이 원본과 상이할 수 있으나, 내용을 해치지 않고 이해를 돕는 선에서 수정하였습니다.)

목차 겸 요약

  • 콘솔을 사용해 변수의 값을 확인하라
  • typeof를 사용해 변수의 type을 확인하라
  • 변수와 함수 이름의 오타를 캐치하라
  • 닫히지 않은 괄호들과 따옴표를 캐치하라
  • 이 섞여 사용됐는지 주의하라
  • === 대신 = 이 사용됐는지 확인하라
  • 함수를 호출할 때 ()를 빠뜨렸는지 확인하라
  • 함수 호출시 인자를 바른 순서로 넣었는지 확인하라
  • 인덱스를 다룰 때 Off-by-One 에러를 조심하라
  • 반복문 안에서 변수를 새로 초기화할 때 주의하라
  • 올바른 종료 조건을 사용해 무한 루프를 방지하라

시작하며..

오류는 일반적으로 세가지 형태로 나타납니다.

  1. 문법 에러(syntax errors)
    • 프로그램이 실행되지 않습니다.
  2. 런타임 에러(runtime errors)
    • 코드가 실행에 실패하거나 생각지도 않은 행위를 합니다.
  3. 시맨틱 에러 또는 논리적 에러(semactic or logical errors)
    • 코드가 의도한대로 동작하지 않습니다.

근래의 최신 코드 에디터는 문법 에러 식별에 도움을 줍니다. (당신의 경험치도요!) 하지만, 시맨틱 에러나 런타임 에러는 문법적인 문제가 아니기 때문에 에디터의 힘을 빌릴 수 없고, 따라서 찾기 더 어렵습니다. 그 결과는 프로그램의 crash, 또는 무한 루프에 빠져 영원히 실행되거나, 의도와는 다른 틀린 결과를 출력합니다.

디버깅(Debugging)이란, ‘내가 작성한 코드가 왜 이렇게 동작하지?’라는 질문의 답을 찾으며 코드를 이해하는 과정이라고 생각하시기 바랍니다.

  1. 문법 에러의 예 (코드 에디터에서 감지됩니다.)
    funtion willNotWork( {
     console.log("Yuck");
    }
    

    function 키워드에 오타가 있고 괄호의 짝이 맞지 않습니다.

  2. 런타임 에러의 예 (프로그램을 실행하는 중에 만나게 됩니다.)
    function loopy() {
     while(true) {
         console.log("Hello, world!");
     }
    }
    

    loopy함수를 호출하면 무한 루프가 시작되며 브라우저에 crash가 발생합니다.

  3. semantic 에러의 예 (코드의 출력을 테스트한 후 결과를 살펴보다 만나게 됩니다.)
    function calcAreaOfRect(w, h) {
     return w + h; // 이 부분이 w * h 였어야 했습니다.
    }
    let myRectArea = calcAreaOfRect(2, 3);
    

    문법이 틀리지 않았고, 프로그램도 잘 동작합니다. 그러나 결과가 의도와 다릅니다.

디버깅 과정은 고통스러울 수 있습니다. 하지만 자꾸 해보면 이것도 연습이 되어서 단계적(step-by-step)으로 접근하는 방법을 익히고 발전시킬 수 있습니다.

다음과 같은 방법으로도 디버깅을 시작할 수 있습니다.

예를 들어, 함수 A와 B로 이루어진 코드에서 에러가 나는 상황을 상상해 보겠습니다.
만약 함수 A 가 잘 작동하고, 리턴하기로 한 값을 제대로 리턴하는 것을 확인했다면, 에러의 용의자는 함수 B로 좁혀집니다.
또는 처음부터 디버깅 탐색 범위를 반으로 줄이기 위해 코드 블록의 중간부터 체크를 시작해 볼 수도 있습니다. 거기에 문제가 없다면 문제는 나머지 절반에 있는 것입니다. (역자 주: 코드의 구성에 따라 위 예시는 좋은 방법이 아닐 수 있습니다. 다만 다양한 방법으로 디버깅을 시도할 수 있다는 것이 원문의 의도라고 생각합니다.)

앞으로 일반적인 오류의 형태와 디버깅에 유용한 도구를 알아 볼 것입니다. 다행히도, 디버깅은 인내심을 갖고 연습하면 누구든 습득할 수 있는 기술입니다.

콘솔을 사용해 변수의 값을 확인하라

크롬과 파이어폭스 브라우저는 모두 훌륭한 자바스크립트 콘솔을 내장하고 있습니다.

console.log()는 가장 유용한 디버깅 도구가 될 것입니다. 코드 내 전략적인 위치에 console.log()를 위치시켜서 변수가 프로그램 실행 중간에 어떤 값을 갖는지 출력할 수 있습니다. 또는 코드의 어느 라인까지 올바르게 실행되는지 체크포인트로 이용하여 버그가 있을 것으로 예상되는 영역을 좁혀갈 수 있습니다.

콘솔에 ‘Hello world!’ 를 출력하는 예제입니다.

console.log('Hello world!');

// (역자 주) 또는 변수도 함께 출력 가능합니다.
var str = 'world!';
console.log('Hello', str);

typeof를 사용해 변수의 type을 확인하라

typeof 키워드는 변수의 type을 확인할 때 사용할 수 있습니다. 다양한 데이터 타입을 다루는 코드를 디버깅 할 때 유용합니다. 만약, 두 숫자를 더하려고 하는데 하나가 사실은 스트링이라면? 결과는 예상대로 되지 않을 것입니다. Type Error는 어떤 계산을 하거나, 함수를 호출할 때 잘못된 부분이 있다면 만나게 됩니다. (JSON 오브젝트 형태의 외부 데이터에 액세스하고 조작할 때는 특별히 조심하세요.)

console.log(typeof ""); // outputs "string"
console.log(typeof 0);  // outputs "number"
console.log(typeof []); // outputs "object"
console.log(typeof {}); // outputs "object"

! 자바스크립트는 6가지의 primitive (immutable) 데이터 타입이 있습니다.

  • Boolean, Null, Undefined, Number, String, Symbol(ES6부터 생김)

! 그리고 한 가지 mutable 타입이 있습니다.

  • Object

변수와 함수 이름의 오타를 캐치하라

syntax error의 한 형태로, 변수명이나 함수 이름에 오타가 있으면 브라우저는 존재하지 않는 대상을 탐색하게 되고 결국 Reference Error가 발생하게 됩니다.

(역자 주: 또는 우연히 기존 변수를 참조하면서 오히려 에러가 나지 않고 잘못된 결과를 출력하는, 시맨틱 에러로 이어질 수 있습니다.)

닫히지 않은 괄호들과 따옴표를 캐치하라

조심해야 할 또다른 syntax error로, 다양한 괄호들과 따옴표들이 각각 열고 닫는 짝을 이루지 않는 경우입니다. 어떤 코드를 다른 코드 블록 안에 삽입하거나 콜백 함수를 추가하는 등의 작업을 할 때 괄호 쌍이 맞지 않게 되는 일이 흔히 발생합니다.

괄호 쌍에 관한 실수를 피하는 한 방법으로, 괄호를 열자마자 바로 닫고, 커서를 한칸 뒤로 옮겨서 안에 들어갈 내용을 코딩하는 방법이 있습니다. 다행히 요즘 코드 에디터들은 괄호 쌍의 나머지 하나를 자동으로 입력해줍니다.

이 섞여 사용됐는지 주의하라

자바스크립트는 스트링 선언에 를 둘 다 허용합니다. 어느 것을 사용할지는 개인 취향인데 약간의 예외 사항이 있습니다.
스트링이 isn’t 같은 축약형을 포함하거나, 인용문으로 따옴표를 포함할 때 인데, 이럴 때는 스트링 선언에 “ 나 ‘ 중에 고를 수 있어서 좋습니다. 하지만, 스트링이 선언이 너무 빨리 닫히지 않게 조심해야 합니다. 잘못 하면 syntax 에러가 납니다.

mixing quotes 의 예 :

// 옳은 예:
const grouchoContraction = "I've had a perfectly wonderful evening, but this wasn't it.";
const quoteInString = "Groucho Marx once said 'Quote me as saying I was mis-quoted.'";

// 틀린 예:
const uhOhGroucho = 'I've had a perfectly wonderful evening, but this wasn't it.';

escape 문자인 백슬래시(\) 를 사용한다면 “ 나 ‘ 중 하나만 일관되게 사용해도 좋습니다.

// Correct use of same quotes:
const allSameQuotes = 'I\'ve had a perfectly wonderful evening, but this wasn\'t it.';

=== 대신 = 이 사용됐는지 확인하라

조건문으로 프로그램에 분기를 주고자 할 때, 어떤 값이 다른 변수의 값과 같은지 체크하게 됩니다. 이 때 ===== 대신에 = 를 쓰지 않도록 주의해야 합니다.

(참고) 6가지 “falsy” 값들: false0""(비어있는 스트링), NaNundefinednull (역자 주: 위 6가지 값은 모두 false로 간주합니다. 반대로 이 6가지 이외엔 모두 true로 간주합니다.)

==== 로 잘 못 쓴 예 :

let x = 1;
let y = 2;
if (x = y) {
    // 이 코드 블록은 y가 (falsy value가 아니면) 어떤 값을 갖든 실행됩니다.
} else {
    // x와 y가 다른 경우를 의미하는 이 블록이 원래 실행시키려 했던 코드 블록이지만 실행이 안됩니다.
}

함수를 호출할 때 ()를 빠뜨렸는지 확인하라

함수나 메서드가 어떤 인자도 받지 않을 때, 그 함수를 호출하는 코드에 () 를 빠뜨리는 실수를 가끔은 하게 됩니다. 이렇게 괄호를 빼놓고 함수를 호출해 변수에 할당하기도 합니다. 그러다 변수의 값이나 타입을 콘솔에 찍어보고는, 자신이 기대했던 리턴값 대신에 함수 자체가 출력되는걸 보고 나서 이런 실수를 했다는걸 깨닫게 됩니다.

function myFunction() {
    return "You rock!";
}
let varOne = myFunction; // 함수 자체가 할당되었습니다.
let varTwo = myFunction(); // 함수가 리턴하는 스트링 "You rock!" 이 할당됩니다.
                           // 보통 이걸 의도하고 코드를 짜다가 실수가 생기곤 합니다.

함수 호출시 인자를 바른 순서로 넣었는지 확인하라

함수를 호출할 때 함수에 인자를 틀린 순서로 적어넣는 실수도 생깁니다. 만약 그 인자들이 서로 다른 타입의 데이터를 요구하는 경우, 즉 함수가 array와 integer를 요구하는데 둘을 서로 바꿔 입력한다면 에러가 발생할 것입니다.

한편, 인자들이 모두 같은 타입(예를들어 모두 interger)을 요구한다면? 그래도 코드의 로직이 말이 안되게 됩니다.

이런 문제를 피하려면, 필요한 모든 인자를 바른 순서로 전달해야 합니다.

(역자 주 예제)

function myFunction(arr, int) {
    for (var i = 0; i < arr.length; i++) {
        // 숫자의 .length 속성이 없어 undefined라 반복문이 돌지 않습니다.
        int += arr[i];
    }
    return int; // int 에 arr 엘리먼트를 더한 결과가 아닌, 배열이 출력됩니다.
}
myFunction(1, [1, 2, 3]);

인덱스를 다룰 때 Off-by-One 에러를 조심하라

Off-by-one errors (OBOE , OB1 errors)는 string이나 array의 특정 인덱스를 다루거나, 인덱스를 따라 반복문을 실행할 때 흔히 발생합니다.

자바스크립트는 0부터 인덱스가 시작되고, 이 말은 항상 .length-1 이 마지막 인덱스라는 뜻입니다.
반복문에서, 조건에 등호를 잘못 넣어 i <= arr.length 이러한 조건을 적용하면 마지막에 한번 더 반복문이 실행되며 ”index out of range” reference error 또는 undefined 를 리턴합니다.

특히 for문 뿐만 아니라, 스트링이나 어레이의 메서드 중에 인자로 인덱스 범위를 받는 메서드를 사용할 때는 꼭 레퍼런스를 읽고, 범위가 inclusive 인지 아닌지(해당 인덱스를 포함하는지 아닌지) 파악하여 사용해야 합니다.

Off-by-one 에러의 예:

let alphabet = "abcdefghijklmnopqrstuvwxyz";
let len = alphabet.length;
for (let i = 0; i <= len; i++) { // 반복문이 마지막에 한번 더 실행됩니다.
    console.log(alphabet[i]);
}
for (let j = 1; j < len; j++) { // 반복문이 한번 덜 실행되며 첫 인덱스 0을 놓칩니다.
    console.log(alphabet[j]);
}
for (let k = 0; k < len; k++) { // Goldilocks approves - 딱 알맞습니다.
    console.log(alphabet[k]);
}

반복문 안에서 변수를 새로 초기화할 때 주의하라

가끔은 정보를 저장하거나, 카운터 숫자를 증가시키거나, 반복문 안의 변수를 리셋할 필요가 있습니다.
변수들이 재초기화(re-initialize) 되어야 하는데 안된 경우, 또는 반대로 재초기화 되지 말아야 하는데 초기화된 경우 둘 다 잠재적 문제가 생깁니다.
특별히 위험한 경우는, 만약 어쩌다 반복문의 종료 조건으로 사용된 변수를 리셋해버린다면 반복문이 무한 루프에 빠집니다.

이런 버그를 디버깅 할 때 console.log()를 사용해 반복의 매 사이클 마다 해당 변수의 값을 출력하면, 변수 리셋이나 리셋 실패에 관련된 buggy한 행동을 드러낼 수 있습니다.

올바른 종료 조건을 사용해 무한 루프를 방지하라

마지막 주제는 무한 루프입니다. 반복문은 코드 블록을 특정 횟수 실행할때나, 어떤 조건이 만족될 때 까지 계속 실행시키는 데 굉장히 유용한 툴이지만, 알맞은 종료 조건(terminal condition)이 필요하고 이 조건이 충족되어야 실행이 종료됩니다.
무한 루프는 브라우저에 freeze 혹은 crash를 발생시키고 이것은 어느 누구도 원치않는 결과입니다.

이 예제는 종료 조건이 빠진 무한 루프 코드입니다. 실행하지 않으시기 바랍니다.

function loopy() {
    while(true) {  // terminal condition을 어디에서도 변경해주지 않고 있습니다.
        console.log("Hello, world!");
    }
}

무한 루프를 일으키는 실수를 더 소개하면
루프에서 카운트 변수를 증/감 시킬 때, 종료 조건을 점차 만족하도록 변화를 줘야 하는데 오히려 종료 조건으로부터 멀어지게 코딩하는 실수도 생깁니다. 또 하나는, 반복 중에 카운트 변수를 실수로 리셋하는 것입니다. (+=-= 대신 = 를 코딩하고 실행시켜 버리는 실수)

이렇게 종료 조건을 비껴가는 실수도 있습니다

for (i = 1; i != 4; i += 2) { // 3까지만 실행하려는 의도였지만, i값은 1 3 5 .. 로 증가합니다.
                              // 결국 종료 조건을 비껴가고 무한 루프에 빠집니다.
}