인사이트

인사이트

새로운 관점으로 깊이 있는 통찰을 제시합니다.

SW 테크놀로지

정적 분석으로 자바스크립트 코드의 오류와 코드 스멜(Code Smell) 찾기

2018.04.10김강호
다운로드

자바스크립트는 근래 전 세계적으로 가장 인기 있는 프로그래밍 언어입니다. 특히 오픈 소스 생태계에서의 사용이 활발하여 2016년부터는 깃허브(GitHub)에서 최고 인기 언어로 선정되고 있기도 하죠. 자바스크립트는 웹 사이트뿐 아니라 서버, 모바일 및 데스크톱 애플리케이션 개발을 아우르는 대표적인 크로스 플랫폼 언어로 큰 관심을 받고 있습니다.

하지만 자바스크립트로 작성된 코드 베이스가 커져 가고 다양하고 파편화된 기술들이 등장하면서 코드를 디버깅하고 관리하는 품질 비용은 급격히 증가하고 있습니다. 자바스크립트의 약한 타입 시스템이나 동적 특성 외에도 코드 문제를 즉시 확인해주는 컴파일러(compiler)의 부재는 더욱 코드 품질 관리를 어렵게 합니다. 타입 검사(type checking)를 지원하는 타입스크립트나 Flow를 사용하면 버그의 15%를 예방할 수 있다는 연구 결과도 있을 정도입니다.

전통적으로 정적 분석 도구는 이런 품질 비용을 줄이는 데 유용한 역할을 했습니다. 연구에 따르면 제품 출시 후 발견된 결함을 수정하는 비용은 코딩이나 단위 테스트 단계에서보다 6배 가량 더 많다고 합니다. 정적 분석 도구는 개발 초기 단계에서부터 코드 결함을 발견하여 C/C++이나 Java 같은 언어에 대한 품질 비용을 크게 줄여왔습니다.

그렇다면 자바스크립트에 정적 분석 도구를 적용해 보는 건 어떨까요?

자바스크립트 개발에서도 정적 분석 도구를 통해 품질 비용을 낮추고 개발의 어려움을 줄일 수 있습니다. 자바스크립트는 그 언어 특성(약한 타입 시스템과 동적인 동작)으로 인해 정적 분석을 적용하기 어려운 것으로 알려져 왔지만 최근에 개발되는 정적 분석 도구는 이러한 문제를 극복하고 있습니다.

본 아티클을 통해 정적 분석 도구가 자바스크립트 개발에 어떤 도움을 줄 수 있는지 소개하고자 합니다. 정적 분석 도구의 작동 방식과 코드 결함의 예시를 통해 정적 분석 도구로써 예방할 수 있는 문제에 대해 알아보겠습니다. 끝으로 정적 분석 도구를 선택할 때 고려해야 할 사항들로 마무리합니다.

 

 

정적 분석의 작동 방식

먼저 정적 분석 도구를 이해하는 데 필요한 개념을 알아보겠습니다.

정적 분석 도구는 컴파일러와 비슷하게 작동합니다. 컴파일러처럼 소스 코드를 분석하여 프로그램의 추상 구문 트리(Abstract Syntax Tree, AST)와 기호 테이블(symbol table)을 생성합니다. 이 추상 구문 트리로부터 중간 표현(Intermediate Representation, IR)이 생성되며, 중간 표현에서 제어 흐름 그래프(Control Flow Graph, CFG)가 구성됩니다.

그림 1 - 정적 분석 도구의 아키텍처_컴파일러와 같이 JS File, Parser를 통해 소스 코드를 분석하여 프로그램의 추상 구문 트리(Abstract Syntax Tree, AST)와 기호 테이블(symbol table)을 생성합니다. 이 추상 구문 트리로부터 중간 표현(Intermediate Representation, IR)이 생성되며, 중간 표현에서 제어 흐름 그래프(Control Flow Graph, CFG)가 구성되는 개요도

 

그림 2 - 중간 표현(IR)의 예_JavaScript Code 에서 의심스러운 코팅 패턴을 탐지하여 Intermediate Representation에서 Null pointer dereference, 잘못된 함수호출등의 문제를 찾아내는 그림.

