JavaScript 문제 해결

#1: 웹페이지 JavaScript 로딩 전략

웹페이지를 의도대로 작동하게 하려면 JavaScript 로딩을 적절한 때 해야한다. 브라우저는 HTML을 분석할 때 순서대로 작업하기 때문에 경우에 따라 블록 타임이 발생하기도 하고, 의존하는 코드가 로드되지 않아 작동하지 않는 경우도 있다.

전통적인 Internal JavaScript 방식에서는, 최대한 블록타임을 미루기 위해 <script> 태그를 </body> 태그 직전에 배치하는 방식을 사용하여 DOMContentLoaded 이벤트 핸들러를 통해 HTML 본문이 완전히 로드되었을 때 스크립트를 실행할 수 있도록 제어하는 패턴을 사용했다. 그러나 HTML 파싱이 완료되고 나서야 JavaScript를 가져와서 실행했기 때문에 대규모 웹사이트에서는 성능 문제가 발생했다. 이후 도입된 asyncdefer 속성은 지금도 Internal JavaScript 방식을 지원하지 않기 때문에 사용할 수 없다.

이후 Script Blocking 문제를 해결하기 위해 async, defer 기능이 도입되었고, 기본적으로 External JavaScript 방식을 사용한다.

async 속성은 스크립트 다운로드가 HTML 파싱을 차단하지 않도록 한다. 다운로드가 완료될 때 HTML 파싱 작업 중이라면 스크립트 실행을 우선 하고 파싱 작업을 계속한다. 따라서 여러개의 파일을 로드하는 경우, 실행 순서를 보장할 수 없다는 특성이 있다. 그렇기 때문에 페이지의 다른 스크립트에 의존하지 않고 독립적으로 즉시 실행되야 하는 스크립트에 사용하는 것이 적절하다.

defer 속성은 <script> 태그를 배치한 순서대로 실행한다. 비동기 속성처럼 다운로드와 HTML 파싱 작업이 동시에 진행되지만, 스크립트 실행은 HTML 파싱이 완료된 후에 순차적으로 실행된다. 따라서 HTML 파싱을 기다렸다가 다른 스크립트 또는 DOM에 의존하는 스크립트에 적절한 순서를 부여하여 사용하는 것이 적절하다.

#2: 잘못된 this를 참조하는 문제

일반적인 프로그래밍 언어에서 this 키워드는 클래스로 생성된 인스턴스를 참조하는 역할이지만, JavaScript에서는 실행 컨텍스트 객체를 참조하는 기능을 한다. 따라서 어떤 상황에서 키워드가 사용되었는지에 따라 참조하는 실행 컨텍스트가 달라진다. 나아가 소프트웨어 복잡도가 늘어나면서, 자연스레 this가 어떤 실행 컨텍스트를 지칭하는지 파악하기는 점점 더 어려워지고, 엉뚱한 실행 컨텍스트를 참조하는 문제가 발생하기 쉬워진다.

고전적인 해결책은 this를 다른 변수에 할당하여 사용하는 방법이다. 그러나 이 방법은 this가 참조하는 실행 컨텍스트를 파악하기 어렵고, this를 사용하는 모든 곳에 변수를 추가해야 하기 때문에 코드량이 증가한다.

이후 bind() 메서드를 통해 원하는 참조를 전달하는 방식이 등장했다. bind() 메서드는 this 키워드가 참조하는 실행 컨텍스트를 전달받은 인자로 고정시키는 역할을 한다. 그러나 bind() 메서드는 this가 참조하는 실행 컨텍스트를 고정시키는 것 외에도, 함수의 인자를 고정시키는 역할도 수행한다. 따라서 bind() 메서드를 사용하면 함수의 인자를 고정시키는 부작용이 발생할 수 있다.

#3: Block Scope 혼동 문제

변수 호이스팅 동작 때문에 var 키워드로 선언한 변수는 할당된 값이 블록을 벗어나도 유지되는 문제가 있다. let 키워드는 선언하면 블록 스코프를 지원한다.

#4: 메모리 누수 유발 문제

구형 자바스크립트 엔진은 실질적으로 사용하지 않는 클로저 정보를 메모리에 계속 유지하는 문제로 인해 함수 호출 횟수가 증가하면 메모리 누수가 발생하여 성능이 저하되는 경향이 있었다.

반면에, 최신 JavaScript 엔진의 GC는 객체 접근성 개념을 기반으로 한다. 따라서 순환 참조 같은 경우, 실질적인 사용이 끝나도 서로를 참조하기 때문에 GC 대상에서 제외되어 메모리 누수를 유발한다.

따라서 큰 객체가 있는 경우, 객체 작업이 끝나면 지역 변수가 더 이상 객체를 가리키지 않는지 확인하는 것이 중요하며, 이런 버그는 포착하기 어렵기 때문에 JavaScript 엔진에서 이런 상황을 만들지 않도록 하는 것을 권장한다.

#5: 타입 강제로 인한 비교 연산 혼동 문제

// JavaScript는 동등 연산자(==)를 사용할 때, 타입 강제를 수행한다.
console.log(false == '0');
console.log(null == undefined);
console.log(" \t\r\n" == 0);
console.log('' == 0);

// 빈 객체와 빈 배열도 객체이기 때문에 true를 반환한다.
if ({}) // ...
if ([]) // ...

JavaScript는 참/거짓 상황에서 모든 값을 강제로 Boolean 값으로 표현한다. 이런 의도적인 모호함으로 인해 개발 편의성과 실수 가능성이 공존한다. 따라서 ==, != 연사자를 사용하지 않고, ===, !== 연산자를 사용하는 것이 실수를 줄이는 방법이다.

또한 NaN을 다른 값과 비교하면 언제나 false를 반환하기 때문에 isNaN() 함수를 사용하는 것이 좋다.

#6: 비효율적인 DOM 조작

DOM 조작은 브라우저의 렌더링 엔진이 수행하는 컴퓨팅 비용이 많이 드는 작업이기 때문에, DOM 조작이 빈번하게 발생하면 브라우저의 성능이 저하된다. 따라서 DOM 조작을 최소화하는 것을 권장한다.

그러나 종종 JavaScript 코드로 여러 DOM 요소를 연속적으로 추가해야 하는 일이 발생하는데, 이 경우 document fragment 인터페이스를 사용하여 가독성을 늘리고, 요소를 분리된 상태에서 생성/수정한 DOM 갱신 작업을 한번만 수행하도록 한다.

참고자료

Last updated