본 글은 아래의 자바스크립트 코딩 스타일 가이드를 참고 및 번역하여 작성되었습니다.
의역 및 잘못된 해석으로 잘못된 정보가 포함될 수 있으니 원문과 함께 참고해주세요.
Airbnb
Nexedi
W3S School
웹 시장이 방대해지면서 많은 웹 개발자와 함께 많은 서비스가 출시되고 있습니다.
웹에 대한 직군은 크게 사용자가 직접 사용하고 볼 수 있는 프론트엔드와 데이터베이스와 맞물려 데이터를 가공하여 프론트엔드에 그 데이터를 제공하는 서버인 백엔드로 나눌 수 있습니다. 따라서 웹 서비스를 개발하기 위해서는 프론트엔드 개발자와 백엔드 개발자의 협엽이 반드시 필요합니다.(풀스택 개발자가 있다면 상황이 달라지겠지만)
원활한 협업과 높은 일률, 그리고 유지보수를 위해서는 가독성이 좋아야하고 일관되고 합의된 스타일 가이드에 따라 코드를 작성해야 합니다.
1편에서는 코딩 컨벤션에 대한 내용을 다루고, 2편에서는 네이밍 컨벤션에 대한 내용을 다룹니다. 오역 및 잘못된 해석으로 인해 잘못된 내용이 들어갈 수 있으니 원문과 함께 참고해주세요.
또, 각 스타일에 대한 자세한 설명은 생략하였습니다. 자세한 설명을 원하신다면 원문을 참고해주세요.
// bad
if (someVeryLongCondition())
doSomething();
// No
for (let i = 0; i < foo.length; i++) bar(foo[i]);
// Yes
if (shortCondition()) foo();
중괄호의 사용은 Kernighan and Ritchie Style에 따른다.
class InnerClass {
constructor() {}
/** @param {number} foo */
method(foo) {
if (condition(foo)) {
try {
something();
} catch (err) {
recover();
}
}
}
}
빈 블록의 경우, 여는 중괄호( { ) 바로 다음에 닫는 중괄호를 사용한다.
단, if-else 구조 혹은 try-catch-finally 구조에서의 빈 블록에서는 줄을 바꾼다.
// Yes
function doNothing() {}
// No
if (condition) {
// …
} else if (otherCondition) {} else {
// …
}
// No
try {
// …
} catch (e) {}
새로운 블록이 나타날 때 마다 두 칸의 공백(스페이스바)를 통해 들여쓴다.
배열, 객체는 블록과 동일하게 들여쓴다.
클래스
함수 표현식을 작성할 대에는 함수를 이전 레벨보다 2칸 들여써 작성한다.
prefix.something.LongFunctionName('whatever', (a1, a2) => {
// Indent the function body +2 relative to indentation depth
// of the 'prefix' statement one line above.
if (a1.equals(a2)) {
someOtherLongFunctionName(a1);
} else {
andNowForSomethingCompletelyDifferent(a2.parrot);
}
});
스위치문은 블록과 동일하게 들여쓰며, break와 다음 case 사이의 공백은 선택
switch (animal) {
case Animal.BANDERSNATCH:
handleBandersnatch();
break;
case Animal.JABBERWOCK:
handleJabberwock();
break;
default:
throw new Error('Unknown animal');
}
점( . )을 이용하여 긴 메소드 체인을 작성할 때는 각 메소드마다 들여쓴다.
// 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').classed('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);
// good
const leds = stage.selectAll('.led').data(data);
한 줄에는 하나의 구문만을 작성한다.
모든 구문의 끝에는 세미콜론을 붙인다.
// bad
let name = 'Cada'
doSomething() saySomething()
// good
let name = 'Cada';
doSomething();
saySomething();
한 줄이 80자를 넘기지 않도록 한다.
줄 공백 (Vertical Whitespace)
// 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;
칸 공백 (Horizontal Whitespace)
칸 공백은 리딩(줄의 시작), 트레일링(줄의 끝), 인터널로 나눌 수 있다.
리딩 공백은 들여쓰기 규칙에 따라 언제든지 사용될 수 있다.
트레일링 공백은 사용하지 않는다.
칸 공백은 아래의 사항에서 나타날 수 있다.
if, for, catch와 같은 키워드와 소괄호 사이
(단, function과 super는 예외)
// bad
if(myCondition) {
// do something
}
// good
if (myCondition) {
// do something
}
else, catch와 같은 키워드와 닫는 중괄호 사이
// bad
if(myCondition) {
// do something
}else { }
// good
if (myCondition) {
// do something
} else { }
여는 중괄호 ( { ) 전
(단, 오브젝트 리터럴의 첫 인자나 배열의 첫 인자, 템플릿 리터럴은 예외)
이항 연산자와 삼항 연산자의 양 쪽
// bad
const name = someCondotion?20:30;
// good
const name = someCondotion ? 20 : 30;
반점( , ), 세미콜론( ; ) 다음 (단, 반점과 세미콜론 전에는 사용하지 않음)
// bad
const arr = [1,2,3,4];
// good
const arr = [1, 2, 3, 4];
오브젝트 리터럴 내에서 콜론( : ) 다음
// bad
const myobject = {
"name":"Chris",
"age":34,
};
// good
const myobject = {
"name": "Chris",
"age": 34,
};
// 양 쪽, /* 다음, */ 전
소괄호 사이, 대괄호 사이에는 칸 공백을 사용하지 않는다.
// bad
function bar( foo ) {
return foo;
}
// good
function bar(foo) {
return foo;
}
// bad
if ( foo ) {
console.log(foo);
}
// good
if (foo) {
console.log(foo);
}
// bad
const foo = [ 1, 2, 3 ];
console.log(foo[ 0 ]);
// good
const foo = [1, 2, 3];
console.log(foo[0]);
중괄호 사이에는 칸 공백을 사용한다.
// bad
const foo = {clark: 'kent'};
// good
const foo = { clark: 'kent' };
한 줄에 하나의 변수를 선언한다.
// bad
let a = 1, b = 3;
// good
let a = 1;
let b = 2;
지역 변수는 그 변수를 포함하는 블록 시작에서 선언하지 않고, 사용 범위를 최소화하기 위해 사용되는 지점과 가장 가까운 곳에서 선언한다.
// good
function() {
test();
console.log('doing stuff..');
const name = getName();
if (name === 'test') {
return false;
}
return name;
}
// bad - 함수 호출의 부적절한 위치
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;
}
JSDoc을 위한 주석은 변수 선언 이전 혹은 변수 이름 이전에 작성한다.
(단, 두 가지 위치에 동시에 모두 작성하지 않는다)
// bad
/** Some description. */
const /** !Array<number> */ data = [];
const /** !Array<number> */ data = [];
// good
/**
* Some description.
* @type {!Array<number>}
*/
const data = [];
변수를 선언할 때는 const를 사용한다.
단, 변수의 값이 바뀌는 경우 let을 사용한다.
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;
배열을 선언할 때는 Array 생성자가 아닌 리터럴 구문을 사용한다.
// 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.from을 사용한다.
const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);
배열의 선언 시, 마지막 원소를 포함하여 각 원소 끝에는 점( . )을 포함한다.
const values = [
'first value',
'second value',
'third value',
];
배열에 숫자가 아닌 속성을 사용하지 않는다.
사용을 원할경우 맵 혹은 오브젝트를 사용한다.
// bad
const arr = [];
arr["str"] = 32;
// good
const obj = {};
arr.str = 32;
const map = new Map();
map.set("str", 32);
한 배열로부터 복수개의 값을 할당받을 때는 구조 분해 할당을 사용한다.
이는 함수의 파라미터에서도 사용할 수 있다.
const [a, b, c, ...rest] = generateResults();
let [, b,, d] = someArray;
function optionalDestructuring([a = 4, b = 2] = []) { … };
오브젝트를 선언할 때는 Object 생성자가 아닌 리터럴 구문을 사용한다.
// bad
const item = new Object();
// good
const item = {};
예약어를 키로 사용하지 않는다. 대신, 동의어를 사용한다.
// bad
const superman = {
default: { clark: 'kent' },
private: true,
};
// good
const superman = {
defaults: { clark: 'kent' },
hidden: true,
};
키는 큰 따옴표( " )를 씌운다.
단, 큰 따옴표를 씌운 키와 씌우지 않은 키를 한 오브젝트에 동시에 사용하지 않는다.
// bad
var my_object = {
key: "value",
};
var my_object = {
key: "value",
"key2": "value2",
};
// good
var my_object = {
"key": "value",
"key2": "value2",
};
동적 속성명을 사용할 때는, 계산된 속성명(Computed Porperty Name)을 사용한다.
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
};
메소드 단축 구문을 사용한다.
// bad
const atom = {
value: 1,
addValue: function (value) {
return atom.value + value;
},
};
// good
const atom = {
value: 1,
addValue(value) {
return atom.value + value;
},
};
속성 단축 구문을 사용한다.
단, 선언의 시작 부분에 그룹화하여 작성한다.
const lukeSkywalker = 'Luke Skywalker';
// bad
const obj = {
lukeSkywalker: lukeSkywalker,
};
// good
const obj = {
lukeSkywalker,
};
// 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,
};
생성자는 선택적으로 작성한다. 하지만 하위 클래스는 필드를 설정하기 전에 반드시 super()를 호출해야한다. 그렇지 않으면 this에 접근할 것이다.
인터페이스의 경우 메소드가 아닌 속성을 생성자에서 반드시 선언해야 한다.
정적 메소드의 사용 보다는 모듈 함수를 더 지향한다.
class Base { /** @nocollapse */ static foo() {} }
class Sub extends Base {}
function callFoo(cls) { cls.foo(); } // Bad: 동적 인스턴스를 통해 호출되어서는 안된다.
Sub.foo(); // Bad: 하위 클래스 내에 선언되지 않은 정적 메소드는 호출되어서는 안된다.
prototype을 직접 조작하지 않고 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를 사용한다.
// 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);
toString() 메소드를 오버라이딩할 수 있지만 사이드 이펙트가 나타나지 않도록 해야한다.
class Jedi {
constructor(options = {}) {
this.name = options.name || 'no name'; // null이 반환되는 것을 방지
}
getName() {
return this.name;
}
toString() {
return `Jedi - ${this.getName()}`;
}
}
함수 내에 또다른 함수를 선언할 수 있다. 함수에 이름이 필요한 경우 const를 사용한다.
함수식 보다는 함수 선언을 사용한다.
// bad
const foo = function () { // foo만 hoist됨
};
// good
function foo() { // 함수 전체가 hoist됨
}
함수 이외의 블록(i.e. if, while) 안에서 함수를 선언하지 않는다.
함수의 마라미터에 arguments를 사용하지 않는다. 이는 오브젝트의 참조를 덮어쓰는 것을 야기한다.
arguments를 사용하는 것 대신에 Rest 신택스( ... )를 사용한다.
// bad
function concatenateAll() {
const args = Array.prototype.slice.call(arguments);
return args.join('');
}
// good
function concatenateAll(...args) {
return args.join('');
}
함수의 파라미터를 조작하는 것보다는 파라미터 기본값을 사용한다.
// 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 = {}) {
// ...
}
함수식을 사용해야만한다면, 화살표 함수(Arrow Function)을 사용하라.
// 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문을 포함시켜야 한다.
// 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.'
));
함수의 인수가 하나인 경우 소괄호를 생략할 수 있다.
// good
[1, 2, 3].map(x => x * x);
// good
[1, 2, 3].reduce((y, x) => x + y);
문자열을 선언할 때는 작은 따옴표( ' )를 사용한다.
문자열 내에 작은 따옴표가 포함될 경우 템플릿 리터럴( ` ` )을 사용한다.
// bad
const name = "Capt. Janeway";
// good
const name = 'Capt. Janeway';
// good
const name = `Mark J' Maclachlan`;
80글자 이상의 긴 문자열을 여러 줄에 걸쳐 쓰기 위해서는 템플릿 리터럴 혹은 문자열 연결( + )을 사용한다.
// 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.';
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
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() 함수를 사용하지 않는다. 취약점이 많은 함수이다.
일반적인 for문 보다는 for-of를 사용한다.
하지만, 가능하다면 map(), reduce()와 같은 고급 함수를 사용하라.
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;
switch (input) {
case 1:
case 2:
prepareOneOrTwo();
// fall through
case 3:
handleOneTwoOrThree();
break;
default:
handleLargeNumber(input);
}
if ([0]) {
// true
// An array is an object, objects evaluate to true
// 배열은 오브젝트이므로 true 로 평가됩니다.
}
2편에서는 네이밍 컨벤션에 대한 내용이 이어집니다.
잘못된 내용이 있다면 코멘트 남겨주세요!
변수 항목에 있는
배열의 선언 시, 마지막 원소를 포함하여 각 원소 끝에는 점( . )을 포함한다.
에 점이 혹시 반점( , )으로 이해해도 될까요