정적 분석 도구는 이렇게 소스 코드로부터 구축된 구문 트리 및 흐름 그래프를 활용하여 특정 속성이나 의심스러운 코딩 패턴을 탐지합니다. 반면 간단한 분석만 가능한 단순 코드 분석 도구(JSHint 또는 ESLint 같은 linters)는 구문 트리만 구성하고 패턴에 일치하는 구문이나 스타일 문제를 찾습니다. 예를 들어, ‘with’ 문의 사용을 탐지하는 경우는 구문 트리에서 ‘with’ 노드의 존재 여부만 확인하면 찾을 수 있는 문제가 되죠.

정적 분석 도구는 단순 코드 분석 도구와 다르게 구문 트리 외에도 흐름 그래프를 구성하고 전체 프로그램의 실행 흐름을 고려해(데이터 흐름 분석) 코드를 검사합니다. 변수의 현재 값이나 조건문(conditional statement)에서의 가능 조건과 같은 프로그램의 추상화된 상태를 흐름 그래프를 통해 추적하여 널 포인터 역참조(NULL pointer dereference)나 모듈 간의 잘못된 함수 호출 같이 더 어렵고 실제적으로 유익한 문제를 찾아낼 수 있습니다.

 

 

정적 분석 도구와 자바스크립트 코드 결함

정적 분석 도구로 예방할 수 있는 여러 유형의 코드 결함을 예시와 함께 다뤄보겠습니다.

최근 Rollbar(웹 애플리케이션의 실시간 오류 모니터링 서비스)에서 상위 10개의 자바스크립트 오류를 발표했습니다. 이 실행 오류는 실제 운영 중인 웹 사이트에서 수집된 것으로 만약 정적 분석 도구가 개발 단계에서 미리 이 오류를 찾고 예방할 수 있었다면 품질 비용을 낮추고 사용자 경험을 향상시킬 수 있었을 것입니다.

이 실행 오류 중 [그림 3]은 범위를 벗어난 값이 함수에 전달되었을 때 발생하는 RangeError의 예를 보여 주고 있습니다. 예를 들어 ‘Number.toFixed()’는 0에서 20까지의 인자만 허용하기 때문에 아래의 ‘Number.toFixed(25)’는 RangeError가 발생합니다.

그림 3 - RangeError 예시 (Rollbar)_구문 트리를 통해 호출할 함수가 ‘toFixed’인지와 해당 인자를 탐지할 수 있습니다. 하지만 함수에 전달된 인자가 상수가 아닌 변수라면 변수 상태를 추적 가능한 정적 분석 도구만 이 RangeError를 탐지

이러한 오류는 정적 분석 도구와 단순 코드 분석 도구 모두가 구문 트리를 통해 호출할 함수가 ‘toFixed’인지와 해당 인자를 탐지할 수 있습니다. 하지만 함수에 전달된 인자가 상수가 아닌 변수라면 변수 상태를 추적 가능한 정적 분석 도구만 이 RangeError를 탐지할 수 있습니다.

또 다른 실행 오류의 예는 널(null) 객체를 참조하여 발생하는 TypeError입니다.

그림 4 - TypeError 예시 (Rollbar)_행을 보면 ‘testFunction()’이 인자 없이 호출되고 있습니다. 그 결과 ‘testArray’ 인자가 정의되지 않은 값(undefined)을 갖게 되므로 루프(loop)에서 ‘length’ 속성에 접근할 때 TypeError가 발생 탐지 예시 그림.

[그림 4]의 9행을 보면 ‘testFunction()’이 인자 없이 호출되고 있습니다. 그 결과 ‘testArray’ 인자가 정의되지 않은 값(undefined)을 갖게 되므로 루프(loop)에서 ‘length’ 속성에 접근할 때 TypeError가 발생하게 됩니다.
정적 분석 도구를 사용하면 위와 같은 오류도 해결할 수 있습니다. 정적 분석 도구가 함수의 의미와 그 호출 정보를 알고 있기 때문이죠. 따라서 위 예제에서는 원인(9행에서 인자가 누락된 채로 함수가 호출됨)과 오류 지점(4행에서 널 객체의 속성이 참조됨)을 미리 알려 줄 수 있습니다.

[그림 4]의 오른쪽 예는 이 널 포인터 문제의 간략화된 패턴을 보여줍니다. 정적 분석 도구는 변수 ‘test’가 1행에서의 할당 때문에 정의되지 않은 값을 갖게 된다는 것을 인식하고 2행에서 널 포인터가 참조되는 문제를 탐지할 수 있습니다.

마지막으로 [그림 5]를 통해 널 포인터 문제의 또 다른 패턴을 볼 수 있습니다.

