최근 들어 더욱 가속화되어 발전하는 자바스크립트 트렌드는 이제 지켜보는 것조차 버겁네요. 최근 신규 프로젝트의 개발환경을 구축하면서 최종적으로 반영된 구성에 관해서 얘기해 보겠습니다. 브라우저에서 Node.js의 모듈 인프라를 이용할 수 있게 하고 실시간으로 트랜스파일(Transpile)이 되도록 개발환경을 구성해 보았습니다. 이 개발환경에 적응하면서 느끼는 것은 "이제 브라우저에서도 자바스크립트로 개발하기가 정말 좋은 세상이 되었구나!" 입니다. 그래서 다른 프로젝트에도 이를 적용할 수 있도록 사내에 전파를 시도하면서 정리했던 내용 일부를 공유합니다.

우리는 Node.js를 기반으로 한 개발환경에 BrowserifyWebpack과 같은 모듈 번들러를 이용하여 자바스크립트로 작성된 기능의 모듈화를 효율적이고 안정적으로 구현한 바 있습니다. 그리고 개발환경에서는 와쳐(Watcher)를 이용하여 파일이 변동되면 번들링한 후 브라우저에서 새로 고침까지 자동으로 되도록 했습니다.

그러나 답답합니다. 코드량이 많아질수록 눈에 띄게 느려지는 번들링 시간과 마이너 픽스에도 꼭 거쳐야만 하는 이 과정은 이제 지긋지긋합니다. 그냥 예전처럼 브라우저에서 새로 고치면 즉시 그 결과를 확인하고 싶습니다. 그래서 이처럼 불편하고 느려터진 번들링 과정을 개발하는 동안에는 하지 않아도 되도록 하는 것이 컨셉이라 하겠습니다. 즉, 실시간 트랜스파일이 되도록 한다는 것은, import 또는 require 문을 브라우저에서 직접 사용할 수 있도록 하는 것입니다.

Requirements

개발환경을 구성하는 주요한 설치 요구사항입니다. 테스트 라이브러리나 태스크 매니저는 취향에 맞게 사용하면 됩니다.

  • Node.js - 실시간으로 트랜스파일이 가능한 개발환경의 기반이 됩니다.
  • Electron - 구글 크롬 브라우저에서 import(require)문을 직접 이용하는 데 필요합니다.
  • Babel - ES6, ES7, JSX등 차세대 자바스크립트 코드를 구사하기 위해 사용합니다.
  • ESLint - 코드 스타일을 안내해주고, 빈번히 발생하는 개발 실수를 줄여줍니다.
  • Webpack - 프로덕션 빌드 과정에서 모듈 패키징에 사용됩니다.
  • Jest - 자바스크립트 유닛 테스트의 고통을 덜 수 있습니다.
  • Nightwatch.js - 구글 크롬 외 다른 브라우저에서의 작동 여부를 테스트하고 자동화를 위해 사용합니다.
  • Grunt - 이 모든 과정을 수월하게 관리할 수 있도록 도와주는 태스크 매니저입니다.

노트: Electron을 개발환경에 적합하다고 판단한 이유가 하나 더 있습니다. Node.js에 내장된 V8 버전(가장 최근에 나온 Node.js 5.1.1의 V8 버전은 4.6) 보다 Electron에 내장된 V8 버전이 4.8로 한참 앞서있기 때문입니다. 또한, V8의 블로그를 보면 ECMAScript 2015(ES6) 스펙을 구현하는 작업이 한창인 것을 알 수 있는데, 안정적인 버전의 크롬이 나오면 1, 2주 내로 Electron에 반영되는 장점은 덤입니다.

Configuration

이제 설정 파일을 작성해 봅시다. React와 jQuery를 사용하는 웹앱을 만든다고 가정합니다. 임의의 프로젝트 폴더에 package.json을 비롯한 프로젝트를 구성하는 파일과 폴더를 생성합니다.

$ mkdir my-project
$ cd my-project
$ touch .babelrc
$ touch .eslintrc
$ touch .gitignore
$ touch package.json
$ touch main.js
$ touch index.html
$ touch LICENSE.md
$ touch README.md
$ mkdir scripts
$ mkdir src
$ cd src
$ touch index.js

.babelrc Babel 트랜스파일러에서 사용할 플러그인을 지정합니다. 일반적으로 프리셋(babel-preset-*)을 사용하지만, 프리셋은 아주 많은 Babel 플러그인들을 포함하고 있어서 실시간으로 트랜스파일 하는 데에는 적합하지 않습니다. 오래 걸리기 때문입니다. 아래의 구성은 Electron 현재 버전 0.36.x(Node.js 5.1.1, 구글 크롬 47)에 내장된 V8 버전 4.7에서 ES2015 스펙이 미구현 되거나 일부만 구현되어 필요하게 된 플러그인을 개별적으로 로드하는 내용입니다. 이렇게 했을 때 프리셋을 사용할 때보다 매 새로 고침 마다 6초에서 4초 정도 시간을 절약할 수 있습니다. 보통 2초 정도면 페이지 로드가 완료됩니다.

노트: Node.js의 require를 훅(Hook)하는 babel-register는 캐시(Cache) 옵션이 기본으로 활성화되어 있습니다. 첫 로딩보다 그 다음 로딩이 훨씬 빠릅니다.

{
  "plugins": [
    "transform-es2015-destructuring",
    "transform-es2015-for-of",
    "transform-es2015-modules-commonjs",
    "transform-es2015-object-super",
    "transform-es2015-parameters",
    "transform-es2015-shorthand-properties",
    "transform-object-rest-spread",
    "transform-react-jsx"
  ]
}

노트: sticky-regexunicode-regex 그리고 typeof-symbol 플러그인은 테스트를 통과하지 못해 로드하지 않았습니다. 그리고 크롬의 최신 버전인 48(아마도 Electron 0.37.x)에서는 object-supertypeof-symbol 플러그인이 필요하지 않게 됩니다. 지난 26일 릴리즈된 V8 버전 4.9(크롬 49)에서는 destructuring, parameters, sticky-regex등이 구현되었습니다.

Electron 버전 0.35.x(Node 4.1.1, 크롬 45, v8 버전 4.5)에서는 아래의 3개 플러그인을 추가로 로드하여 ES6을 정상적으로 사용할 수 있습니다. Babel의 Github 리파지토리에 있는 380여 개 테스트 케이스를 돌려서 확인했으며 속도 역시 그럭저럭 나옵니다.

    ...
    // Require if using electron version 0.35.x
    "transform-es2015-block-scoping",
    "transform-es2015-classes",
    "transform-es2015-spread"
    ...

object-rest-spread는 ES2015의 공식 스펙은 아니지만 ES7의 꽃이라 할만한 멋진 연산자입니다. 이미 널리 사용되고 있으며, ESLint의 object-shorthand 룰에 영향을 받습니다.

.eslintrc 파일은 ESLint의 설정입니다. ESLint는 코드를 작성하는 과정에서 빈번하게 발생하는 실수를 예방하고, 엘레강스한 코드 스타일을 추천해 주며, 미래에 발생할 수 있는 잠재적 오류를 수정할 수 있도록 도와줍니다. 제가 사용하는 코드 편집기는 CODA 2인데, 여기에 ESLint JS Validator 플러그인을 추가하면 아래 설정에 기반을 두어 코드 검증기를 통해 꾸역꾸역 잔소리(?)해 대도록 꾸몄습니다. 만약 아톰(Atom) 편집기를 사용한다면, linter-eslint 패키지를 설치하여 사용할 수 있고 Sublime Text에도 비슷한 녀석이 있습니다.

eslint.gif

요런 느낌입니다. 가장 우선순위에 있는 룰은 얼마 전에 번역한 바 있는 Airbnb 코드 스타일이고, 그다음으로 JavaScript Standard Style의 프리셋과 React 플러그인을 적용하고, 개인적으로 탐탁지 못한 몇몇 규칙을 "rules"에 재정의한 것입니다.

{
  "ecmaFeatures": {
    "jsx": true,
    "modules": true,
    "experimentalObjectRestSpread": true
  },
  "env": { "es6": true, "node": true, "browser": true },
  "extends": ["standard", "airbnb"],
  "globals": { "$": true },
  "parser": "babel-eslint",
  "plugins": ["standard", "react"],
  "rules": {
    "comma-dangle": [2, "never"],
    "default-case": 0,
    "func-names": 0,
    "new-cap": [2, { "newIsCap": true, "capIsNew": false }],
    "no-console": 0,
    "object-curly-spacing": 0,
    "react/prop-types": 0,
    "react/sort-comp": 0,
    "space-before-function-paren": [2, "never"],
    "strict": 0
  }
}

노트: 경우에 따라서는 ESLint관련 패키지를 글로벌에 설치해야 할 수도 있습니다.

$ npm install -g eslint
$ npm install -g eslint-config-airbnb eslint-config-standard
$ npm install -g eslint-plugin-react eslint-plugin-standard

