Octoping의 블로그
article thumbnail

시작에 앞서

시대의 흐름은 변화무쌍하여, Javascript의 생태계를 엄청나게 바꾸어놓은 ES6(ECMA2015) 스펙이 출몰한지 이제 8년이 되었다.

여러 회사들이 프론트엔드 채용 공고에 이제는 React를 올려놓고 있으며, 신입 채용에서 우대사항이었던 Typescript는 이제 슬슬 자격 요건으로까지 내려오고 있다.

하지만 그럼에도, 세상의 어떤 곳에는 아직도 ES5 이하 버전으로 개발하며, jQuery를 사용하는 회사들이 우리와 함께 숨쉬고 있다.

이런 이야기를 남들에게 하면 한숨을 쉬며 얼른 다른 회사로 이직을 하라는 조언의 목소리를 보내겠지만, 그럼에도 회사가 굴러가기 위해서는 누군가는 이런 작업을 해내야 한다.

그렇다면 우리 회사는 애초부터 안 좋은 스택을 쓰고 있었으니.. 하며 그저 손 놓고 악몽같은 기술 스택을 받아들여야만 하는 것일까?

다 죽은 미라 같은 프로젝트에 심장 제세동기를 붙이는 방법을 알아보자.

가장 먼저.. npm 환경 구성하기

 

npm

Bring the best of open source to you, your team, and your company Relied upon by more than 17 million developers worldwide, npm is committed to making JavaScript development elegant, productive, and safe. The free npm Registry has become the center of Java

www.npmjs.com

개념

npm은 Node.js 환경에서 패키지를 관리할 수 있는 도구다. Python에서의 pip, Java에서의 Gradle을 생각해보면 이해가 쉬울 것 같다.

물론 yarn, pnpm과 같은 대체제도 많이 있지만, 일단 가장 대중적인 npm에 대해 얘기해보려고 한다.

장점

  1. 기본적으로 패키지 관리자이기 때문에 라이브러리를 설치하거나 삭제하고, 업데이트하는 과정이 매우 매우 간단하다. 설치되어있는 라이브러리는 package.json이라는 파일에 버전 정보가 명시되니, 버전 정보를 확인하는 데에 있어서도 큰 도움이 된다.
  2. Node.js라는 어마어마한 생태계의 지원을 받아 스크립트를 실행할 수 있다. 이를 통해 추후 소개할 라이브러리들과 함께 활용해서, 프로젝트의 빌드, 테스트, 실행, 빌드 후 작업 등을 매우 간단하게 설정하고 사용할 수 있다.

npm을 사용하지 않는 환경에서 npm을 사용한 환경으로 들어섰다는 것은, 솔직히 메모장으로 코딩을 하다가 vsCode에서 코딩을 하는 것과 동일한 수준의 격변이라고 생각한다.

앞으로 소개할 모든 툴들을 위한 초석이 되는 작업이다.

Babel으로 구형 브라우저에서도 최신 문법 사용하기

 

Babel · Babel

The compiler for next generation JavaScript

babeljs.io

개념

Chrome과 같이 자동 업데이트를 지원하는 브라우저들이 대세를 차지하기 전까지, Javascript 생태계에는 참 어려운 점이 많았다.

왜? 아무리 좋은 최신 문법을 만들었어도 사용자들이 브라우저를 직접 수동으로 업데이트 하지 않는다면 말짱 도루묵이니까. 하지만 이제는 브라우저들이 자동 업데이트를 지원하기 시작해서 이런 문제는 많이 사라지게 되었다.

하지만 공공기관에서 사용하는 사이트라던가, 구형 모바일 기기를 사용하는 사람들을 위한 웹의 경우는 여전히 이런 애로사항에 시달릴 수 밖에 없다.

공공기관은 아직 ie를 사용하는 곳들이 있고, 모바일 기기 같은 경우 Android Web View는 자동 업데이트를 지원하지 않으니까!

이러한 슬픔으로 인해 아직 ES5 이하 버전과 함께 코딩을 하는 사람들이 많다 (우리 회사가 그렇다).