그림 5 - 불충분한 널 체크의 예 (Apache Amban)_2행에서 ‘data’ 인자가 널인지 체크하고 있습니다. 하지만 널로 체크된 경우에도 실행이 멈추지 않기 때문에 널 값이 그대로 저장되고 결국 6행에서 ‘data’ 객체에 접근할 때 TypeError가 발생 탐지 그림.

2행에서 ‘data’ 인자가 널인지 체크하고 있습니다. 하지만 널로 체크된 경우에도 실행이 멈추지 않기 때문에 널 값이 그대로 저장되고 결국 6행에서 ‘data’ 객체에 접근할 때 TypeError가 발생하게 됩니다.
정적 분석 도구는 이렇게 변수의 널 체크가 일관되게 적용되지 않는 상황도 실행 흐름 상의 변수 사용을 추적하여 미리 찾아낼 수 있습니다.

이제 깃허브에 있는 수천 개의 공용 자바스크립트 및 타입스크립트 프로젝트를 분석하여 정적 분석 도구로 수집한 코드 결함 사례와 정적 분석 도구를 선택할 때 고려해야 할 사항들을 살펴보겠습니다.

먼저 상위 10가지 코드 결함은 다음과 같습니다.

그림 6 - 상위 10가지 코드 결함_ Assigned variable is not used, Function or variable is redeclared, Local variable or function is not used, Result of condition check is always same, Imported binding is not used, Result of expression is not used, Undefined object can be accessed, Uninitialized local variable is used, Required module is not used, Execution of switch/case does not break.

그림 7 - 유형별 코드 결함_ Assigned variable is not used (25%), Function or variable is... (12%), Local variable or function is not... (9%), Result of condition check is always same (9%), Imported binding is not used (9%), Result of expression is not used (6%), Undefined object can be accessed (4%), Uninitialized local variable is used (2%), Required module is not used (2%), Execution of switch/case does... (2%), Others (20%)

상위 코드 결함 중 대부분은 미사용 변수, 초기화되지 않은 로컬 변수 및 널 포인터와 연관이 있습니다.
각 경우에 대해 자세히 살펴보도록 하겠습니다.

 

미사용 변수(Unused Variables)
미사용 변수란 선언/정의만 하고 사용되지 않거나 사용 전에 재정의(할당)되는 변수를 말합니다. 개발자는 간과하는 경우가 많지만 이 미사용 변수는 추후 코드 베이스의 유지 관리를 어렵게 만드는 요인이 됩니다. 외관상 사소해 보이는 문제이지만 원하지 않는 결과로 이어질 수 있는데, 아래 [그림 3]을 보면 3행에서 ‘target’ 변수에 할당된 값이 4행에서의 재할당으로 무시되어 url에 ‘userName’ 매개 변수가 추가되지 않은 채로 치환됩니다.

그림 8 - 미사용 변수의 예_3행에서 ‘target’ 변수에 할당된 값이 4행에서의 재할당으로 무시되어 url에 ‘userName’ 매개 변수가 추가되지 않은 채로 치환되는 이미지

 

중복 변수와 함수(Duplicated Variables or Functions)
변수나 함수를 중복 정의하는 경우인데, 이것은 개발자가 자바스크립트 언어나 새 기능에 대해 잘 모르고 있다는 징후일 수도 있습니다. 자바스크립트는 중복 정의에 대해 에러를 내지 않고 마지막에 정의된 것만 유효하며 함수 오버로딩을 지원하지 않는데, 특히 오버로딩(overloading)을 지원하는 기존 언어에 익숙한 개발자의 경우에 혼동하는 경우가 많습니다.
이 결함으로 인해 실행 오류가 발생하지는 않지만 중복된 코드는 제거하는 것이 좋은 코딩 습관입니다. 중복 변수와 함수로 인해 어떤 것이 실제로 사용되는지 혼동을 가져와 유지 보수성 (maintainability)을 저하시키고 기술 부채가 증가하기 때문입니다.

초기화되지 않은 변수(Uninitialized Variables)
초기화되지 않은 변수를 사용하는 경우로서 ‘var’ 변수의 스코프(scope)와 함수 호이스팅(hoisting)에 대한 이해 부족 때문에 종종 발생합니다.
아래 [그림 4]에서 ‘html5’ 변수는 2행과 5행에서 선언되고 있습니다. ‘var’로 선언된 변수는 함수 범위를 갖기 때문에 두 변수는 다르게 취급되는데 5행의 변수만 초기화되고 2행의 변수는 초기화되지 않았습니다. 따라서 2행의 변수를 반환하게 되면 호출자(caller)는 기대한 값을 받지 못해 예기치 않은 동작이 발생할 수 있습니다.

