[Node.js] JSdoc 으로 타입 명시하여 VSCode 에서 편하게 코딩하기

개요

자바스크립트는 기본적으로 동적 타입 언어라서 어떤 변수에 대해 타입이 왔다리갔다리 합니다. 그래서 실제로 자바스크립트 코드를 실행시키는 시점에서는 해당 변수가 어떤 변수일지 확신할 수가 없으므로 실행 시점에 코드가 의도대로 진행할지에 대한 확신도 내리기 힘듭니다. 이는 프로젝트의 규모가 커질수록 큰 문제가 될 수 있어서 그런 점을 컴파일 타임에 보완해볼 수 있는 Typescript 가 등장하기도 했습니다.

하지만 실제 코드 행동이 어떻냐는 건 이 글에서 다룰 주제는 아닙니다. 이 글에서는 자동 완성 기능에 집중합니다. 정적 타입 언어 계열인 C, C++ 등은 에디터 입장에서 어떤 변수가 어떤 행동을 할 수 있고 어떤 멤버 변수가 있는지를 유추해내기 쉽습니다. 왜냐하면 타입이 정해져있고, 그 타입에 대한 정보만 읽어오면 되기 때문이죠. 그래서 멤버 변수에 접근하고자 할 때, 해당 타입에 관한 문서를 직접 찾는 것이 아니라 에디터가 그냥 가능한(available) 목록을 출력해주니 개발 생산성이 올라갑니다. 이런 걸 자바스크립트에서도 하고 싶다 이 말입니다! 그래서 필자는 JSDoc 을 이용하기로 했습니다.

JSDoc 3 is an API documentation generator for JavaScript, similar to Javadoc or phpDocumentor. You add documentation comments directly to your source code, right alongside the code itself. The JSDoc tool will scan your source code and generate an HTML documentation website for you.

(번역) JSDoc 3는 자바스크립트 API 문서 생성기이고 Javadoc, phpDocumentor 와 비슷합니다. 소스 코드, 코드 바로 앞에 주석으로 직접 문서를 추가할 수 있습니다. JSDoc 도구로 소스 코드를 스캔하고 HTML 문서 웹사이트를 생성할 수 있습니다.

JSDOC 소개, https://jsdoc.app/about-getting-started.html

물론 JSDoc 은 코드를 문서화하기 위한 도구이고, 그에 따른 이점은 아주 다양하게 있겠지만, 필자는 오로지 자동 완성 기능을 위해서 JSDoc 을 사용하고자 마음먹었습니다. 물론 이것도 에디터가 JSDoc 을 읽을 능력이 없다면 무용지물이겠지만, 아주 칭찬이 자자한 Microsoft Visual Studio Code 를 사용할 거니깐 괜찮습니다. JSDoc 말고 다른 툴은 없느냐? 어쩔 수 없이 Javascript 를 계속 직접 다루어야 한다면 JSDoc 이 차선책이 될 수 있겠지만, 이제 새롭게 프로젝트를 시작한다면 시간을 좀 더 들여서 Typescript 공부하는 걸 강력하게 추천드립니다.

기본적인 사용법

굉장히 간략하게 설명하도록 하겠습니다. 어차피 공식 문서를 보면 다 나옵니다. 하하. 일단 기본적으로 /** 로 시작합니다. 주석은 대상 바로 앞에 등장합니다. (용도에 따라 위치가 상관없을 수 있습니다.) 내용은 기본적으로 설명을 의미합니다.

/**
 * 내용
 */
function foo() {
}

이렇게요. 이제 어떤 맥락을 만들기 위해 JSDoc 에서 명시한 tag를 이용할 수 있습니다. tag는 @로 시작하며 한 줄에 하나씩 작성됩니다. tag는 종류가 다양하고, 저마다의 문법 규칙이 있습니다.

/**
 * 설명
 * @constructor
 * @param {string} title - The title of the book.
 * @param {string} author - The author of the book.
 */
function Book(title, author) {
}

콜백 함수 또는 객체 정의하기

아래 내용은 새로운 타입을 JSDoc 으로 정의하므로 실제 코드와 붙을 필요가 없는 것들입니다. 복잡한 객체는 아래와 같이 정의내린 다음에 사용할 수 있습니다. 일반 객체 타입은 @typedef 를 사용하고, 함수 타입은 @callback 을 사용합니다.

// PostSearch 라는 객체 타입을 정의합니다.

/**
 * 게시물 검색 정보를 담는 객체
 * @typedef {object} PostSearch
 * @property {number} page 0이 1페이지임.
 * @property {number} perpage
 * @property {Date} date_gte
 * @property {Date} date_lte
 * @property {string} search
 * @property {string[]} board_permalinks
 * @property {string} board_belongs_to
 */



// Resolver 라는 함수 타입을 정의합니다.

/**
 * resolver의 기본 형태
 * @callback Resolver
 * @param {object} obj
 * @param {object} args
 * @param {PassportContext} context
 * @param {object} info
 * @returns {Promise<any>}
 */