package.json 파일의 내용은 다음과 같습니다. 프로젝트에서 필요한 모듈들의 정보를 포함한 여러 내용으로 구성됩니다. 더 자세한 내용은 이곳을 참고하세요.

{
  "name": "MyProject",
  "version": "0.0.1",
  "license": "MIT",
  "description": "My Awesome Project",
  "author": "firejune",
  "main": "main.js",
  "dependencies": {
    "jquery": "^2.2.0",
    "jquery-ui": "^1.10.5",
    "react": "^0.14.3",
    "react-dom": "^0.14.3"
  },
  "devDependencies": {
    "babel-core": "^6.4.5",
    "babel-eslint": "^5.0.0-beta6",
    "babel-loader": "^6.2.1",
    "babel-plugin-transform-es2015-destructuring": "^6.4.0",
    "babel-plugin-transform-es2015-for-of": "^6.3.13",
    "babel-plugin-transform-es2015-modules-commonjs": "^6.4.5",
    "babel-plugin-transform-es2015-object-super": "^6.4.0",
    "babel-plugin-transform-es2015-parameters": "^6.4.5",
    "babel-plugin-transform-es2015-shorthand-properties": "^6.3.13",
    "babel-plugin-transform-object-rest-spread": "^6.3.13",
    "babel-plugin-transform-react-jsx": "^6.4.0",
    "electron-prebuilt": "^0.36.5",
    "eslint": "^1.10.3",
    "eslint-config-airbnb": "^3.1.0",
    "eslint-config-standard": "^4.4.0",
    "eslint-plugin-react": "^3.15.0",
    "eslint-plugin-standard": "^1.3.1",
    "grunt": "^0.4.5",
    "jest-cli": "^0.8.2",
    "nightwatch": "^0.8.15",
    "webpack": "^1.12.11"
  },
  "scripts": {
    "start": "electron .",
    "lint": "eslint ./src",
    "test:unit": "npm run lint && jest -c ./scripts/unit-test.json",
    "test:ui": "npm run test:unit  && nightwatch --test ./scripts/ui-test.js",
    "build": "npm run test:ui  && webpack --config ./scripts/package.js --release"
  }
}

굳이 ES6 문법까지는 필요는 없고, JSX 트랜스파일만 필요한 상황이라면 "transform-react-jsx" 플러그인만 남기거나 Babel이 아닌 node-jsx 모듈을 이용하는 방법도 있습니다. node-jsx가 사용법도 간단하고 빠르긴 한데, Babel로 이관되면서 deprecated 되었습니다.

Electron Starter

이제 Electron에서의 작업환경을 구성할 차례입니다. main.js 파일은 package.json에 명시되어 Electron이 처음으로 접근하는 파일이며, Electron에 의해 브라우저 윈도를 만들어줍니다. 아쉽지만, 이 파일은 ES6으로 작성할 수 없습니다.

'use strict';

const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;

// 실행 준비를 마치면 브라우저 창 생성
app.on('ready', function () {
  // 브라우저 생성
  const mainWindow = new BrowserWindow({width: 800, height: 600});
  // 브라우저에서 처음으로 그려질 페이지
  mainWindow.loadURL('file://' + __dirname + '/index.html');
  // 브라우저의 개발자 도구 자동으로 열기
  mainWindow.webContents.openDevTools();
  // 창이 닫히면 프로세스 종료
  mainWindow.on('closed', function() {
    app.quit();
  });
});

index.html 파일은 main.js에 의해 브라우저(BrowserWindow)에서 처음으로 그려질 페이지입니다. 이 브라우저가 바로 앞으로 동고동락할 작업용 브라우저입니다. 일단 다음과 같이 내용을 작성합니다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
  </head>
  <body>
    <div id="example">Hello</div>
    <script>
      // Install babel hooks to the browser process
      require('babel-core/register')();
      require('./src');
    </script>
  </body>
</html>

노트: Electron은 main과 browser로 구분된 프로세스 두 개를 실행합니다. 따라서 두 프로세스 간에는 IPC 스타일의 통신을 이용해야 하지만, Electron 앱이 아닌, 일반적인 웹앱 개발에 Electron을 이용하는 것이므로 main 프로세스에서 하는 일에 대해서는 크게 걱정하지 않아도 됩니다. main.js는 main 프로세서에서 작동하고 index.html 및 하위 참조 스크립트들은 browser 프로세스에서 작동합니다.

자, 이제 기본적인 프로젝트 파일의 구성과 작업 실행 환경이 완료되었습니다. scripts 폴더는 정적 또는 동적 테스트 코드, 자동화 관련 코드, 작업 태스크 관리, 빌드 스크립트 등을 넣어둘 장소입니다. 이 글은 개발 환경을 구성하는 데 목적을 두기 때문에 이와 관련한 자세한 내용은 다루지 않을 것입니다. (엄청나게 다양하고 복잡하고 일일이 설명하기가 귀찮기도 하고 뭐 그렇습니다) LICENSE.md, README.md 파일에는 프로젝트와 관련된 내용을 작성하면 됩니다. 이제, 커멘드 라인에 개발에 필요한 모듈들을 설치하고 작업 결과를 확인할 수 있는 브라우저(Electron)를 실행해 봅시다. 'Hello'문자가 보이나요?

$ npm install
$ npm start

Enjoying Web Development with Electron

모든 준비는 끝났습니다. 멋들어지게 최신 자바스크립트 문법을 이용하여 본격적으로 개발을 시작해 봅시다. src/index.js 파일을 열고 간단한 React 기반의 'Hello, world!' 애플리케이션을 만들겠습니다.

import React from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
import 'jquery-ui/draggable';

class Title extends React.Component {
  constructor() {
    super();
    this.state = {
      value: 'Hello, world!'
    };
  }
  componentDidMount() {
    $(this.refs.el).draggable();
  }
  render() {
    return (
      <h1 ref="el">
        {this.state.value}
      </h1>
    );
  }
}

ReactDOM.render(
  <Title />,
  document.getElementById('example')
);

파일 작성 후 브라우저 창에서 새로 고칩니다. 단축키는 맥인 경우 CMD+R(윈도: Ctrl+R)입니다. 브라우저 개발자 도구는 CMD+ALT+I(윈도: Ctrl+Shift+I)로 열 수 있습니다. 'Hello, world!' 문자가 보이나요? 정상적으로 작동하는 것입니다. 놀랍죠? 문자의 드래그 앤 드롭 기능도 확인해 보세요!

크로스-브라우저는 어찌할까요? Browserify나 Webpack을 이용해서 번들링 한 후 실제 브라우저에서 동작하는 동적-테스트 도구(Nightwatch 와 같은)로 테스트 수행을 자동화하고 오류가 보고되면 그때 처리합니다.

Comments

기왕 번역한 김에 하나 더 했습니다. Airbnb에서 Reac와 JSX 스타일 가이드도 작성했더군요. ES6과 JSX기반의 React 코드를 작성하는 것을 마치 기본적인 것인 양 소개하고 있습니다. 요즘 Electron 기반 하이브리드 데스크탑 애플리케이션을 개발하고 있는데 전반에 걸쳐 React를 적용하는 중입니다. Electron은 구글 크롬 브라우저와 Node.js를 포함하고 있어서 하나의 브라우저에서 작동하는 것에만 집중할 수 있고 browserify또는 webpack와 같은 모듈 번들러(module bundler)의 도움 없이 네이티브 require를 브라우저에서도 사용할 수 있다 보니, 의존성(dependency)이나 네임스페이스(namespace) 관리는 물론이고, 실시간 트랜스파일(Transpile)이 가능해서 ES6 코드를 마구 내질러도 크게 문제 될 것이 없는 개발환경을 만들 수 있다는 것은... 이건 뭐 그냥 완전히 다른 세상이라고 밖에 표현하지 못하겠군요.

1. 기본 규칙(Basic Rules)

2. 클래스(Class) vs React.createClass

특별한 이유로 믹스인(mixin)하는 경우를 제외하고는 class extends React.Component를 사용하세요.

eslint rules: react/prefer-es6-class.

// bad
const Listing = React.createClass({
  render() {
    return <div />;
  }
});

// good
class Listing extends React.Component {
  render() {
    return <div />;
  }
}

3. 명명(Naming)

확장자: React 컴포넌트는 .jsx 확장자를 사용합니다.

파일명: 파일명에는 PascalCase(대문자로 시작)를 사용합니다. 예), ReservationCard.jsx.

참조명: React 컴포넌트의 참조 이름에는 PascalCase를 쓰고 그 인스턴스의 이름에는 camelCase(소문자로 시작)를 사용합니다.

eslint rules: react/jsx-pascal-case.

// bad
import reservationCard from './ReservationCard';

// good
import ReservationCard from './ReservationCard';

// bad
const ReservationItem = <ReservationCard />;