그림 9 - 초기화 되지 않은 변수의 예_html5’ 변수는 2행과 5행에서 선언되고 있습니다. ‘var’로 선언된 변수는 함수 범위를 갖기 때문에 두 변수는 다르게 취급되는데 5행의 변수만 초기화되고 2행의 변수는 초기화되지 않았습니다. 따라서 2행의 변수를 반환하게 되면 호출자(caller)는 기대한 값을 받지 못해 예기치 않은 동작이 발생

 

부주의한 널 체크(Incautious NULL Checking)
변수나 인자의 널을 체크하는 상황에서 TypeError 발생 가능성이 높습니다. 특히 예외적인 유스 케이스(코드의 else 분기문)에서 더 그런데 이는 개발자가 일반적으로 주된 유스 케이스(코드의 if 분기문) 또는 정상적인 인자 값만 테스트하기 때문입니다. 이와 같이 널 체크를 제대로 하지 않으면 다양한 유스 케이스를 갖는 사용자 경험을 방해하게 됩니다.
아래 [그림 5]을 보면 변수 ‘match’가 9행의 else 분기문에서 널 값을 갖게 되고 12행에서 역참조되어 TypeError가 발생합니다. 결과적으로 사용자의 특정 유스 케이스(에디터에서 마크를 하나도 추가하지 않은 상황)에서 에러가 발생하는 좋지 않은 영향을 미칩니다.

그림 10 - 부주의한 널 체크의 예_변수 ‘match’가 9행의 else 분기문에서 널 값을 갖게 되고 12행에서 역참조되어 TypeError가 발생

 

지금까지 코드 결함 사례를 살펴봤는데, 특기할 점은 위의 예시와 통계가 저장소(repository)에 최종 커밋된 코드로부터 나왔다는 것입니다. 즉, 개발자 자신 혹은 동료의 코드 리뷰를 거쳤다는 것이고 사람에 의한 코드 리뷰로는 코드 결함 예방이 불완전함을 의미합니다. 정적 분석 도구는 감정이나 주변 여건에 영향을 받는 개발자와 달리 일관적이고 자동화된 방식으로 문제를 발견하고 수십만 라인 이상의 코드도 빠르게 분석하여 이 과정을 보완할 수 있습니다.
또, 데이터 흐름 분석을 지원하는 도구는 결함 원인을 정확하게 파악해 주기 때문에 개발자는 빠르고 쉽게 결함을 수정할 수 있습니다.
위 [그림 5]의 예제에 대해 정적 분석 도구는 다음과 같이 널 포인터 문제(널 객체가 이 지점에서 참조됩니다)와 그 원인(9행에 있는 할당문 때문입니다)을 출력해 줄 수 있습니다.

추가적으로 결함 수정 시간에 대한 정적 분석 도구의 통계를 확인해 보겠습니다.
결함 수정 시간은 결함이 처음 발견된 시점부터 해결(코드 수정, 제거, 선별)될 때까지의 시간을 평균한 것입니다.

그림 11 - 평균 결함 수정 시간_ Defects fixed quickly-Property of primitive value is accessed: 0.2hours, Invalid this in strict mode: 0.8hours, Invalid event handling in React event handler: 3.1hour, Strict mode is not declared properly: 3.6hour, Assignment operaator in conditional statement: 4.2hour. Defects fixed more slowly-Non-function object is called as a function: 39days, Useless React PropTypes declaration: 49.3days, Function or variable is redeclared: 50.3days, Same implementation in the conditional branches: 50.5days, Duplicate case clauses: 51.9days

개발자들이 느리게 수정하는 결함을 보면 미사용 PropTypes, 재선언된 ‘var’ 변수, if/else 분기문에서의 동일한 구현과 같은 결함이 있고 이런 종류의 결함은 상대적으로 개발자가 중요한 문제로 간주하지 않는다고 생각할 수 있습니다. 하지만 개발자 의도와 다르게 발생하는 명백한 실행 오류나 실수는 중요한 문제로 간주되고, 널 포인터, 부주의한 널 체크, 잘못된 Strict 모드, React에서의 잘못된 이벤트 처리 (React는 DOM과 다른 자체 이벤트 시스템을 가집니다), 조건문에서의 할당 연산자(비교 연산자를 잘못 쓴 경우) 같은 결함은 개발자가 빠르게 수정하는 것으로 파악됩니다.
이 결과를 보면 정적 분석 도구는 실행 오류 및 코드 품질에 영향을 미치는 유의미한 결함을 발견할 때만 개발자가 저항 없이 사용한다는 사실을 알 수 있습니다. 전체 자바스크립트 프로그램의 데이터 흐름을 이해하는 정적 분석 도구만이 이런 결함을 찾아냄으로써 개발팀의 생산성을 향상시키고 비용 대비 만족스러운 효과를 낼 수 있겠습니다.

 