하지만 Babel과 함께라면 구형 브라우저 지원 또한 두렵지 않다.

Babel은 자바스크립트 컴파일러다. 물론 Javascript는 실행할 때 컴파일이 필요 없는 언어다. 정확히 말해, Babel은 Javascript를 구버전 Javascript으로 트랜스파일 해주는 컴파일러다.

ES6+ 문법을 Babel로 트랜스파일링한 결과물

ES6+의 최신 문법으로 짜인 코드 (솔직히 최신이라고 하긴 뭐하지만)를 넣고 Babel을 실행시킬 경우, 오른쪽과 같이 옛날 문법의 코드로 컴파일해준다.

이를 통해 최신 문법을 잔뜩 사용한 후, 마지막에 Babel을 사용해서 빌드를 한 후 나온 결과물을 배포해준다면 얼마든지 구형 브라우저를 위한 대응이 가능하다.

장점

  1. 구형 브라우저를 타겟으로 하는 여부와 상관 없이 Javascript의 최신 문법을 활용 가능하다.
  2. ReactTypescript를 사용할 때에 같이 활용이 가능하다.

ESLint로 Javascript 코드 정적 분석하기

 

Find and fix problems in your JavaScript code - ESLint - Pluggable JavaScript Linter

A pluggable and configurable linter tool for identifying and reporting on patterns in JavaScript. Maintain your code quality with ease.

eslint.org

내가 짠 코드에 대해 언제나 강력한 신뢰를 할 수 있는가? 나는 그렇지 않다. 만약 그런 사람이 있다면, ESLint를 설치한다면 그 신뢰가 사라질 것이다.

개념

What is ESLint?
*ESLint is a configurable JavaScript linter. It helps you find and fix problems in your JavaScript code. Problems can be anything from potential runtime bugs, to not following best practices, to styling issues.

ESLint는 구성 가능한 자바스크립트 린터입니다. JavaScript 코드에서 문제를 찾아 수정하는 데 도움이 됩니다. 문제는 잠재적인 런타임 버그부터 모범 사례를 따르지 않는 것, 스타일링 문제까지 다양합니다.*

 

ESLint는 Microsoft에서 만든 Javascript 코드 정적 분석 플러그인이다.

도달할 수 없는 코드, 선언만 되고 사용되지 않은 변수, 중복된 import와 같은 코드 악취부터, 세미콜론 미사용, 탭의 스페이스바 개수, 줄 바꿈과 같은 스타일링 문제와 같이 다양한 사항에 대해 문제를 탐지하고 지적해준다.

이를 통해 문제 발생 여지가 있는 코드를 배포 전에 탐지하는 것부터, 단순 코딩 컨벤션의 미스매치까지 폭 넓게 커버할 수 있다.

장점

  1. 코드 상의 에러 발생 가능성, 코딩 컨벤션 미준수와 같은 문제를 코드 빌드 시점에 탐지해준다.
  2. 다음에 얘기할 Prettier와 같이 사용하면 효율이 배가 된다.

Prettier로 코딩 컨벤션 통일하기

 

Prettier · Opinionated Code Formatter

Opinionated Code Formatter

prettier.io

개념

Prettier는 코드 포맷터이다.

Javascript는 언어 특성 상 자유도가 높기 때문에 개발자 간에 코딩 스타일이 다양할 수 밖에 없다. 누군가는 화살표 함수의 파라미터에 괄호를 사용하길 원할 수도 있고, 탭 1개에 스페이스바 4개를 원할 수도 있고, 세미콜론을 붙이길 원치 않을 수도 있다.

Prettier를 사용하면 개발자가 작성한 코드를 지정해둔 코드 스타일에 맞게 레이아웃을 자동으로 바로잡아준다. 이를 통해 팀 내의 코딩 컨벤션을 맞추기 편하다.

vsCode에서 지원하는 format on save 옵션을 같이 사용하면, 코드를 저장할 때마다 포맷을 해주기 때문에 포매팅을 까먹는 일을 아예 봉쇄할 수 있다.