다른 라이브러리에서 타입을 가져오기

이는 VSCode 에서만 작동되는 방법이라고 감히 추정합니다. 일단 상황을 가정합시다. mongoose 객체를 인수로 받는 함수를 만들어서, 적절하게 무언가를 만들어내는 팩토리 함수를 만든다고 가정합시다.

/**
 * 모델을 만들어 반환합니다.

 */
function makeModel(mongoose) {
  // ...
}

왜 굳이 mongoose 를 인수로 받아오냐구요? 그냥 const mongoose = require('mongoose') 로 받아와서 바로 사용해도 되지 않느냐구요? 뭐 그것도 맞는 말이지만 다른 mongoose 여러 개가 쓰인다고 그냥 가정합시다. ㅎㅎ.. 아무튼 mongoose 는 우리가 만든 객체도 아니고 이미 잘 짜여진 객체이기 때문에 직접 @typedef로 만들고 @param 으로 mongoose 에 직접 갖다 붙이고 하는 작업은 너무너무 낭비입니다. 이럴 때 우리는 해당 타입을 mongoose 라이브러리부터 가져올 수 있습니다. 아래와 같이 하면 됩니다.

/**
 * 모델을 만들어 반환합니다.
 * @param {import('mongoose')} mongoose 
 */
function makeModel(mongoose) {
  return mongoose.model(/* ... */); // 우리가 무엇을 쓸 수 있는지 VSCode 가 읽을 수 있습니다!
}

이제 mongoose.model 위에 마우스를 올려보면 화려한 설명을 확인할 수 있습니다!

mongoose.model 함수에 마우스를 올렸을 때 나오는 화면

위 함수에 맞춰 자동 완성 기능을 즐기시면 됩니다. 위 타입 설명은 Typescript 스럽게 설명하고 있으므로 관련된 내용, 특히 제네릭 부분을 알고 있다면 좀 더 타입을 해석하는 데 도움이 될 겁니다. 왜 Typescript 식으로 설명이 되느냐? 왜냐하면 Javascript 에는 애초에 타입과 관한 문법은 없고, 타입 설명은 그 방법을 선택하기 나름이고, Typescript 식으로 설명하는 건 나쁘지 않은 방법이고, 게다가 Typescript 도 마이크로소프트에서 개발한 것이기 때문입니다.

정의한 타입을 여러 파일에서 사용하기

타입 정의를 하나의 파일에서 관리합시다! typedef.js 파일을 만들어줍니다. 앞서 나왔던 예제와 거의 동일합니다.

// typedef.js
// PostSearch 라는 객체 타입을 정의합니다.

/**
 * 게시물 검색 정보를 담는 객체
 * @typedef {object} PostSearch
 * @property {number} page 0이 1페이지임.
 * @property {number} perpage
 * @property {Date} date_gte
 * @property {Date} date_lte
 * @property {string} search
 * @property {string[]} board_permalinks
 * @property {string} board_belongs_to
 */



// Resolver 라는 함수 타입을 정의합니다.

/**
 * resolver의 기본 형태
 * @callback Resolver
 * @param {object} obj
 * @param {object} args
 * @param {PassportContext} context
 * @param {object} info
 * @returns {Promise<any>}
 */

이 파일을 이제 이용해봅시다. 그냥 단순히 해당 타입을 이용하고자 하는 파일에서 아래와 같이 require 하면 됩니다.

// test.js
require('./typedef.js');
/**
 * 게시글을 가져옵니다.
 * @param {PostSearch} condition 조건
 */
function getPosts(condition) {
  console.log(condition.search);
}

condition. 를 입력하는 순간 아래와 같이 자동 완성 목록이 뜨는 것을 확인하실 수 있습니다.

condition. 를 입력하는 순간 나오는 목록

직접 정의한 클래스를 여러 파일에서 사용하기

직접 정의한 클래스를 비슷한 방식으로 진행할 수 있습니다.

// /typedef.js

/** @typedef {import("./manager/db").DBManager} DBManager */
// /manager/db.js

class DBManager {
  // 각종 메서드, 속성 등 정의
}
// /test.js

require('./typedef.js');
/**
 * 설명
 * @param {DBManager} db DB매니저
 */
function SomeJobWithDB(db) {
  // 내용
}

직접 test.js에서 DBManagerrequire하지 않고 굳이 한 단계 더 거치는 이유는, 타입 관련한 작업은 모두 typedef.js 에게 위임할 수 있기 때문입니다. 이렇게 되면 또 다른 파일에서 DBManager를 이용하고자 할 때 경로를 일일히 찾을 필요 없이 typedef.js 파일만 require하면 됩니다.

결론

Typescript 를 연습하고 사용하세요. JSDoc 은 깔끔하지 않은 기분입니다. 그럼 다들 파이팅입니다!

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다

Scroll to top