// good
const reservationItem = <ReservationCard />;

컴포넌트명: 컴포넌트명으로 파일명을 씁니다. 예), ReservationCard.jsx 파일은 ReservationCard라는 참조명을 가집니다. 그러나, 루트 컴포넌트가 디렉토리에 구성되었다면 파일명을 index.jsx로 쓰고 디렉토리명을 컴포넌트명으로 사용합니다:

// bad
import Footer from './Footer/Footer';

// bad
import Footer from './Footer/index';

// good
import Footer from './Footer';

4. 선언(Declaration)

displayName을 이용하여 컴포넌트명을 정하지 않습니다. 그대신, 참조에 의해 이름을 지정합니다.

// bad
export default React.createClass({
  displayName: 'ReservationCard',
  // stuff goes here
});

// good
export default class ReservationCard extends React.Component {
}

5. 조정(Alignment)

JSX 구문에 따른 정렬 스타일을 사용합니다.

eslint rules: react/jsx-closing-bracket-location.

// bad
<Foo superLongParam="bar"
     anotherSuperLongParam="baz" />

// good
<Foo
  superLongParam="bar"
  anotherSuperLongParam="baz"
/>

// if props fit in one line then keep it on the same line
<Foo bar="bar" />

// children get indented normally
<Foo
  superLongParam="bar"
  anotherSuperLongParam="baz"
>
  <Spazz />

6. 인용(Quotes)

JSX 속성(attributes)에는 항상 큰 따옴표(")를 사용합니다. 그러나 다른 모든 자바스크립트에는 작은 따옴표(single quotes)를 사용합니다.

왜죠? JSX 속성(attributes)은 따옴표(quotes)의 탈출(escaped)을 포함할 수 없습니다. 그래서 큰 따옴표를 이용하여 "don't"와 같은 접속사를 쉽게 입력할 수 있습니다. 일반적으로 HTML 속성(attributes)에는 작은 따옴표 대신 큰 따옴표를 사용합니다. 그래서 JSX 속성역시 동일한 규칙이 적용됩니다.

eslint rules: jsx-quotes.

// bad
<Foo bar='bar' />

// good
<Foo bar="bar" />

// bad
<Foo style={{ left: "20px" }} />

// good
<Foo style={{ left: '20px' }} />

7. 공백(Spacing)

자신을 닫는(self-closing) 태그에는 항상 하나의 공백만을 사용합니다.

// bad
<Foo/>

// very bad
<Foo                 />

// bad
<Foo
 />

// good
<Foo />

8. 속성(Props)

prop 이름은 항상 camelCase(소문자로 시작)를 사용합니다.

// bad
<Foo
  UserName="hello"
  phone_number={12345678}
/>

// good
<Foo
  userName="hello"
  phoneNumber={12345678}
/>

명시적으로 true 값을 가지는 prop은 그 값을 생략할 수 있습니다.

eslint rules: react/jsx-boolean-value.

// bad
<Foo
  hidden={true}
/>

// good
<Foo
  hidden
/>

9. 괄호(Parentheses)

JSX 태그가 감싸여(Wrap) 있어 한 줄 이상인 경우 괄호(parentheses)를 사용합니다.

eslint rules: react/wrap-multilines.

 // bad
  render() {
    return <MyComponent className="long body" foo="bar">
             <MyChild />
           </MyComponent>;
  }
  
  // good
  render() {
    return (
      <MyComponent className="long body" foo="bar">
        <MyChild />
      </MyComponent>
    );
  }
  
  // good, when single line
  render() {
    const body = <div>hello</div>;
    return <MyComponent>{body}</MyComponent>;
  }

10. 태그(Tags)

자식(children)을 가지지 않는다면 항상 자신을 닫는(self-close) 태그로 작성합니다.

eslint rules: react/self-closing-comp.

// bad
<Foo className="stuff"></Foo>

// good
<Foo className="stuff" />

만약, 컴포넌트의 속성(properties)을 여러 줄에 있는 경우, 닫는 태그는 다음 줄에 작성합니다.

eslint rules: react/jsx-closing-bracket-location.

// bad
<Foo
  bar="bar"
  baz="baz" />

// good
<Foo
  bar="bar"
  baz="baz"
/>

11. 메소드(Methods)

렌더(Render) 메소드에서 이벤트 핸들러에 바인드(Bind)가 필요한 경우에는 생성자(constructor)에서 합니다.

왜죠? 렌더러 메소드에서 바인드(bind)를 호출하게 되면 랜더링 할 때 마다 매번 새로운 함수를 생성하게 됩니다.

eslint rules: react/jsx-no-bind.

// bad
class extends React.Component {
  onClickDiv() {
    // do stuff
  }

  render() {
    return <div onClick={this.onClickDiv.bind(this)} />
  }
}

// good
class extends React.Component {
  constructor(props) {
    super(props);

    this.onClickDiv = this.onClickDiv.bind(this);
  }

  onClickDiv() {
    // do stuff
  }

  render() {
    return <div onClick={this.onClickDiv} />
  }
}

React 컴포넌트의 내부 메소드에 밑줄(underscore)을 접두사로 사용하지 않습니다.

// bad
React.createClass({
  _onClickSubmit() {
    // do stuff
  },

  // other stuff
});

// good
class extends React.Component {
  onClickSubmit() {
    // do stuff
  }

  // other stuff
}

12. 호출순서(Ordering)

class extends React.Component의 호출순서(Ordering):

  • constructor
  • 추가적인(optional) static 메소드
  • getChildContext
  • componentWillMount
  • componentDidMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • componentDidUpdate
  • componentWillUnmount
  • onClickSubmit()와 같은 clickHandlers 또는 eventHandlers 또는 onChangeDescription()
  • getSelectReason()와 같은 render를 위한 getter methods 또는 getFooterContent()
  • renderNavigation()와 같은 추가적인 렌더러 메소드 또는 renderProfilePicture()
  • render

React.createClass의 호출순서(Ordering):

  • displayName
  • propTypes
  • contextTypes
  • childContextTypes
  • mixins
  • statics
  • defaultProps
  • getDefaultProps
  • getInitialState
  • getChildContext
  • componentWillMount
  • componentDidMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • componentDidUpdate
  • componentWillUnmount
  • onClickSubmit()와 같은 clickHandlers 또는 eventHandlers 또는 onChangeDescription()
  • getSelectReason()와 같은 render를 위한 getter methods 또는 getFooterContent()
  • renderNavigation()와 같은 추가적인 렌더러 메소드 또는 renderProfilePicture()
  • render

eslint rules: react/sort-comp.

propTypes, defaultProps, contextTypes, 등을 어떻게 정의할까요...

import React, { PropTypes } from 'react';

const propTypes = {
  id: PropTypes.number.isRequired,
  url: PropTypes.string.isRequired,
  text: PropTypes.string,
};

const defaultProps = {
  text: 'Hello World',
};

class Link extends React.Component {
  static methodsAreOk() {
    return true;
  }

  render() {
    return <a href={this.props.url} data-id={this.props.id}>{this.props.text}</a>
  }
}

Link.propTypes = propTypes;
Link.defaultProps = defaultProps;

export default Link;

13. isMounted

isMounted는 사용하지 않습니다.

왜죠? isMounted안티-패턴(anti-pattern)입니다. ES6 클래스에서는 사용할수도 없습니다. 그리고 공식적으로 사용되지 않게(deprecated) 될 것입니다.

eslint rules: react/no-is-mounted.

Comments

ECMAScript 2015(ES6)가 우후죽순처럼 활개를 치고 있습니다. Transpile(Source-to-source compiler)이 필요한 소스들은 몇 번이고 무시하려고 했지만 ES6만큼은 그러지 못했습니다. 요즘 왠만한 Node.js 프로젝트나 React에 기반을 둬서 나오는 결과물들은 이미 장악되었다고 해도 과언이 아닐정도로 많은 사람이 애용하고 있어, 원만히 소스를 읽는 데에 큰 장애가 생겨났습니다. 이런, 까막눈이 되고 말았군요. 그러던 중 때마침 Airbnb에서 ES5이었던 자바스크립트 스타일 가이드를 ES6으로 업데이트했기에 학습차 번역해 보았습니다.

1. 유형(Types)

Primitives: 원시형(Primitive type)은 그 값을 직접 조작합니다.

  • string
  • number
  • boolean
  • null
  • undefined
const foo = 1;
let bar = foo;

bar = 9;

console.log(foo, bar); // => 1, 9

Complex: 참조형(Complex type)은 참조를 통해 값을 조작합니다.

  • object
  • array
  • function
const foo = [1, 2];
const bar = foo;

bar[0] = 9;

console.log(foo[0], bar[0]); // => 9, 9

2. 참조(References)

모든 참조에는 const를 사용하고 var를 사용하지 않습니다.

왜죠? 참조를 다시 할당할 수 없어서, 버그로 연결되거나 이해하기 어려운 코드가 되는 것을 예방합니다.

eslint rules: prefer-const, no-const-assign.

// bad
var a = 1;
var b = 2;

// good
const a = 1;
const b = 2;

참조를 다시 할당해야 하는 경우 var 대신에 let을 사용하세요.

왜죠? var는 함수-범위(function-scoped)이고 let은 블록-범위(block-scoped)이기 때문입니다.

eslint rules: no-var.

// bad
var count = 1;
if (true) {
  count += 1;
}

// good, use the let.
let count = 1;
if (true) {
  count += 1;
}

letconst는 모두 블록-범위(block-scoped)인 것에 주의해야 합니다.

// const와 let은 선언 된 블록 안에서만 존재함. 
{
  let a = 1;
  const b = 1;
}
console.log(a); // ReferenceError
console.log(b); // ReferenceError

3. 객체(Objects)

객체를 만들 때에는 리터럴 구문을 사용합니다.

eslint rules: no-new-object.

// bad
const item = new Object();

// good
const item = {};

코드가 브라우저에서 실행되는 경우 예약어를 키로 사용하지 마세요. 이것은 IE8에서 작동하지 않습니다. 더 알아보기. ES6 모듈과 서버 사이드에서는 사용할 수 있습니다.

// bad
const superman = {
  default: { clark: 'kent' },
  private: true,
};

// good
const superman = {
  defaults: { clark: 'kent' },
  hidden: true,
};

예약어 대신에 알기 쉬운 동의어(Readable Synonyms)를 사용하세요.

// bad
const superman = {
  class: 'alien',
};

// bad
const superman = {
  klass: 'alien',
};

// good
const superman = {
  type: 'alien',
};

동적인 속성 이름을 가진 객체를 만들 때에는 계산된 속성 이름(Computed Property Names)을 사용하세요.

왜죠? 이렇게하면 객체 속성을 한 개의 장소에서 정의 할 수 있습니다.

function getKey(k) {
  return `a key named ${k}`;
}

// bad
const obj = {
  id: 5,
  name: 'San Francisco',
};
obj[getKey('enabled')] = true;

// good
const obj = {
  id: 5,
  name: 'San Francisco',
  [getKey('enabled')]: true,
};

메소드에 단축 구문(Object Shorthand)을 사용하세요.

eslint rules: object-shorthand.

// bad
const atom = {
  value: 1,

  addValue: function (value) {
    return atom.value + value;
  },
};

// good
const atom = {
  value: 1,

  addValue(value) {
    return atom.value + value;
  },
};

속성에 단축 구문(Object Concise)을 사용하세요.

왜죠? 표현이나 설명이 간결해지기 때문입니다.

eslint rules: object-shorthand.

const lukeSkywalker = 'Luke Skywalker';

// bad
const obj = {
  lukeSkywalker: lukeSkywalker,
};

// good
const obj = {
  lukeSkywalker,
};

속성의 단축 구문(Object Concise)은 객체 선언의 시작 부분에 무리를 지어줍니다.

왜죠? 어떤 속성이 단축 구문을 사용하고 있는지를 알기가 쉽기 때문입니다.

const anakinSkywalker = 'Anakin Skywalker';
const lukeSkywalker = 'Luke Skywalker';

// bad
const obj = {
  episodeOne: 1,
  twoJediWalkIntoACantina: 2,
  lukeSkywalker,
  episodeThree: 3,
  mayTheFourth: 4,
  anakinSkywalker,
};

// good
const obj = {
  lukeSkywalker,
  anakinSkywalker,
  episodeOne: 1,
  twoJediWalkIntoACantina: 2,
  episodeThree: 3,
  mayTheFourth: 4,
};

속성 이름에 작은 따옴표를 사용하는 경우는 오직 잘못된 식별자(Invalid Identifiers)일 때입니다.

왜죠? 주관적으로 쉽게 읽을 수 있는 것을 항상 고민해야 합니다. 이 것은 구문이 강조되고, 수많은 JS엔진에 쉽게 최적화되어 있습니다.

eslint rules: quote-props.

// bad
const bad = {
  'foo': 3,
  'bar': 4,
  'data-blah': 5,
};

// good
const good = {
  foo: 3,
  bar: 4,
  'data-blah': 5,
};

4. 배열(Arrays)

배열을 만들 때 리터럴 구문을 사용하세요.

eslint rules: no-array-constructor.

// bad
const items = new Array();

// good
const items = [];

배열에 항목을 직접 대체하지 말고 Array#push를 사용하세요.

const someStack = [];

// bad
someStack[someStack.length] = 'abracadabra';

// good
someStack.push('abracadabra');

배열을 복사하는 경우, 배열의 확장 연산자인 ...을 사용하세요.

// bad
const len = items.length;
const itemsCopy = [];
let i;

for (i = 0; i < len; i++) {
  itemsCopy[i] = items[i];
}

// good
const itemsCopy = [...items];

Array-Like 객체를 배열로 변환하려면 Array#from을 사용하세요.

const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);