혹은 아예 github action에 코드가 push될 때마다 Prettier를 실행하도록 스크립트를 작성해둔다면 혹여 모를 가능성도 완전히 배제할 수 있다.

장점

  1. 개발자 간에 다른 코딩 컨벤션을 손쉽게 통일할 수 있다.
  2. 꼭 코딩 컨벤션 아니더라도, 대충 짜고 포맷 시키면 알아서 예쁘게 맞춰지니 코딩할 때 편하다.
  3. Javascript에서만 사용할 수 있는 것은 아니고, 다른 언어에서도 사용할 수 있다.

Webpack으로 소스코드 번들링하기

 

webpack

웹팩은 모듈 번들러입니다. 주요 목적은 브라우저에서 사용할 수 있도록 JavaScript 파일을 번들로 묶는 것이지만, 리소스나 애셋을 변환하고 번들링 또는 패키징할 수도 있습니다.

webpack.kr

개념

먼 과거, ES6으로 import, export 문법이 탄생하기 이전에 Javascript에는 모듈이라는 개념이 존재하지 않았다.

따라서 파일을 여러개로 분리하여 프로그램을 작성할 수 없었고, 파일 하나로 몇 천 줄의 코드를 작성하는 것은 너무나도 고된 정신 고문이 아닐 수 없었다.

이를 해결하기 위해 AMD(RequireJS), CommonJS 등과 같은 방법들이 나왔다. 혹은 이런 모듈 대신에 그냥 막 짜고 html에서 <script type='text/javascript' />와 같이 html에 인라인으로 싸그리 박아서 사용해서 썼을 수도.. (이렇게 하면 전역 스코프가 심각하게 오염되어, 문제가 생길 가능성이 매우 매우 매우 농후하다)

아무튼 Webpack은 이렇게 여러 개의 파일로 나뉘어진 프로젝트를 한 개의 파일로 합쳐서 번들로 만들어내는 번들러이다.

이를 통해, 여러 개의 스크립트를 로딩하느라 특정 js 하나가 로딩이 지연될 때 전체가 지연되는 문제나, 코드에는 있지만 실제로는 사용되지 않는 코드 (jQuery 라이브러리는 쓰지만 jQuery의 모든 함수를 싹싹 긁어쓰지는 않는 것처럼)를 불러오느라 필요없는 네트워크 자원을 낭비하는 일을 방지할 수 있다.

웹 프론트엔드에서 무엇보다도, 가장 중요한 UX는 로딩 속도 아니겠는가.

또한, Webpack로더 덕분에 js뿐 아니라 css, image, font와 같은 파일들도 모두 모듈로 보기 때문에 import 구문을 사용해서 모든 것을 자바스크립트 코드에서 사용할 수 있다.

추가로 Webpack에 플러그인을 따로 설치해서 번들된 결과물에 추가적인 작업을 해줄 수도 있다. 이를 통해 환경 변수를 빌드 시점에 저장해서 서버 URL 같은 변수들을 운영/테스트 간의 코드 수정 하나 없이도 간단하게 변경하며 사용할 수 있다.

위에 설명한 예시 외에도 로더, 플러그인을 통해 할 수 있는 일은 너무나도 많다. Webpack은 이제 그저 단순한 코드 번들링 툴을 넘어서서, 프론트엔드의 개발 환경을 세팅해주는 통합 툴의 역할을 차지하고 있다.

Jest, Cypress를 통해 자동화된 테스트 구축하기

 

Jest is a delightful JavaScript Testing Framework with a focus on simplicity.
Jest는 단순성에 중점을 둔 유쾌한 JavaScript 테스트 프레임워크입니다.

With Cypress, you can easily create tests for your modern web applications, debug them visually, and automatically run them in your continuous integration builds.
Cypress를 사용하면 최신 웹 애플리케이션에 대한 테스트를 쉽게 생성하고, 시각적으로 디버그하고, 지속적 통합 빌드에서 자동으로 실행할 수 있습니다.

