버그가 있는 JavaScript 코드: JavaScript 개발자가 저지르는 10가지 가장 일반적인 실수
게시 됨: 2022-03-11오늘날 JavaScript는 거의 모든 최신 웹 애플리케이션의 핵심입니다. 특히 지난 몇 년 동안 SPA(단일 페이지 애플리케이션) 개발, 그래픽 및 애니메이션, 심지어 서버 측 JavaScript 플랫폼을 위한 강력한 JavaScript 기반 라이브러리 및 프레임워크가 광범위하게 확산되었습니다. JavaScript는 웹 앱 개발의 세계에서 진정으로 유비쿼터스 되었으며 따라서 마스터해야 할 점점 더 중요한 기술이 되었습니다.
처음에는 자바스크립트가 매우 단순해 보일 수 있습니다. 그리고 실제로 웹 페이지에 기본적인 JavaScript 기능을 구축하는 것은 경험 많은 소프트웨어 개발자가 JavaScript를 처음 접하더라도 매우 간단한 작업입니다. 그러나 그 언어는 처음에 믿게 된 것보다 훨씬 더 미묘하고 강력하며 복잡합니다. 사실, JavaScript의 미묘함 중 많은 부분이 JavaScript의 작동을 방해하는 여러 가지 일반적인 문제로 이어집니다. 그 중 10가지가 여기에서 논의됩니다. 이는 마스터 JavaScript 개발자가 되기 위해 인식하고 피해야 하는 중요한 문제입니다.
일반적인 실수 #1: this
대한 잘못된 참조
언젠가 코미디언이 말하는 것을 들은 적이 있습니다.
내가 여기 있는 것이 아닙니다. 거기 외에 't'가 없는 여기가 무엇입니까?
그 농담은 JavaScript의 this
키워드와 관련하여 개발자에게 종종 존재하는 혼란 유형을 여러 면에서 특징짓습니다. 내 말은, this
정말로 이것입니까, 아니면 완전히 다른 것입니까? 아니면 정의되지 않은 것입니까?
JavaScript 코딩 기술과 디자인 패턴이 수년에 걸쳐 점점 더 정교해짐에 따라 콜백 및 클로저 내에서 자체 참조 범위의 확산도 이에 상응하여 증가했으며, 이는 "이것/저것 혼동"의 상당히 일반적인 원인입니다.
다음 예제 코드 조각을 고려하십시오.
Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // what is "this"? }, 0); };
위 코드를 실행하면 다음 오류가 발생합니다.
Uncaught TypeError: undefined is not a function
왜요?
컨텍스트에 관한 모든 것입니다. 위의 오류가 발생하는 이유는 setTimeout()
을 호출할 때 실제로 window.setTimeout()
을 호출하기 때문입니다. 결과적으로 setTimeout()
에 전달되는 익명 함수는 clearBoard()
메서드가 없는 window
개체의 컨텍스트에서 정의됩니다.
전통적인 구식 브라우저 호환 솔루션은 클로저에 의해 상속될 수 있는 변수에 this
에 대한 참조를 저장하는 것입니다. 예:
Game.prototype.restart = function () { this.clearLocalStorage(); var self = this; // save reference to 'this', while it's still this! this.timer = setTimeout(function(){ self.clearBoard(); // oh OK, I do know who 'self' is! }, 0); };
또는 최신 브라우저에서는 bind()
메서드를 사용하여 적절한 참조를 전달할 수 있습니다.
Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(this.reset.bind(this), 0); // bind to 'this' }; Game.prototype.reset = function(){ this.clearBoard(); // ahhh, back in the context of the right 'this'! };
일반적인 실수 #2: 블록 수준 범위가 있다고 생각
JavaScript Hiring Guide에서 논의한 바와 같이 JavaScript 개발자들 사이에서 혼동을 일으키는 일반적인 원인(따라서 버그의 일반적인 원인)은 JavaScript가 각 코드 블록에 대해 새로운 범위를 생성한다고 가정하는 것입니다. 이것은 다른 많은 언어에서 사실이지만 JavaScript에서는 사실이 아닙니다 . 예를 들어 다음 코드를 고려하십시오.
for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); // what will this output?
console.log()
호출이 undefined
결과를 출력하거나 오류를 발생시킬 것이라고 추측했다면 잘못 추측한 것입니다. 믿거나 말거나 10
을 출력합니다. 왜요?
대부분의 다른 언어에서 위의 코드는 변수 i
의 "수명"(즉, 범위)이 for
블록으로 제한되기 때문에 오류를 발생시킵니다. 그러나 JavaScript에서는 그렇지 않으며 변수 i
는 for
루프가 완료된 후에도 범위에 남아 있으며 루프를 종료한 후에도 마지막 값을 유지합니다. (이 동작은 부수적으로 가변 호이스팅으로 알려져 있습니다).
그러나 블록 수준 범위 에 대한 지원이 새로운 let
키워드를 통해 JavaScript에 적용되고 있다는 점은 주목할 가치가 있습니다. let
키워드는 이미 JavaScript 1.7에서 사용할 수 있으며 ECMAScript 6부터 공식적으로 지원되는 JavaScript 키워드가 될 예정입니다.
JavaScript가 처음이신가요? 스코프, 프로토타입 등에 대해 읽어보세요.
일반적인 실수 #3: 메모리 누수 생성
메모리 누수를 피하기 위해 의식적으로 코딩하지 않는다면 메모리 누수는 거의 피할 수 없는 JavaScript 문제입니다. 이러한 현상이 발생하는 방법에는 여러 가지가 있으므로 보다 일반적으로 발생하는 몇 가지만 강조하겠습니다.
메모리 누수 예 1: 존재하지 않는 객체에 대한 댕글링 참조
다음 코드를 고려하십시오.
var theThing = null; var replaceThing = function () { var priorThing = theThing; // hold on to the prior thing var unused = function () { // 'unused' is the only place where 'priorThing' is referenced, // but 'unused' never gets invoked if (priorThing) { console.log("hi"); } }; theThing = { longStr: new Array(1000000).join('*'), // create a 1MB object someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000); // invoke `replaceThing' once every second
위의 코드를 실행하고 메모리 사용량을 모니터링하면 엄청난 메모리 누수가 발생하여 초당 전체 메가바이트가 누수된다는 것을 알게 될 것입니다! 그리고 수동 GC도 도움이 되지 않습니다. 따라서 replaceThing
이 호출될 때마다 longStr
이 누출되는 것 같습니다. 하지만 왜?
좀 더 자세히 살펴보겠습니다.
각 Thing 개체는 고유한 1MB theThing
개체를 longStr
합니다. 매초 우리가 replaceThing
을 호출할 때 이는 priorThing
에 있는 theThing
객체에 대한 참조를 유지합니다. 그러나 매번 이전에 참조 priorThing
이 역참조되기 때문에 이것이 문제가 될 것이라고 생각하지 않습니다( priorThing
이 priorThing = theThing;
을 통해 재설정될 때). 또한, replaceThing
의 본문과 실제로 unused
함수에서만 참조됩니다.
그래서 다시 우리는 여기에 메모리 누수가 있는 이유를 궁금해하게 됩니다!?
무슨 일이 일어나고 있는지 이해하려면 내부에서 JavaScript에서 작동하는 방식을 더 잘 이해할 필요가 있습니다. 클로저가 구현되는 일반적인 방법은 모든 함수 개체에 어휘 범위를 나타내는 사전 스타일 개체에 대한 링크가 있다는 것입니다. replaceThing
내부에 정의된 두 함수가 모두 priorThing
을 실제로 사용했다면, priorThing
이 계속해서 할당되더라도 두 함수가 동일한 객체를 가져와서 두 함수가 동일한 어휘 환경을 공유하는 것이 중요합니다. 그러나 어떤 클로저에서 변수를 사용하자마자 해당 범위의 모든 클로저가 공유하는 어휘 환경에서 끝납니다. 그리고 그 작은 뉘앙스가 이 끔찍한 메모리 누수로 이어지는 것입니다. (이에 대한 자세한 내용은 여기에서 확인할 수 있습니다.)
메모리 누수 예 2: 순환 참조
다음 코드 조각을 고려하십시오.
function addClickHandler(element) { element.click = function onClick(e) { alert("Clicked the " + element.nodeName) } }
여기에서 onClick
에는 element
에 대한 참조를 유지하는 클로저가 있습니다( element.nodeName
을 통해). onClick
을 element.click
에 할당하면 순환 참조가 생성됩니다. 예: element
-> onClick
-> element
-> onClick
-> element
...
흥미롭게도 DOM에서 element
가 제거되더라도 위의 순환 자체 참조는 element
와 onClick
이 수집되는 것을 방지하므로 메모리 누수가 발생합니다.
메모리 누수 방지: 알아야 할 사항
JavaScript의 메모리 관리(특히 가비지 수집)는 주로 객체 도달 가능성의 개념을 기반으로 합니다.
다음 객체는 도달 가능한 것으로 간주되며 "루트"로 알려져 있습니다.
- 현재 호출 스택 의 모든 위치에서 참조되는 개체(즉, 현재 호출되는 함수의 모든 지역 변수와 매개변수, 클로저 범위의 모든 변수)
- 모든 전역 변수
개체는 참조 또는 참조 체인을 통해 루트 중 하나에서 액세스할 수 있는 한 최소한 메모리에 보관됩니다.
브라우저에는 연결할 수 없는 개체가 차지하는 메모리를 정리하는 GC(가비지 수집기)가 있습니다. 즉, GC가 도달할 수 없다고 판단하는 경우에만 개체가 메모리에서 제거됩니다. 불행히도 실제로는 더 이상 사용되지 않지만 GC가 여전히 "연결 가능"하다고 생각하는 존재하지 않는 "좀비" 개체로 끝나는 것은 상당히 쉽습니다.
일반적인 실수 #4: 평등에 대한 혼란
JavaScript의 편리함 중 하나는 부울 컨텍스트에서 참조되는 모든 값을 부울 값으로 자동으로 강제 변환한다는 것입니다. 그러나 이것이 편리한 만큼 혼란스러울 수 있는 경우가 있습니다. 예를 들어 다음 중 일부는 많은 JavaScript 개발자를 물어뜯는 것으로 알려져 있습니다.
// All of these evaluate to 'true'! console.log(false == '0'); console.log(null == undefined); console.log(" \t\r\n" == 0); console.log('' == 0); // And these do too! if ({}) // ... if ([]) // ...
마지막 두 개와 관련하여 비어 있음에도 불구하고(이로 인해 false
로 평가될 것이라고 믿게 될 수 있음), {}
및 []
둘 다 사실 객체이고 모든 객체는 JavaScript에서 true
의 부울 값으로 강제 변환됩니다. ECMA-262 사양과 일치합니다.
이 예에서 알 수 있듯이 유형 강제의 규칙은 때때로 진흙탕처럼 명확할 수 있습니다. 따라서 형식 강제 변환이 명시적으로 필요한 경우가 아니면 형식 강제 변환의 의도하지 않은 부작용을 방지하기 위해 일반적으로 ===
및 !==
( ==
및 !=
대신)를 사용하는 것이 가장 좋습니다. ( ==
및 !=
는 두 항목을 비교할 때 자동으로 유형 변환을 수행하는 반면 ===
및 !==
유형 변환 없이 동일한 비교를 수행합니다.)
그리고 완전히 곁가지로서 – 하지만 우리가 유형 강제 및 비교에 대해 이야기하고 있기 때문에 – NaN
을 무엇이든 (심지어 NaN
!)과 비교하면 항상 false
를 반환한다는 점을 언급할 가치가 있습니다. 따라서 등호 연산자( ==
, ===
, !=
, !==
)를 사용하여 값이 NaN
인지 여부를 결정할 수 없습니다. 대신 내장 전역 isNaN()
함수를 사용하세요.
console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(isNaN(NaN)); // true
일반적인 실수 #5: 비효율적인 DOM 조작
JavaScript를 사용하면 DOM을 비교적 쉽게 조작할 수 있지만(즉, 요소 추가, 수정 및 제거) 효율적으로 수행할 수 있습니다.
일반적인 예는 일련의 DOM 요소를 한 번에 하나씩 추가하는 코드입니다. DOM 요소를 추가하는 것은 비용이 많이 드는 작업입니다. 여러 DOM 요소를 연속적으로 추가하는 코드는 비효율적이며 제대로 작동하지 않을 수 있습니다.
여러 DOM 요소를 추가해야 할 때 효과적인 대안 중 하나는 문서 조각을 대신 사용하여 효율성과 성능을 모두 향상시키는 것입니다.

예를 들어:
var div = document.getElementsByTagName("my_div"); var fragment = document.createDocumentFragment(); for (var e = 0; e < elems.length; e++) { // elems previously set to list of elements fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true));
이 접근 방식의 본질적으로 향상된 효율성 외에도 연결된 DOM 요소를 만드는 데 비용이 많이 드는 반면 분리된 상태에서 만들고 수정한 다음 연결하면 훨씬 더 나은 성능을 얻을 수 있습니다.
일반적인 실수 #6: for
루프 내에서 함수 정의의 잘못된 사용
다음 코드를 고려하십시오.
var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example for (var i = 0; i < n; i++) { elements[i].onclick = function() { console.log("This is element #" + i); }; }
위의 코드에 따르면 10개의 입력 요소가 있는 경우 그 중 하나를 클릭 하면 "This is element #10"이 표시됩니다! 이는 요소 중 하나에 대해 onclick
이 호출될 때 위의 for 루프가 완료되고 i
값이 이미 10( 모든 요소에 대해)이 되기 때문입니다.
원하는 동작을 달성하기 위해 위의 코드 문제를 수정하는 방법은 다음과 같습니다.
var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example var makeHandler = function(num) { // outer function return function() { // inner function console.log("This is element #" + num); }; }; for (var i = 0; i < n; i++) { elements[i].onclick = makeHandler(i+1); }
이 수정된 버전의 코드에서 makeHandler
는 루프를 통과할 때마다 즉시 실행되고, i+1
의 당시 현재 값을 수신하고 이를 범위가 지정된 num
변수에 바인딩할 때마다 실행됩니다. 외부 함수는 내부 함수(이 범위의 num
변수도 사용)를 반환하고 요소의 onclick
은 해당 내부 함수로 설정됩니다. 이렇게 하면 각 onclick
이 적절한 i
값을 수신하고 사용하도록 합니다(범위가 지정된 num
변수를 통해).
일반적인 실수 #7: 프로토타입 상속을 적절히 활용하지 못함
놀랍게도 높은 비율의 JavaScript 개발자가 프로토타입 상속의 기능을 완전히 이해하지 못하고 완전히 활용하지 못합니다.
다음은 간단한 예입니다. 다음 코드를 고려하십시오.
BaseObject = function(name) { if(typeof name !== "undefined") { this.name = name; } else { this.name = 'default' } };
상당히 직관적인 것 같습니다. 이름을 제공하는 경우 이름을 사용하고, 그렇지 않으면 이름을 '기본값'으로 설정합니다. 예:
var firstObj = new BaseObject(); var secondObj = new BaseObject('unique'); console.log(firstObj.name); // -> Results in 'default' console.log(secondObj.name); // -> Results in 'unique'
하지만 이렇게 하면 어떻게 될까요?
delete secondObj.name;
그러면 다음을 얻을 수 있습니다.
console.log(secondObj.name); // -> Results in 'undefined'
그러나 이것을 '기본값'으로 되돌리는 것이 좋지 않을까요? 다음과 같이 프로토타입 상속을 활용하도록 원본 코드를 수정하면 이 작업을 쉽게 수행할 수 있습니다.
BaseObject = function (name) { if(typeof name !== "undefined") { this.name = name; } }; BaseObject.prototype.name = 'default';
이 버전에서 BaseObject
는 prototype
개체에서 name
속성을 상속합니다. 여기서 기본적으로 'default'
로 설정됩니다. 따라서 생성자가 이름 없이 호출되면 이름은 기본적으로 default
가 됩니다. 마찬가지로 BaseObject
의 인스턴스에서 name
속성이 제거되면 프로토타입 체인이 검색되고 name
속성은 값이 여전히 'default'
인 prototype
객체에서 검색됩니다. 이제 우리는 다음을 얻습니다.
var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // -> Results in 'unique' delete thirdObj.name; console.log(thirdObj.name); // -> Results in 'default'
일반적인 실수 #8: 인스턴스 메서드에 대한 잘못된 참조 생성
다음과 같이 간단한 개체를 정의하고 개체를 만들고 인스턴스를 만들어 보겠습니다.
var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject();
이제 편의를 위해 더 긴 obj.whoAmI()
whoAmI()
만으로 액세스할 수 있도록 whoAmI
메서드에 대한 참조를 만들어 보겠습니다.
var whoAmI = obj.whoAmI;
그리고 모든 것이 초조해 보이는지 확인하기 위해 새로운 whoAmI
변수의 값을 출력해 보겠습니다.
console.log(whoAmI);
출력:
function () { console.log(this === window ? "window" : "MyObj"); }
그래요 좋아요. 괜찮아 보인다.
하지만 이제 obj.whoAmI()
와 편의 참조 whoAmI()
를 호출할 때의 차이점을 살펴보세요.
obj.whoAmI(); // outputs "MyObj" (as expected) whoAmI(); // outputs "window" (uh-oh!)
무엇이 잘못되었나요?
여기서 가짜는 할당을 수행할 때 var whoAmI = obj.whoAmI;
, 새로운 변수 whoAmI
가 전역 네임스페이스에 정의되었습니다. 결과적으로 this
의 값은 MyObject
의 obj
인스턴스가 아니라 window
입니다!
따라서 객체의 기존 메서드에 대한 참조를 실제로 생성해야 하는 경우 this
값을 보존하기 위해 해당 객체의 네임스페이스 내에서 수행해야 합니다. 이를 수행하는 한 가지 방법은 예를 들어 다음과 같습니다.
var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject(); obj.w = obj.whoAmI; // still in the obj namespace obj.whoAmI(); // outputs "MyObj" (as expected) obj.w(); // outputs "MyObj" (as expected)
일반적인 실수 #9: setTimeout
또는 setInterval
에 대한 첫 번째 인수로 문자열 제공
우선 여기에서 명확하게 합시다. setTimeout
또는 setInterval
에 대한 첫 번째 인수로 문자열을 제공하는 것 자체는 실수가 아닙니다 . 완벽하게 합법적인 JavaScript 코드입니다. 여기서 문제는 성능과 효율성 중 하나입니다. 거의 설명되지 않는 것은 내부적으로 문자열을 setTimeout
또는 setInterval
의 첫 번째 인수로 전달하면 새 함수로 변환될 함수 생성자 에 전달된다는 것입니다. 이 프로세스는 느리고 비효율적일 수 있으며 거의 필요하지 않습니다.
이러한 메서드에 대한 첫 번째 인수로 문자열을 전달하는 것의 대안은 대신 함수 를 전달하는 것입니다. 예를 들어 보겠습니다.
다음은 문자열 을 첫 번째 매개변수로 전달하는 setInterval
및 setTimeout
의 일반적인 사용입니다.
setInterval("logTime()", 1000); setTimeout("logMessage('" + msgValue + "')", 1000);
더 나은 선택은 함수 를 초기 인수로 전달하는 것입니다. 예:
setInterval(logTime, 1000); // passing the logTime function to setInterval setTimeout(function() { // passing an anonymous function to setTimeout logMessage(msgValue); // (msgValue is still accessible in this scope) }, 1000);
일반적인 실수 #10: "strict 모드" 사용 실패
JavaScript 고용 가이드에 설명된 대로 "엄격한 모드"(예: JavaScript 소스 파일 시작 부분에 'use strict';
포함)는 런타임 시 JavaScript 코드에 대해 더 엄격한 구문 분석 및 오류 처리를 자발적으로 시행하는 방법이기도 합니다. 더 안전하게 만들기 때문입니다.
엄격 모드를 사용하지 않는 것이 그 자체로 "실수"는 아니지만, 엄격 모드의 사용이 점점 권장되고 있으며 생략이 점점 더 나쁜 형태로 간주되고 있습니다.
엄격 모드의 몇 가지 주요 이점은 다음과 같습니다.
- 디버깅을 더 쉽게 만듭니다. 그렇지 않으면 무시되거나 자동으로 실패했을 코드 오류는 이제 오류를 생성하거나 예외를 발생시켜 코드의 문제에 대해 더 빨리 경고하고 해당 소스로 더 빨리 안내합니다.
- 우발적인 전역을 방지합니다. 엄격 모드가 없으면 선언되지 않은 변수에 값을 할당하면 해당 이름을 가진 전역 변수가 자동으로 생성됩니다. 이것은 JavaScript에서 가장 흔한 오류 중 하나입니다. 엄격 모드에서 그렇게 하려고 하면 오류가 발생합니다.
-
this
강제를 제거합니다 . 엄격 모드가 없으면 null 또는 정의되지 않은this
값에 대한 참조는 자동으로 전역으로 강제 변환됩니다. 이것은 많은 headfake와 pull-out your hair 종류의 버그를 일으킬 수 있습니다. 엄격 모드에서 null 또는 정의되지 않은this
값을 참조하면 오류가 발생합니다. - 속성 이름 또는 매개변수 값의 중복을 허용하지 않습니다. Strict 모드는 객체에서 중복된 명명된 속성(예:
var object = {foo: "bar", foo: "baz"};
) 또는 함수에 대한 중복 명명된 인수(예:function foo(val1, val2, val1){}
), 따라서 추적하는 데 많은 시간을 낭비했을 수 있는 코드의 버그가 거의 확실한 것을 포착합니다. - eval()을 더 안전하게 만듭니다.
eval()
이 strict 모드와 non-strict 모드에서 작동하는 방식에는 몇 가지 차이점이 있습니다. 가장 중요한 것은 엄격 모드에서eval()
문 내부에 선언된 변수와 함수는 포함 범위에서 생성 되지 않는다는 것입니다(비엄격 모드에서는 포함 범위에서 생성되며 문제의 일반적인 원인이 될 수도 있음). -
delete
를 잘못 사용하면 오류가 발생합니다. 개체에서 속성을 제거하는 데 사용되는delete
연산자는 개체의 구성할 수 없는 속성에 사용할 수 없습니다. 비 엄격 코드는 구성할 수 없는 속성을 삭제하려고 하면 자동으로 실패하는 반면, 엄격 모드에서는 이러한 경우 오류가 발생합니다.
마무리
모든 기술이 그렇듯이 JavaScript가 작동하고 작동하지 않는 이유와 방법을 더 잘 이해할수록 코드가 더 견고해지고 언어의 진정한 힘을 효과적으로 활용할 수 있게 됩니다. 반대로 JavaScript 패러다임과 개념에 대한 적절한 이해 부족은 실제로 많은 JavaScript 문제가 있는 곳입니다.
언어의 뉘앙스와 미묘함에 완전히 익숙해지는 것이 숙달을 향상시키고 생산성을 높이는 가장 효과적인 전략입니다. 많은 일반적인 JavaScript 실수를 피하면 JavaScript가 작동하지 않을 때 도움이 됩니다.