5. 구조화 대입(Destructuring)

여러 속성에서 객체에 접근할 때 객체 구조화 대입(Destructuring)을 사용하세요.

왜죠? 구조화 대입을 이용하여 그 속성에 대한 중간 참조를 줄일 수 있습니다.

// bad
function getFullName(user) {
  const firstName = user.firstName;
  const lastName = user.lastName;

  return `${firstName} ${lastName}`;
}

// good
function getFullName(user) {
  const { firstName, lastName } = user;
  return `${firstName} ${lastName}`;
}

// best
function getFullName({ firstName, lastName }) {
  return `${firstName} ${lastName}`;
}

배열에 구조화 대입(Destructuring)을 사용하세요.

const arr = [1, 2, 3, 4];

// bad
const first = arr[0];
const second = arr[1];

// good
const [first, second] = arr;

여러 값을 반환하는 경우, 배열의 구조화 대입이 아니라 객체의 구조화 대입을 사용하세요.

왜죠? 이렇게하면 나중에 새 속성을 추가하거나 호출에 영향을 주지않고 순서를 변경할 수 있습니다.

// bad
function processInput(input) {
  // 그러면 기적이 일어난다
  return [left, right, top, bottom];
}

// 호출자에 반환되는 데이터의 순서를 고려해야 함
const [left, __, top] = processInput(input);

// good
function processInput(input) {
  // 그러면 기적이 일어난다
  return { left, right, top, bottom };
}

// 호출하면서 필요한 데이터만 선택할 수 있음
const { left, right } = processInput(input);

6. 문자열(Strings)

문자열에는 작은 따옴표''를 사용하세요.

eslint rules: quotes.

// bad
const name = "Capt. Janeway";

// good
const name = 'Capt. Janeway';

100자 이상의 문자열은 여러 행을 사용하여 연결할 수 있습니다.

주의: 문자열 연결이 많으면 성능에 영향을 줄 수 있습니다. jsPerf & Discussion.

// bad
const errorMessage = 'This is a super long error that was thrown because of Batman. When you stop to think about how Batman had anything to do with this, you would get nowhere fast.';

// bad
const errorMessage = 'This is a super long error that was thrown because \
of Batman. When you stop to think about how Batman had anything to do \
with this, you would get nowhere \
fast.';

// good
const errorMessage = 'This is a super long error that was thrown because ' +
  'of Batman. When you stop to think about how Batman had anything to do ' +
  'with this, you would get nowhere fast.';

프로그램에서 문자열을 생성하는 경우, 문자열 연결이 아닌 템플릿 문자열(Template Strings)을 사용하세요.

왜죠? 템플릿 문자열의 문자열 완성 기능과 다중 문자열 기능을 가진 간결한 구문으로 가독성이 좋아지기 때문입니다.

eslint rules: prefer-template.

// bad
function sayHi(name) {
  return 'How are you, ' + name + '?';
}

// bad
function sayHi(name) {
  return ['How are you, ', name, '?'].join();
}

// good
function sayHi(name) {
  return `How are you, ${name}?`;
}
절대로 eval()을 사용하지 않습니다. 이것은 지금까지 수많은 취약점을 만들어 왔기 때문입니다.

7. 함수(Functions)

함수 선언 대신에 함수 표현식을 사용합니다.

왜죠? 이름이 붙은 함수 선언은 콜스택에서 쉽게 알수 있습니다. 또한 함수 선언의 몸 전체가 Hoist됩니다. 반면 함수는 참조만 Hoist됩니다. 이 규칙은 함수 부분을 항상 애로우 함수로 대체 사용할 수 있습니다.

// bad
const foo = function () {
};

// good
function foo() {
}

함수 표현식(Function expressions):

// 즉시-호출(Immediately-Invoked) 함수 표현식(IIFE)
(() => {
  console.log('Welcome to the Internet. Please follow me.');
})();

함수 이외의 블록 (ifwhile 등)에 함수를 선언하지 않습니다. 브라우저는 변수에 함수를 할당하는 처리를 할 수는 있지만, 모두 다르게 해석됩니다.