개념

아무리 코드를 잘 작성하였다 하더라도 버그는 얼마든지 발생할 수 있고, 코드를 수정했을 때 사람이 손수 테스트해야 한다면 어지간히 귀찮은 일이 아닐 수 없으며, 자동화된 테스트 없이는 코드의 수정이 두려워서 꺼리게 될 수 밖에 없다.

테스트 코드의 작성은 내가 과거에 짠 코드에 대한 검증이며, 내가 미래에 수정할 코드에 대한 안전망이며, 내 코드가 최소한의 버그에 대한 검증이 되었음을 의미하는 보증서이기도 하며, 추후에 내 코드를 읽어야 할 상대방이 참고할 수 있는 문서이기도 하다.

Jest는 단위 테스트 (보통 함수 하나 단위의 테스트)를 도와주는 프레임워크다. 기본적으로 단위 테스트란 다음 세 가지로 이루어진다.

  1. 이러한 파라미터를 가지고 (given)
  2. 이런 함수가 실행되었을 때 (when)
  3. 결과가 이렇게 된다 (then)
test("occurrenceCount는 배열의 각 원소가 나온 횟수를 카운팅한다", () => {
  // given
  const occurrences = ["a", "b", "c", "c", "d"];

  // when
  const result = occurrenceCount(occurrences);

  // then
  expect(result).toEqual({
    a: 1,
    b: 1,
    c: 2,
    d: 1,
  });
});

하지만 프론트엔드 프로젝트는 이것만으로는 버그 검증이 불가능하다. 직접 화면을 보면서 이것저것 눌러보는 E2E 테스트를 피할 수는 없을 것이다.

describe('login', () => {
  it('user should be able to log in', () => {
    cy.visit('/')

    // open the login modal
    cy.get('button').contains('Login').click()

    // fill in the form
    cy.get('input[type="email"]').type('test@test.com')
    cy.get('input[type="password"]').type('test123')

    // submit the form 
    cy.get('button').contains('Sign in').click()
    cy.contains('button', 'Logout').should('be.visible')
  })
})

Cypress는 실제 애플리케이션을 인터넷 브라우저에서 직접 실행해보며 테스트하는 E2E 테스트 프레임워크다.

진짜로 api도 날려보고, 버튼도 눌러보면서 테스트를 진행하게 된다.

사실상 QA 엔지니어가 할 일을 대신 해주는, 약간의 하위호환 버전일 수 있지만, QA가 없는 중소 같은 곳에서 자동으로 이런 테스트를 진행해주는 툴이 있다는 점은 역시 든든하지 않을 수 없다.

Typescript로 정적 타이핑 코딩하기

 

JavaScript With Syntax For Types.

TypeScript extends JavaScript by adding types to the language. TypeScript speeds up your development experience by catching errors and providing fixes before you even run your code.

www.typescriptlang.org

개념

서론에서 얘기했듯, Typescript는 이제 필수 역량이 되어가고 있는 추세다.

동적 타입을 제공하는 Javascript는 너무나도 버그 발생의 가능성이 높다.

// Javascript
function add(num1, num2) {
    return num1 + num2;
}

console.log(add(1, 2)); // 3
console.log(add("1", "2")); // "12"  <- 버그가 발생했다.

// Typescript
function add(num1: number, num2: number) {
    return num1 + num2;
}

console.log(add(1, 2)); // 3
console.log(add("1", "2")); // 아예 컴파일 타임에 에러가 발생해서 실행조차 되지 않는다

Typescript는 타입만 제공하는 것이 아니라, enum, interface, abstract class등의 다양한 추가기능 또한 제공한다. 안 써볼 이유가 없지 않을까?

물론, 이 글의 주제는 레거시 프로젝트 개선하기이다. 이미 운영 중인 프로젝트를 한번에 바로 Typescript로 갈아끼우는 것은 불가능하다. 달리는 자동차의 바퀴를 갈아끼는 격이다.