정적 분석 도구 선택 Tip
지금까지 정적 분석 도구가 자바스크립트의 코드 결함을 얼마나 효과적이고 효율적으로 탐지할 수 있는지 살펴보았습니다. 전체 자바스크립트 프로그램을 이해할 수 있도록 설계된 정적 분석 도구가 할 수 있는 일은 다음과 같습니다.

– 단순 코드 분석 도구가 찾을 수 없는 코드 결함의 탐지

– 개발 초기 단계에서부터 실행 오류 및 코드 품질 저하를 예방

– 자바스크립트 개발자/테스터의 잘못된 코딩 관행을 바로잡아 프로그래밍 스킬 향상을 도움

– 개발자가 놓칠 수 있는 코드 결함을 탐지합니다.

본 아티클의 예는 최종 코드 리뷰를 마친 코드로부터 발췌한 것입니다. 코드 리뷰(peer review)는 매우 유용하나 사람이 대량의 전체 프로그램을 쉽게 이해할 수는 없기 때문에 기계적인 일관성과 빠른 속도를 지닌 자동 분석 도구로 보완되어야 함

다음 체크 리스트에서 자바스크립트 정적 분석 도구의 채택 및 도입에 대한 권장 사항을 제시하였습니다.

표. 자바스크립트 정적 분석 도구의 채택 및 도입에 대한 권장사항을 확인하기 위한 체크리스트. 데이터 흐름 분석-정적 분석 도구는 자바스크립트 모듈 사이의 데이터 흐름을 인식해야 합니다. 자바스크립트 코드 베이스가 커지고 모듈이 세분화되면서 이런 분석 기능이 더욱 중요해지고 있습니다. 낮은 오탐지율-정적분석도구가 발견한 결함이 오탐으로 판명될 경우 해당 도구에 대한 개발자의 신뢰가 저하됩니다. 따라서 정적 분석도구는 실제 발생 가능한 결함을 예방하면서도 오탐지율을 낮게 유지해야 합니다, 최신 기술 지원-자바스크립트 생태계는 매우 빠르게 변하고 있으며 적극적으로 최신 기술을 수용하는 경향을 보입니다. 정적 분석 도구는 최신 자바스크립트 사양과 타입스크립트, Flow, JSX, React, Vun.js같이 근래 많이 사용되는 기술을 지원해야 다양한 개발 프로젝트에서 활용할 수 있습니다. 개발 워크플로우의 통합- 정적 분석도구는 개발자의 잘못을 찾아내어 비난하기 위한 도구로 자칫 인식될 수 있습니다. 또, 개발이 많이 진척된 단계에서 처음 적용하면 수정해야 할 결함이 많이 나오고 기능 회귀에 대한 부담이 커집니다. 따라서 에디터 플러그인이나 명령행 도구 같이 개발자가 일상적인 개발에 쉽게 적용하여 도움을 받는 도구로 활용할 수 있는 방법이 제공되어야 합니다.

결론적으로, 자바스크립트를 위한 정적 분석 도구를 개발/테스트 프로세스나 제품 검수 기준에 도입함으로써 웹 애플리케이션에 대한 신뢰성 보장을 기대할 수 있습니다. 자바스크립트 사용의 확대와 함께 웹 개발 및 프로젝트 관리 팀의 생산성 향상과 품질 비용 절감에 대한 요구가 커지면서 정적 분석 도구의 필요성도 더욱 커질 것입니다.
정적 분석 도구를 활용해 더 즐거운 자바스크립트 개발을 시작하시길 바랍니다!

김강호 프로

에스코어㈜ 소프트웨어사업부 개발플랫폼그룹

타이젠 웹 SDK의 개발 리더를 역임했고 현재 자바스크립트 정적 분석 솔루션인 DeepScan의 제품 관리자를 맡고 있습니다. DeepScan 서비스(https://deepscan.io)를 통해 개발자와 관계를 맺고 싶어 합니다.