주의: ECMA-262에서 block은 statements 목록에 정의되지만, 함수 선언은 statements가 없습니다. 이 문제는 ECMA-262의 설명을 참조하세요.

// bad
if (currentUser) {
  function test() {
    console.log('Nope.');
  }
}

// good
let test;
if (currentUser) {
  test = () => {
    console.log('Yup.');
  };
}

매개변수(parameter)에 arguments를 절대로 지정하지 않습니다. 이것은 함수 영역으로 전달 될 arguments객체의 참조를 덮어 써버릴 것입니다.

// bad
function nope(name, options, arguments) {
  // ...stuff...
}

// good
function yup(name, options, args) {
  // ...stuff...
}

arguments를 사용하지 않습니다. 대신 레스트(Rest) 문법인 ...을 사용하세요.

왜죠? ... 를 이용하여 여러가지 매개변수를 모두 사용할 수 있습니다. 추가로 rest 매개변수인 arguments는 Array-Like 객체가 아니라 진정한 배열(Array)입니다.

// bad
function concatenateAll() {
  const args = Array.prototype.slice.call(arguments);
  return args.join('');
}

// good
function concatenateAll(...args) {
  return args.join('');
}

함수의 매개변수를 조작하지 말고 기본 매개변수(Default Parameters)를 사용하세요.

// really bad
function handleThings(opts) {
  // 안되! 함수의 매개변수를 조작하지 않습니다. 
  // 만약 opts가 falsy 인 경우는 바란대로 객체가 설정됩니다. 
  // 그러나 미묘한 버그를 일으키는 원인이 될수도 있습니다. 
  opts = opts || {};
  // ...
}

// still bad
function handleThings(opts) {
  if (opts === void 0) {
    opts = {};
  }
  // ...
}

// good
function handleThings(opts = {}) {
  // ...
}

부작용이 있는 기본 매개변수를 사용하지 않습니다.

왜죠? 혼란스럽기 때문입니다.

var b = 1;
// bad
function count(a = b++) {
  console.log(a);
}
count();  // 1
count();  // 2
count(3); // 3
count();  // 3

항상 기본 매개변수는 앞쪽에 배치하세요.

// bad
function handleThings(opts = {}, name) {
  // ...
}

// good
function handleThings(name, opts = {}) {
  // ...
}

새로운 함수를 만드는 데 Function 생성자를 사용하지 않습니다.

왜죠? 이 방법은 문자열을 구분하는 새로운 함수를 만들 수 있는 eval()과 같은 취약점이 발생할 수 있습니다.

// bad
var add = new Function('a', 'b', 'return a + b');

// still bad
var subtract = Function('a', 'b', 'return a - b');

함수 시그네이쳐(Signature)에 공백을 사용합니다.

왜죠? 일관성이 좋고, 함수이름을 추가 하거나 삭제할 때 공백을 제거할 필요가 없습니다.

// bad
const f = function(){};
const g = function (){};
const h = function() {};

// good
const x = function () {};
const y = function a() {};

절대로 매개변수를 조작하지 않습니다.

왜죠? 매개변수로 전달 된 객체를 조작하는 것은 원래의 호출에 원치 않는 변수 부작용을 일으킬 수 있습니다.

eslint rules: no-param-reassign.

// bad
function f1(obj) {
  obj.key = 1;
};

// good
function f2(obj) {
  const key = Object.prototype.hasOwnProperty.call(obj, 'key') ? obj.key : 1;
};

절대로 매개변수를 다시 지정하지 않습니다.

왜죠? arguments 객체에 접근하는 경우 다시 지정된 매개변수는 예기치 않은 동작이 발생할 수 있습니다. 그리고 특히 V8 최적화에 문제가 발생할 수 있습니다.

eslint rules: no-param-reassign.

// bad
function f1(a) {
  a = 1;
}

function f2(a) {
  if (!a) { a = 1; }
}

// good
function f3(a) {
  const b = a || 1;
}

function f4(a = 1) {
}

8. 애로우 함수(Arrow Functions)

함수 표현식을 사용해야하는 경우(익명 함수와 같은), 애로우 함수(Arrow Functions)를 사용하세요.

왜죠? 애로우 함수는 함수가 실행되는 컨텍스트의 this를 가두어줍니다. 이것은 너무나 원했던 것이며 구문도 더욱 간결해집니다.

언제 쓰죠? 복잡한 함수 논리를 정의한 함수의 바깥쪽으로 이동하고 싶은 경우입니다.

eslint rules: prefer-arrow-callback, arrow-spacing.

// bad
[1, 2, 3].map(function (x) {
  const y = x + 1;
  return x * y;
});

// good
[1, 2, 3].map((x) => {
  const y = x + 1;
  return x * y;
});

함수의 본체가 하나의 표현식으로 구성되어있는 경우 중괄호{}를 생략하고 암묵적 return을 사용할 수 있습니다. 그렇지 않으면 return 문을 사용해야 합니다.

왜죠? 가독성이 좋아지기 때문입니다. 여러 함수가 연결되는 경우에 쉽게 읽을 수 있습니다.

언제 쓰죠? 객체를 반환하는 경우.

eslint rules: arrow-parens, arrow-body-style.

// good
[1, 2, 3].map(number => `A string containing the ${number}.`);

// bad
[1, 2, 3].map(number => {
  const nextNumber = number + 1;
  `A string containing the ${nextNumber}.`;
});

// good
[1, 2, 3].map(number => {
  const nextNumber = number + 1;
  return `A string containing the ${nextNumber}.`;
});

구문의 길이가 여러 행에 걸치는 경우 가독성을 향상시키기 위해 괄호() 안에 써주세요.

왜죠? 함수의 시작과 끝 부분을 알아보기 쉽게 합니다.

// bad
[1, 2, 3].map(number => 'As time went by, the string containing the ' +
  `${number} became much longer. So we needed to break it over multiple ` +
  'lines.'
);

// good
[1, 2, 3].map(number => (
  `As time went by, the string containing the ${number} became much ` +
  'longer. So we needed to break it over multiple lines.'
));

함수의 인수가 한 개인 경우 괄호()를 생략할 수 있습니다.

왜죠? 시각적 혼란이 덜하기 때문입니다.

eslint rules: arrow-parens.

// bad
[1, 2, 3].map((x) => x * x);

// good
[1, 2, 3].map(x => x * x);

// good
[1, 2, 3].map(number => (
  `A long string with the ${number}. It’s so long that we’ve broken it ` +
  'over multiple lines!'
));

// bad
[1, 2, 3].map(x => {
  const y = x + 1;
  return x * y;
});

// good
[1, 2, 3].map((x) => {
  const y = x + 1;
  return x * y;
});

9. 생성자(Constructors)

prototype의 직접 조작을 피하고 항상 class를 사용하세요.

왜죠? class 구문은 간결하고 의도를 알아내기가 쉽기 때문입니다.

// bad
function Queue(contents = []) {
  this._queue = [...contents];
}
Queue.prototype.pop = function () {
  const value = this._queue[0];
  this._queue.splice(0, 1);
  return value;
}


// good
class Queue {
  constructor(contents = []) {
    this._queue = [...contents];
  }
  pop() {
    const value = this._queue[0];
    this._queue.splice(0, 1);
    return value;
  }
}

상속에는 extends를 사용하세요.

왜죠? 프로토타입을 상속하기 위해 내장된 방식으로 instanceof 를 파괴할 수 없기 때문입니다.

// bad
const inherits = require('inherits');
function PeekableQueue(contents) {
  Queue.apply(this, contents);
}
inherits(PeekableQueue, Queue);
PeekableQueue.prototype.peek = function () {
  return this._queue[0];
}

// good
class PeekableQueue extends Queue {
  peek() {
    return this._queue[0];
  }
}

메소드의 반환값에 this를 돌려주는 것으로, 메소드 체인을 구현할 수 있습니다.

// bad
Jedi.prototype.jump = function () {
  this.jumping = true;
  return true;
};

Jedi.prototype.setHeight = function (height) {
  this.height = height;
};

const luke = new Jedi();
luke.jump(); // => true
luke.setHeight(20); // => undefined

// good
class Jedi {
  jump() {
    this.jumping = true;
    return this;
  }

  setHeight(height) {
    this.height = height;
    return this;
  }
}

const luke = new Jedi();

luke.jump()
  .setHeight(20);

조작된(Custom) toString() 메소드를 이용할 수도 있지만, 올바르게 작동 하는지, 부작용이 없는지를 꼭 확인하세요.

class Jedi {
  constructor(options = {}) {
    this.name = options.name || 'no name';
  }

  getName() {
    return this.name;
  }

  toString() {
    return `Jedi - ${this.getName()}`;
  }
}

10. 모듈(Modules)