그렇다고 손을 놓고만 있을 수는 없다. 이런 상황을 위해 Typescript에는 allowJS라는 옵션이 있다. 프로젝트 내에서 Typescript와 Javascript를 같이 사용 가능하도록 해주는 옵션이다.

점진적으로 접근하라. 신규로 작성하는 코드나, 수정 가능한 부분부터 Typescript로 작성을 시작해보고, 점차 늘려나가면 되는 것이다.

참고

그럼에도 Typescript의 사용이 불가능하다면..?

 

Use JSDoc: Index

Index Getting Started Getting Started with JSDoc 3 A quick-start to documenting JavaScript with JSDoc. Using namepaths with JSDoc 3 A guide to using namepaths with JSDoc 3. Command-line arguments to JSDoc About command-line arguments to JSDoc. Configuring

jsdoc.app

개념

그럼에도 Typescript를 사용할 수 없는 상황이 있을 것이다. 사수들의 반대같은 경우?

JSDoc은 Javascript에 주석을 달 때 사용하는 마크업 언어이다.

@param, @example, @returns등의 어노테이션을 이용해서 함수나 파일 등에 대한 설명을 작성할 수 있다.

그런데 우리가 가장 눈여겨볼 부분은 바로 @param이다.

이걸 사용하면, 함수의 파라미터에 대한 설명도 작성할 수 있지만, 그 파라미터의 타입도 작성할 수 있다.

/**
 * 두 숫자를 더한다
 * @param num1 {number} 더할 숫자 1
 * @param num2 {number} 더할 숫자 2
 * @returns {number} 더한 결과
 */
function add(num1, num2) {
    return num1 + num2;
}

다음과 같이 작성할 경우, IDE 내에서 Javascript지만 해당 파라미터의 타입을 JSDoc에 입력된 타입으로 인식하고 자동완성을 지원해준다.

또한 아예 함수 위에 마우스를 올렸을 때 해당 타입을 표시해주기 때문에, 처음 보는 함수여도 타입이 이렇게 문서화되어있다면 훨씬 코드를 이해할 때 도움이 될 수 있다.

여기에 한 술 더 떠서.. 파일의 맨 위 줄에 // @ts-check라는 주석을 달아줄 경우, 타입이 맞지 않았을 때 아예 Typescript처럼 에러를 발생시켜준다. (이건 vsCode에서만 지원되고, 인텔리제이에선 지원하지 않는다)

지속적인 코드 리팩토링

지금까지 우리의 프론트엔드 프로젝트를 개선해줄 수 있는 좋은 도구에 대해 소개했다.

하지만 사실 아무리 좋은 툴을 사용하더라도, 짜는 사람이 거지같이 짠다면 그 아무리 Typescript를 쓰던, ReactNext.jsReact Query에 어마어마한 기술 스택을 넣더라도 코드의 가독성은 떨어지고 응집성이 낮을 수 밖에 없다.

코드의 악취에는 참 여러가지가 있는데, 보통 Vanilla JS 프론트엔드에서 가장 많이 지켜지지 않는 원칙 중 하나에는 CQS가 있는 것 같다.

function getMinwonList() {
  var workerId;

  if (sessionStorage.getItem('search_id')) {
    workerId = sessionStorage.getItem('search_id');
  } else{
    workerId = userInfo.wrk_id;
  }

  $.ajax({
    url: siteURL + 'minwonList',
    data: {
      reserveDate: $("#rsv_dt").val(),
      completeDivision: page.cmptDvsn,
      assignerId: workerId,
    },
    success : function(json) {
      page.minwonList = json.data;
      page.dataDisplay();
    },
    error : function(){
      // ...
    }
  });
}