비표준 모듈 시스템이 아니라면 항상 (import/export) 를 사용하세요. 이렇게 함으로써 원하는 모듈 시스템에 언제든지 트랜스파일(Transpile) 할 수 있습니다.

왜죠? 모듈은 곧 미래입니다. 미래를 선점하고 애용합시다.

// bad
const AirbnbStyleGuide = require('./AirbnbStyleGuide');
module.exports = AirbnbStyleGuide.es6;

// ok
import AirbnbStyleGuide from './AirbnbStyleGuide';
export default AirbnbStyleGuide.es6;

// best
import { es6 } from './AirbnbStyleGuide';
export default es6;

와일드카드를 이용한 가져오기는 사용하지 않습니다.

왜죠? single default export인 것에 주의할 필요가 있기 때문입니다.

// bad
import * as AirbnbStyleGuide from './AirbnbStyleGuide';

// good
import AirbnbStyleGuide from './AirbnbStyleGuide';

import 문에서 직접 추출(Export)하지 않습니다.

왜죠? 한개의 라인이라 간결하기는 하지만, import와 export하는 방법을 명확하게 구분함으로써 일관성을 유지할 수 있습니다.

// bad
// filename es6.js
export { es6 as default } from './airbnbStyleGuide';

// good
// filename es6.js
import { es6 } from './AirbnbStyleGuide';
export default es6;

11. 이터레이터와 제너레이터(Iterators and Generators)

이터레이터(Iterators)를 사용하지 않습니다. for-of 루프 대신 map()reduce()같은 자바스크립트의 고급함수(higher-order functions)를 사용하세요.

왜죠? 이것은 불변(Immutable)의 규칙을 적용합니다. 값을 반환하는 함수를 처리하는 것이 부작용을 예측하기가 더 쉽습니다.

eslint rules: no-iterator.

const numbers = [1, 2, 3, 4, 5];

// bad
let sum = 0;
for (let num of numbers) {
  sum += num;
}

sum === 15;

// good
let sum = 0;
numbers.forEach((num) => sum += num);
sum === 15;

// best (use the functional force)
const sum = numbers.reduce((total, num) => total + num, 0);
sum === 15;

현재 제너레이터(Generators)는 사용하지 않는 것이 좋습니다

왜죠? ES5에서 트랜스파일(Transpile)이 올바로 작동하지 않습니다.

12. 속성(Properties)

속성에 접근하려면 점.을 사용하세요.

eslint rules: dot-notation.

const luke = {
  jedi: true,
  age: 28,
};

// bad
const isJedi = luke['jedi'];

// good
const isJedi = luke.jedi;

변수를 사용하여 속성에 접근하려면 대괄호[]를 사용하세요.

const luke = {
  jedi: true,
  age: 28,
};

function getProp(prop) {
  return luke[prop];
}

const isJedi = getProp('jedi');

13. 변수(Variables)

변수를 선언할 때는 항상 const를 사용하세요. 그렇지 않으면 전역 변수로 선언됩니다. 글로벌 네임 스페이스가 오염되지 않도록 캡틴 플래닛(역자주: 환경보호와 생태를 테마로 한 슈퍼히어로 애니메이션)도 경고하고 있습니다.

// bad
superPower = new SuperPower();

// good
const superPower = new SuperPower();

하나의 변수 선언에 대해 하나의 const를 사용하세요.

왜죠? 이 방법은 새로운 변수를 쉽게 추가할 수 있습니다. 또한 구분 기호의 차이에 의한 ;,로 다시금 대체하는 작업에 대해 신경쓸 필요가 없습니다.

eslint rules: one-var.

// bad
const items = getItems(),
    goSportsTeam = true,
    dragonball = 'z';

// bad
// (compare to above, and try to spot the mistake)
const items = getItems(),
    goSportsTeam = true;
    dragonball = 'z';

// good
const items = getItems();
const goSportsTeam = true;
const dragonball = 'z';

먼저 const를 그룹화하고 그 다음으로 let을 그룹화 하세요.

왜죠? 이전에 할당 된 변수에 따라 나중에 새로운 변수를 추가하는 경우에 유용하기 때문입니다.

// bad
let i, len, dragonball,
    items = getItems(),
    goSportsTeam = true;

// bad
let i;
const items = getItems();
let dragonball;
const goSportsTeam = true;
let len;

// good
const goSportsTeam = true;
const items = getItems();
let dragonball;
let i;
let length;

변수를 할당을 필요로 하는 부분에서 적당한 장소에 배치해야 합니다.

왜죠? letconst는 함수 범위에는 없는 블록 범위이기 때문입니다.

// good
function () {
  test();
  console.log('doing stuff..');

  //..other stuff..

  const name = getName();

  if (name === 'test') {
    return false;
  }

  return name;
}

// bad - unnecessary function call
function (hasName) {
  const name = getName();

  if (!hasName) {
    return false;
  }

  this.setFirstName(name);

  return true;
}

// good
function (hasName) {
  if (!hasName) {
    return false;
  }

  const name = getName();
  this.setFirstName(name);

  return true;
}

14. 호이스팅(Hoisting)

var 선언은 할당이 없는 상태로 범위(Scope)의 위로 Hoist될 수 있습니다. 하지만 constlet 선언은 시간적 데드 존(Temporal Dead Zones (TDZ))이라는 새로운 개념의 혜택을 받고 있습니다. 이것은 왜 typeof가 안전하지 않은가(typeof is no longer safe)를 알고있는 것이 중요합니다.

// (notDefined가 글로벌 변수에 존재하지 않는다고 가정했을 경우)
// 이것은 잘 작동하지 않습니다. 
function example() {
  console.log(notDefined); // => throws a ReferenceError
}

// 변수를 참조하는 코드 후에 그 변수를 선언한 경우
// 변수가 Hoist되어서 작동합니다.
// 주의: `true` 값 자체는 Hoist할 수 없습니다.
function example() {
  console.log(declaredButNotAssigned); // => undefined
  var declaredButNotAssigned = true;
}

// 인터프린터는 변수 선언을 범위(Scope)의 시작부분에 Hoist합니다.
// 위의 예는 다음과 같이 다시 작성할 수 있습니다:
function example() {
  let declaredButNotAssigned;
  console.log(declaredButNotAssigned); // => undefined
  declaredButNotAssigned = true;
}

// const와 let을 사용하는 경우
function example() {
  console.log(declaredButNotAssigned); // => throws a ReferenceError
  console.log(typeof declaredButNotAssigned); // => throws a ReferenceError
  const declaredButNotAssigned = true;
}

익명 함수 표현식에서는 함수가 할당되기 전에 변수가 Hoist될 수 있습니다.

function example() {
  console.log(anonymous); // => undefined

  anonymous(); // => TypeError anonymous is not a function

  var anonymous = function () {
    console.log('anonymous function expression');
  };
}

명명된 함수의 경우도 마찬가지로 변수가 Hoist될 수 있습니다. 함수이름과 함수본문은 Hoist되지 않습니다.

function example() {
  console.log(named); // => undefined

  named(); // => TypeError named is not a function

  superPower(); // => ReferenceError superPower is not defined

  var named = function superPower() {
    console.log('Flying');
  };
}

// 함수이름과 변수이름이 같은 경우에도 같은 일이 일어납니다.
function example() {
  console.log(named); // => undefined

  named(); // => TypeError named is not a function

  var named = function named() {
    console.log('named');
  }
}

함수 선언은 함수이름과 함수본문이 Hoist됩니다.

function example() {
  superPower(); // => Flying

  function superPower() {
    console.log('Flying');
  }
}

더 자세한 정보는 Ben CherryJavaScript Scoping & Hoisting을 참조하세요.

15. 조건식과 등가식(Comparison Operators & Equality)

==!= 보다는 ===!==를 사용하세요.

if와 같은 조건문은 ToBoolean방법에 의한 강제 형(Type) 변환으로 구분되고 항상 다음과 같은 간단한 규칙을 따릅니다:

eslint rules: eqeqeq.

  • Objectstrue로 구분됩니다.
  • Undefinedfalse로 구분됩니다.
  • Nullfalse로 구분됩니다.
  • Booleansboolean형의 값으로 구분됩니다.
  • Numberstrue로 구분됩니다. 그러나, +0, -0, 또는 NaN인 경우 false로 구분됩니다.
  • Stringstrue로 구분됩니다. 그러나, 비어있는 ''경우는 false로 구분됩니다.
if ([0]) {
  // true
  // 배열은 객체이므로 true로 구분됩니다.
}

손쉬운 방법(Shortcuts)을 사용하세요.

// bad
if (name !== '') {
  // ...stuff...
}

// good
if (name) {
  // ...stuff...
}

// bad
if (collection.length > 0) {
  // ...stuff...
}

// good
if (collection.length) {
  // ...stuff...
}

더 자세한 내용은 여기를 참조하세요. Truth Equality and JavaScript by Angus Croll.

16. 블록(Blocks)

여러 줄의 블록은 중괄호{}를 사용합니다.

// bad
if (test)
  return false;

// good
if (test) return false;

// good
if (test) {
  return false;
}

// bad
function () { return false; }

// good
function () {
  return false;
}

여러 블록에 걸친 ifelse를 사용하는 경우, elseif블록의 끝 중괄호{}와 같은 행에 두세요.

eslint rules: brace-style.

// bad
if (test) {
  thing1();
  thing2();
}
else {
  thing3();
}

// good
if (test) {
  thing1();
  thing2();
} else {
  thing3();
}

17. 주석(Comments)

여러 줄의 주석에는 /** ... */를 사용하세요. 그 안에는 설명과 모든 매개변수와 반환값에 대한 형식과 값을 표기합니다.

// bad
// make() returns a new element
// based on the passed in tag name
//
// @param {String} tag
// @return {Element} element
function make(tag) {

  // ...stuff...

  return element;
}

// good
/**
 * make() returns a new element
 * based on the passed in tag name
 *
 * @param {String} tag
 * @return {Element} element
 */
function make(tag) {

  // ...stuff...

  return element;
}

한 줄 주석에는 //를 사용하세요. 주석을 추가하고 싶은 코드의 상단에 배치하세요. 또한 주석 앞에 빈 줄을 넣어주세요.

// bad
const active = true;  // is current tab

// good
// is current tab
const active = true;

// bad
function getType() {
  console.log('fetching type...');
  // set the default type to 'no type'
  const type = this._type || 'no type';

  return type;
}

// good
function getType() {
  console.log('fetching type...');

  // set the default type to 'no type'
  const type = this._type || 'no type';

  return type;
}

// also good
function getType() {
  // set the default type to 'no type'
  const type = this._type || 'no type';

  return type;
}

문제를 지적하고 재고를 촉구하거나 문제의 해결책을 제시하는 경우 등, 주석 앞에 FIXME 또는 TODO 를 붙이는 것으로 다른 개발자의 빠른 이해를 도울 수 있습니다. 이들은 어떠한 액션을 따른다는 의미에서 일반 댓글과 다를 수 있습니다. 액션은 FIXME -- 해결책 필요 또는 TODO -- 구현 필요.

문제에 대한 주석으로 // FIXME:를 사용하세요.

class Calculator extends Abacus {
  constructor() {
    super();

    // FIXME: shouldn't use a global here
    total = 0;
  }
}

해결책에 대한 주석으로 // TODO:를 사용하세요.

class Calculator extends Abacus {
  constructor() {
    super();

    // TODO: total should be configurable by an options param
    this.total = 0;
  }
}

18. 공백(Whitespace)

탭에는 공백 2개를 설정하세요.

eslint rules: indent.

// bad
function () {
∙∙∙∙const name;
}

// bad
function () {
∙const name;
}

// good
function () {
∙∙const name;
}

중괄호{} 앞에 공백을 넣어주세요.

eslint rules: space-before-blocks.

// bad
function test(){
  console.log('test');
}

// good
function test() {
  console.log('test');
}

// bad
dog.set('attr',{
  age: '1 year',
  breed: 'Bernese Mountain Dog',
});

// good
dog.set('attr', {
  age: '1 year',
  breed: 'Bernese Mountain Dog',
});

제어 구문(if, while 등)의 괄호() 앞에 공백을 넣어주세요. 함수 선언과 함수 호출시 인수 목록 앞에는 공백을 넣지 않습니다.

eslint rules: space-after-keywords, space-before-keywords.

// bad
if(isJedi) {
  fight ();
}

// good
if (isJedi) {
  fight();
}

// bad
function fight () {
  console.log ('Swooosh!');
}

// good
function fight() {
  console.log('Swooosh!');
}

연산자 사이에는 공백이 있습니다.

eslint rules: space-infix-ops.

// bad
const x=y+5;

// good
const x = y + 5;

파일의 마지막에 빈 줄을 하나 넣어주세요.

// bad
(function (global) {
  // ...stuff...
})(this);

// bad
(function (global) {
  // ...stuff...
})(this);↵
↵

// good
(function (global) {
  // ...stuff...
})(this);↵

메소드 체인이 길어지는 경우 적절히 들여쓰기(indentation) 하세요. 행이 메소드 호출이 아닌 새로운 문장임을 강조하기 위해 선두에 점.을 배치하세요.

// bad
$('#items').find('.selected').highlight().end().find('.open').updateCount();

// bad
$('#items').
  find('.selected').
    highlight().
    end().
  find('.open').
    updateCount();

// good
$('#items')
  .find('.selected')
    .highlight()
    .end()
  .find('.open')
    .updateCount();

// bad
const leds = stage.selectAll('.led').data(data).enter().append('svg:svg').class('led', true)
    .attr('width', (radius + margin) * 2).append('svg:g')
    .attr('transform', 'translate(' + (radius + margin) + ',' + (radius + margin) + ')')
    .call(tron.led);

// good
const leds = stage.selectAll('.led')
    .data(data)
  .enter().append('svg:svg')
    .classed('led', true)
    .attr('width', (radius + margin) * 2)
  .append('svg:g')
    .attr('transform', 'translate(' + (radius + margin) + ',' + (radius + margin) + ')')
    .call(tron.led);

블록과 다음 Statement 사이에 빈 줄을 넣어주세요.

// bad
if (foo) {
  return bar;
}
return baz;

// good
if (foo) {
  return bar;
}

return baz;

// bad
const obj = {
  foo() {
  },
  bar() {
  },
};
return obj;

// good
const obj = {
  foo() {
  },

  bar() {
  },
};

return obj;

// bad
const arr = [
  function foo() {
  },
  function bar() {
  },
];
return arr;

// good
const arr = [
  function foo() {
  },

  function bar() {
  },
];

return arr;

블록에 빈 줄을 끼워넣지 않습니다.

eslint rules: padded-blocks.

// bad
function bar() {

  console.log(foo);

}

// also bad
if (baz) {

  console.log(qux);
} else {
  console.log(foo);

}

// good
function bar() {
  console.log(foo);
}

// good
if (baz) {
  console.log(qux);
} else {
  console.log(foo);
}

괄호() 안에 공백을 추가하지 않습니다.

eslint rules: space-in-parens.

// bad
function bar( foo ) {
  return foo;
}

// good
function bar(foo) {
  return foo;
}

// bad
if ( foo ) {
  console.log(foo);
}

// good
if (foo) {
  console.log(foo);
}

대괄호[] 안에 공백을 추가하지 않습니다.

eslint rules: array-bracket-spacing.

// bad
const foo = [ 1, 2, 3 ];
console.log(foo[ 0 ]);

// good
const foo = [1, 2, 3];
console.log(foo[0]);

중괄호{} 안에 공백을 추가합니다.

eslint rules: object-curly-spacing.

// bad
const foo = {clark: 'kent'};

// good
const foo = { clark: 'kent' };

한 줄에 100문자(공백 포함)가 넘는 코드는 피하세요.

왜죠? 가독성과 유지 보수성을 보장합니다.

eslint rules: max-len.

// bad
const foo = 'Whatever national crop flips the window. The cartoon reverts within the screw. Whatever wizard constrains a helpful ally. The counterpart ascends!';

// bad
$.ajax({ method: 'POST', url: 'https://airbnb.com/', data: { name: 'John' } }).done(() => console.log('Congratulations!')).fail(() => console.log('You have failed this city.'));

// good
const foo = 'Whatever national crop flips the window. The cartoon reverts within the screw. ' +
  'Whatever wizard constrains a helpful ally. The counterpart ascends!';

// good
$.ajax({
  method: 'POST',
  url: 'https://airbnb.com/',
  data: { name: 'John' },
})
  .done(() => console.log('Congratulations!'))
  .fail(() => console.log('You have failed this city.'));

19. 쉼표(Commas)

쉼표로 시작: 제발 그만하세요.

eslint rules: comma-style.

// bad
const story = [
    once
  , upon
  , aTime
];

// good
const story = [
  once,
  upon,
  aTime,
];

// bad
const hero = {
    firstName: 'Ada'
  , lastName: 'Lovelace'
  , birthYear: 1815
  , superPower: 'computers'
};

// good
const hero = {
  firstName: 'Ada',
  lastName: 'Lovelace',
  birthYear: 1815,
  superPower: 'computers',
};

마지막에 쉼표: 좋습니다.

eslint rules: comma-dangle.

왜죠? 이것은 git의 diff를 깨끗하게 합니다. 또한 Babel과 같은 트랜스 컴파일러는 끝에 불필요한 쉼표를 알아서 제거합니다. 이것은 기존 브라우저에서 불필요한 쉼표 문제를 걱정할 필요가 없다는 것을 의미합니다.

// bad - git diff without trailing comma
const hero = {
     firstName: 'Florence',
-    lastName: 'Nightingale'
+    lastName: 'Nightingale',
+    inventorOf: ['coxcomb graph', 'modern nursing']
};