이 함수를 보자. 뭐가 문제일까? 여러가지가 보인다.

  1. 정보를 가져오는 getter 함수임에도, return이 존재하지 않는다.
    1. api 호출 결과를 대체 왜 return하는 것이 아니라, 어딘가에 저장하는 것일까? 이럴 경우 이 함수를 실행한 사람이 아무 것도 리턴하지 않는 함수의 결과를 보고 놀람을 금치 못하고 함수의 로직을 들여다봐야만 하게 되고, 이게 반복될 경우 개발자는 함수의 이름을 보고 함수의 역할을 유추하는 것을 포기하게 된다.
  2. 정보를 가져오는 getter 함수임에도, 부수효과가 존재한다. (api 쏘는 거 말고)
    1. page.minwonList라는 변수의 값을 수정하는 것일까? 이럴 경우 이 함수가 개발자가 의도치 않던 변수의 수정을 일으키므로 테스트를 할 때도 애로사항이 생기고, 이 함수를 실행하는 것이 두려워진다.
    2. 왜 정보를 가져오는 함수가 직접 화면을 조작하는 걸까? (page.dataDisplay)

이는 CQS가 제대로 지켜지지 않았기 때문에 발생한 문제다. CQS는 명령(Command)과 조회(Query)를 분리(Seperation)하자는 의미다.

다시 말해, 조회할거면 조회만 하고. 수정할거면 수정만 하라는 뜻이다. 이게 지켜지지 않을 경우, 코드를 보는 개발자는 큰 혼란에 빠지게 되고, 이것이 반복되면 개발자는 모든 것을 믿을 수 없게 된다. 테스트하기 어려워지고 프로그램의 동작 방향을 예상하기 어려워져서 유지보수가 헬이 되는 것은 둘 째치고도.

이 외에도 많이 존재할 코드악취를, 개발자들이 지속적인 역량 향상을 위해 노력하고 분투하며 고치는 수밖에 없다.

Sentry를 통한 에러 모니터링 및 예외사항 핸들링

Application Performance Monitoring & Error Tracking Software

개념

Backend 환경에서 발생하는 에러는 서버 로그에라도 찍히지, Frontend 환경에서 발생한 에러는 기껏해야 사용자 브라우저에만 빨간 줄로 찍히고 만다.

개발자들은 에러가 발생해도 직접 재현해보지 않는 이상 알 수 없으며, 브라우저의 종류에 따라 에러가 나기도 하고 안 나기도 하는 기묘한 환경을 경험해본다면 참 힘든 일이 아닐 수 없다.

Sentry는 다양한 언어에서 사용할 수 있는 모니터링 시스템이다. 이를 사용하면 Frontend 환경에서 발생하는 에러도 간단한 설정만으로 알림을 받아볼 수 있으며, 에러 리포트도 메일로 받아볼 수 있다.

Sentry에서 캡쳐한 에러의 상세 페이지

어떤 브라우저에서 에러가 발생했는지, 또 어떤 파일의 몇 번째 줄에서 에러가 발생했는지도 받아볼 수 있으니 효과적으로 에러를 핸들링할 수 있다.

import * as Sentry from "@sentry/browser";

try {
  aFunctionThatMightFail();
} catch (err) {
  Sentry.captureException(err);
}

또한 꼭 에러가 발생했을 때만 기록할 수 있는 것이 아니라, 에러를 우리가 try-catch로 핸들링했지만 예외 사항이기 때문에 기록하고 싶을 때에도 Sentry에 간단히 이슈를 등록하는 것이 가능하다.

마치며

프론트엔드 환경은 백엔드에 비해 환경이 빠르게 변화하며, 알아야 할 라이브러리도 참 많은 것이 사실인 것 같다.

하지만 이는 반대로 말해서 좋은 라이브러리를 잘 알아두면 효과적으로 프로그래밍을 할 수 있다는 뜻으로 받아들여지기도 한다.

아무리 현재 안 좋은 환경과 기술 스택에 놓여져있다 하더라도, 포기하지 않고 꾸준히 노력하며 자신의 가치를 빛내며 회사를 개선하기 위해 다양한 노력을 하며 치열한 고민을 한 사고 과정이 있다면, 결과가 어떻든 그 자체로 더 높은 곳으로 한 발짝 나아갈 수 있다고 생각한다.

profile

Octoping의 블로그

@Octoping

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!