// good - git diff with trailing comma
const hero = {
     firstName: 'Florence',
     lastName: 'Nightingale',
+    inventorOf: ['coxcomb chart', 'modern nursing'],
};

// bad
const hero = {
  firstName: 'Dana',
  lastName: 'Scully'
};

const heroes = [
  'Batman',
  'Superman'
];

// good
const hero = {
  firstName: 'Dana',
  lastName: 'Scully',
};

const heroes = [
  'Batman',
  'Superman',
];

20. 세미콜론(Semicolons)

물론 사용합시다.

eslint rules: semi.

// bad
(function () {
  const name = 'Skywalker'
  return name
})()

// good
(() => {
  const name = 'Skywalker';
  return name;
})();

// good (guards against the function becoming an argument when two files with IIFEs are concatenated)
;(() => {
  const name = 'Skywalker';
  return name;
})();

Read more.

21. 형변환과 강제(Type Casting & Coercion)

문장의 시작 부분에서 형(Type)을 강제합니다.

String:

//  => this.reviewScore = 9;

// bad
const totalScore = this.reviewScore + '';

// good
const totalScore = String(this.reviewScore);

Number: Number형으로 변환하려면 parseInt를 사용하세요. 항상 형변환을 위한 기수(radix)를 인수로 전달합니다.

eslint rules: radix.

const inputValue = '4';

// bad
const val = new Number(inputValue);

// bad
const val = +inputValue;

// bad
const val = inputValue >> 0;

// bad
const val = parseInt(inputValue);

// good
const val = Number(inputValue);

// good
const val = parseInt(inputValue, 10);

어떤 이유로 parseInt가 병목이되고, 성능적인 이유에서 Bitshift를 사용해야 하는 경우, 무엇을(what) 왜(why)에 대한 설명을 댓글로 남겨 주세요.

// good
/**
 * parseInt가 병목이되고 있었기 때문에, 
 * Bitshift 문자열을 수치로 강제로 변환하여 
 * 성능을 향상시킵니다.
 */
const val = inputValue >> 0;

주의: Bitshift를 사용하는 경우 수치는 64-비트 값들로 표현되어 있지만, Bitshift를 연산하면 항상 32-비트 단 정밀도로 돌려 주어집니다(source). 32-비트 이상의 값을 비트 이동하면 예상치 못한 행동을 일으킬 가능성이 있습니다. Discussion. 부호있는 32-비트 정수의 최대 값은 2,147,483,647입니다:

2147483647 >> 0 //=> 2147483647
2147483648 >> 0 //=> -2147483648
2147483649 >> 0 //=> -2147483647

Booleans:

const age = 0;

// bad
const hasAge = new Boolean(age);

// good
const hasAge = Boolean(age);

// good
const hasAge = !!age;

22. 명명 규칙(Naming Conventions)

하나의 문자로 구성된 이름은 피하세요. 이름에서 의도를 읽을 수 있도록 해야 합니다.

// bad
function q() {
  // ...stuff...
}

// good
function query() {
  // ..stuff..
}

객체, 함수 인스턴스에는 camelCase(소문자로 시작)를 사용하세요.

eslint rules: camelcase.

// bad
const OBJEcttsssss = {};
const this_is_my_object = {};
function c() {}

// good
const thisIsMyObject = {};
function thisIsMyFunction() {}

클래스와 생성자는 PascalCase(대문자로 시작)를 사용하세요.

// bad
function user(options) {
  this.name = options.name;
}

const bad = new user({
  name: 'nope',
});

// good
class User {
  constructor(options) {
    this.name = options.name;
  }
}

const good = new User({
  name: 'yup',
});

Private 속성 이름은 앞에 밑줄_을 사용하세요.

eslint rules: no-underscore-dangle.

// bad
this.__firstName__ = 'Panda';
this.firstName_ = 'Panda';

// good
this._firstName = 'Panda';

this에 대한 참조를 저장하지 않습니다. 애로우 함수 또는 Function#bind를 사용하세요.

// bad
function foo() {
  const self = this;
  return function () {
    console.log(self);
  };
}

// bad
function foo() {
  const that = this;
  return function () {
    console.log(that);
  };
}

// good
function foo() {
  return () => {
    console.log(this);
  };
}

파일을 하나의 클래스로 추출(Export)할 경우 파일 이름은 클래스 이름과 정확하게 일치해야 합니다.

// file contents
class CheckBox {
  // ...
}
export default CheckBox;

// in some other file
// bad
import CheckBox from './checkBox';

// bad
import CheckBox from './check_box';

// good
import CheckBox from './CheckBox';

export-default 함수의 경우, camelCase(소문자로 시작)를 사용하세요. 파일이름은 함수이름과 동일해야 합니다.

function makeStyleGuide() {
}

export default makeStyleGuide;

싱글톤(singleton) / 함수 라이브러리(function library) / 단순한 객체(bare object)를 추출하는 경우, PascalCase(대문자로 시작)를 사용하세요.

const AirbnbStyleGuide = {
  es6: {
  }
};

export default AirbnbStyleGuide;

23. 액세서(Accessors)

속성에 대한 접근자(Accessor) 함수는 필요하지 않습니다.

접근자 함수가 필요한 경우 getVal()setVal('hello')로 하세요.

// bad
dragon.age();

// good
dragon.getAge();

// bad
dragon.age(25);

// good
dragon.setAge(25);

속성이 boolean의 경우 isVal() 또는 hasVal()로 하세요.

// bad
if (!dragon.age()) {
  return false;
}

// good
if (!dragon.hasAge()) {
  return false;
}

일관된다면, get()set() 함수를 작성해도 좋습니다.

class Jedi {
  constructor(options = {}) {
    const lightsaber = options.lightsaber || 'blue';
    this.set('lightsaber', lightsaber);
  }

  set(key, val) {
    this[key] = val;
  }

  get(key) {
    return this[key];
  }
}

24. 이벤트(Events)

(DOM 이벤트, Backbone 이벤트)처럼 자신의 이벤트 페이로드 값을 전달하려면 원시값 대신 해시인수를 전달합니다. 이렇게 하면 나중에 개발자가 이벤트에 관련된 모든 핸들러를 찾아 업데이트하지 않고 이벤트 페이로드에 값을 추가할 수 있습니다. 예를 들면:

// bad
$(this).trigger('listingUpdated', listing.id);

...

$(this).on('listingUpdated', function (e, listingId) {
  // do something with listingId
});

보다 아래쪽이 더 선호됨:

// good
$(this).trigger('listingUpdated', { listingId: listing.id });

...

$(this).on('listingUpdated', function (e, data) {
  // do something with data.listingId
});

25. jQuery

jQuery 객체 변수 앞에는 $로 구분합니다.

// bad
const sidebar = $('.sidebar');

// good
const $sidebar = $('.sidebar');

// good
const $sidebarBtn = $('.sidebar-btn');

jQuery의 검색 결과를 캐시합니다.

// bad
function setSidebar() {
  $('.sidebar').hide();

  // ...stuff...

  $('.sidebar').css({
    'background-color': 'pink'
  });
}

// good
function setSidebar() {
  const $sidebar = $('.sidebar');
  $sidebar.hide();

  // ...stuff...

  $sidebar.css({
    'background-color': 'pink'
  });
}

DOM의 검색에는 $('.sidebar ul') 또는 $('.sidebar > ul')과 같은 Cascading을 사용하세요. jsPerf

jQuery 객체의 검색에는 범위가있는 find 를 사용하세요.

// bad
$('ul', '.sidebar').hide();

// bad
$('.sidebar').find('ul').hide();

// good
$('.sidebar ul').hide();

// good
$('.sidebar > ul').hide();

// good
$sidebar.find('ul').hide();

26. ECMAScript 5 호환성(ECMAScript 5 Compatibility)

Kangax의 ES5 호환성 표를 참조하세요.

27. ECMAScript 6 스타일(ECMAScript 6 Styles)

이것은 ES6 명세 링크를 모아 놓은 것입니다.

28. 테스팅(Testing)

물론 해야 합니다.

function () {
  return true;
}

물론 심각하게:

  • 대부분 테스트 프레임워크를 이용하여 테스트를 작성합니다.
  • 작은 기능의 함수를 자주 쓰고 이변이 발생할 수 있는 부분을 최소화하기 위해 노력합니다.
  • stubs와 mocks에 주의하세요. 이 것들로 인해 테스트가 깨지기 쉽습니다.
  • Airbnb는 mocha를 이용하고 있습니다. 작게 분할된 개별 모듈은 tape을 사용합니다.
  • 지금은 달성할 필요가 없어도 100%의 테스트 커버리지를 목표로하는 것이 좋습니다.
  • 버그를 수정할 때 마다 회귀 테스트를 씁니다. 회귀 테스트 없는 버그 수정은 나중에 반드시 다시 출현할 것입니다.

Comments