CleanCode for JavaScript -1
2018-03-22
클린코드 for 자바스크립트
- 변수(Variables)
- 함수(Functions)
이 글은 단순히 스타일 가이드가 아니라 자바스크립트로 코드를 작성할때 읽기 쉽고, 재사용 가능하며 리팩토링 가능하게끔 작성하도록 돕기 위한 글이다.
모든 원칙이 엄격히 지켜져야하는 것은 아니다. 이것들은 지침 일뿐이다.
변수(Variables)
- 의미있고 발음하기 쉬운 변수 이름을 사용하자
// BAD
const yyyymmdstr = moment().format('YYYY/MMDD');
// GOOD
const currentData = moment().format('YYYY/MMDD');
- 검색가능한 이름을 사용하자
- 작성할 코드보다 읽을 코드가 더 많기 때문에 읽기 쉽고 검색가능하게 작성해야한다.
// BAD
setTimeout(blastOff, 86400000); // 86400000의 의미를 알 수 없다.
// GOOD
const MILLISECONDS_IN_A_DAY = 86400000; // const 변수를를 대문자로 선언
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
// 매직넘버 관리!!!
- 의도를 나타내는 변수를 사용하자
- 의도는 알겠는데 코드는 모르겠다
// BAD
const address = 'One Infinite Loop, Cupertino 95014';
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(address.match(cityZipCodeRegex)[1], address.match(cityZipCodeRegex)[2]);
// GOOD
const address = 'One Infinite Loop, Cupertino 95014';
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [, city, zipCode] = address.match(cityZipCodeRegex) || [];
saveCityZipCode(city, zipCode);
- 자신만 알아볼 수 있는 작명을 피하자
- 명시적인 것이 암시적인 것보다 좋다.
// BAD
const loacations = ['seoul', 'suwon', 'busan'];
locations.forEach((l) => { // l의 의미를 알지 못한다
doStuff();
doSomeOtherStuff();
dispatch(l);
})
// GOOD
const locations = ['seoul', 'suwon', 'busan'];
locations.forEach(location => { // locations을 location으로 변경
doStuff();
doSomeOtherStuff();
dispatch(location);
});
- 문맥상 필요 없는 것들을 쓰지 말자
// BAD
const car = {
carColor: 'blue'
}
function paintCar(car){
car.carColor = 'red';
}
// GOOD
const car = {
color: 'blue'
}
function paintCar(car){
car.color = 'red';
}
함수 (Functions)
함수 인자는 2개 이하가 인상적이다.
매개변수의 개수를 제한하는것은 테스팅을 쉽게 만들어 주기 때문에 중요하다.
1개나 2개의 인자 를 가지고 있는 것이 가장 이상적인 케이스이고 3개의 인자는 가능한 피해야한다.
만약 인자가 더 많아진다면 통합되어야 한다.
인자가 많다는 것은 함수가 너무 많은 역할을 하고 있다는 것.
- 함수가 기대하는 속성을 좀더 명확히 하기 위해서 es6의 비구조화(destructuring) 구문을 사용할 수 있다.
- 어떤 사람이 그 함수의 시그니쳐(인자의 타입, 반환되는 값의 타입 등)를 볼 때 어떤 속성이 사용되는지 즉시 알 수 있다.
- 비구조화는 함수에 전달된 인수 객체의 지정된 기본타입 값을 복제하며 이는 사이드이펙트가 일어나는 것을 방지한다. 참고로 인수 객체로부터 비구조화된 객체와 배열은 복제되지 않는다.
- Linter를 사용하면 사용하지않는 인자에 대해 경고해주거나 비구조화 없이 코드를 짤 수 없게 할 수 있다.
// BAD
function createMenu(title, body, buttonText, cancellable) {
// ...
}
// GOOD
function createMenu({title, body, buttonText, cancellable}){
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
- 함수는 하나의 행동만 해야 한다.
함수가 1개 이상의 행동을 한다면 작성, 테스트, 이해 모두 어려워 진다. 함수에 하나의 행동을 정의하게 된다면 함수는 좀 더 고치기 쉬워지고 읽기 쉬워진다.
// BAD
function emailClients(clients) {
clients.forEach(client => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) { // 함수안에 두개의 행동
email(client);
}
});
}
// GOOD
// 행동을 분리함
function emailClients(clients) {
clients
.filter(isClientActive)
.forEach(email);
}
function isClientActive(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
-
함수명은 함수가 무엇을 하는지 알 수 있어야 한다,
-
함수는 단일 행동을 추상화 해야한다. 추상화된 이름이 여러 의미를 내포하고 있다면, 그 함수는 너무 많은 일을 하게 설계된것이다.
// BAD
function parseBetterJSAlternative(code) { // 함수명이 여러 의미를 내포하고 있다.
const REGEXES = [
// ...
];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach(REGEX => {
statements.forEach(statement => {
// ...
});
});
const ast = [];
tokens.forEach(token => {
// lex...
});
ast.forEach(node => {
// parse...
});
}
// GOOD
function tokenize(code) {
const REGEXES = [
// ...
];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach(REGEX => {
statements.forEach(statement => {
tokens.push( /* ... */ );
});
});
return tokens;
}
function lexer(tokens) {
const ast = [];
tokens.forEach(token => {
ast.push( /* ... */ );
});
return ast;
}
function parseBetterJSAlternative(code) { // 여러 의미를 내포하는 함수내부의 행동들을 나눔
const tokens = tokenize(code);
const ast = lexer(tokens);
ast.forEach(node => {
// parse...
});
}
- 중복된 코드를 작성하지 마라.
- 중복된 코드가 있다는 것은 어떤 로직을 수정해야 할 일이 생겼을 때 수정 해야할 코드가 한 곳 이상이라는 것을 뜻한다.
// BAD
function showDeveloperList(developers) {
developers.forEach(developers => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};
render(data);
});
}
function showManagerList(managers) {
managers.forEach(manager => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
// GOOD
function showEmployeeList(employees) {
employees.forEach((employee) => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
let portfolio = employee.getGithubLink();
if (employee.type === 'manager') {
portfolio = employee.getMBAProjects();
}
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
- 매개변수로 플래그를 사용하지 마라.
- 플래그를 사용하는 것 자체가 그 함수가 한가지 이상의 역할을 하고 있다는 것을 뜻한다.
- Boolean 기반으로 함수가 실행되는 코드가 나뉜다면 함수를 분리해라.
// BAD
function createFile(name, temp){
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
// GOOD
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}
사이드 이펙트를 피해라 - 1
- 함수는 값을 받아서 어떤 일을 하거나 값을 리턴할때 사이드이펙트를 만들어낸다.
- 어떠한 구조체도 없이 객체 사이의 상태를 공유하거나, 같은 사이드이펙트를 만들어 내는 함수를 여러개 만들면 안된다.
// BAD
// 아래 함수에 의해 참조되는 전역 변수다.
// 이 전역 변수를 사용하는 또 하나의 함수가 있다고 생각해자. 이제 이 변수는 배열이 될 것이고, 프로그램을 망가뜨리게 된다.
let name = 'Ryan McDermott';
function splitIntoFirstAndLastName() {
name = name.split(' ');
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];
// GOOD
function splitIntoFirstAndLastName(name) {
return name.split(' ');
}
const name = 'Ryan McDermott';
const newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];
사이드 이펙트를 피해라 - 2
- 실제로 입력된 객체를 수정하고 싶은 경우가 있을 수 있지만 예제를 보고 적용해보면 그런 경우는 거의 없다는 것을 알 수 있다.
- 큰 객체를 복제하는것은 성능 측면에서 매우 비싼 코스트다. 하지만 큰 문제가 되지는 않는다. 왜냐하면 좋은라이브러리를 사용하면 해결 할 수 있다. 이는 개체와 배열을 수동으로 복제하는 것처럼 메모리 집약적이지 않게 하고, 빠르게 복제해준다.
// BAD
const addItemToCart = (cart, item) => {
car.push({item, data:Date.now()});
};
// GOOD
const addItemToCart = (cart, item) => {
return [...cart, {item, date : Date.now()}]
};
- 전역 함수를 사용하지 마라.
- 전역 환경을 사용하는 것은 JavaScript에서 나쁜 관행이다. 왜냐하면 다른 라이브러리들과 충돌이 날 수 있고, 당신의 API를 쓰는 유저들은 운영환경에서 예외가 발생하기 전까지 문제를 인지 못하기 때문이다.
Array메소드를 확장하여 두 배열간의 차이를 보여주는 diff 메소드를 사용하려면 새로운 함수를 Array.prototype에 쓸 수도 있지만, 똑같은 일을 시도한 다른 라이브러이와 충돌 할 수 있다.
ES6을 사용하여 전역 Array를 상속 받는게 더 나은 방법이다.
// BAD
Array.prototype.diff = function diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
};
// GOOD
class SuperArray extends Array {
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}
-
명령형 프로그래밍보다 함수형 프로그래밍을 지향해라.
- 함수형 언어는 더 깔끔하고 테스트하기 쉽다.
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
// BAD
let totalOutput = 0;
for (let i = 0; i < programmerOutput.length; i++) {
totalOutput += programmerOutput[i].linesOfCode;
}
// GOOD
const totalOutput = programmerOutput
.map(programmer => programmer.linesOfCode)
.reduce((acc, linesOfCode) => acc + linesOfCode, INITIAL_VALUE);
- 조건문을 캡슐화 하자.
// BAD
if (fsm.state === 'fetching' && isEmpty(listNode)) {
// ...
}
// GOOD
function shouldShowSpinner(fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
- 부정조건문을 사용하지 마라.
function isDOMNodeNotPresent(node) {
// ...
}
// BAD
if (!isDOMNodeNotPresent(node)) {
// ...
}
// GOOD
if (isDOMNodePresent(node)) {
// ...
}
- 조건문 작성을 피해라.
- 함수나 클래스에 if문을 쓴다면 그것은 그 함수나 클래스가 한가지 이상의 일을 수행하고 있다는것과 같다.
// BAD
class Airplane {
// ...
getCruisingAltitude() {
switch (this.type) {
case '777':
return this.getMaxAltitude() - this.getPassengerCount();
case 'Air Force One':
return this.getMaxAltitude();
case 'Cessna':
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
}
// GOOD
class Airplane {
// ...
}
class Boeing777 extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getPassengerCount();
}
}
class AirForceOne extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude();
}
}
class Cessna extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
- 타입-체킹을 피해라 - 1
- 자바스크립트는 타입이 정해져있지 않다. 함수가 어떤 타입의 인자든 받을 수 있다는 것을 의미한다.
- 때문에 함수에 타입-체킹을 시도 할 수 있다. 타입 체킹에는 많은 방법들이 존재한다.
- 첫번째 방법은 일관성 있는 API를 사용하는 것 입니다.
// BAD
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(this.currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(this.currentLocation, new Location('texas'));
}
}
// GOOD
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location('texas'));
}
- 타입-체킹을 피해라 - 2
- 기본 자료형을 사용하고 다형성을 사용할 수 없을때에도 여전히 타입-체킹이 필요하다고 느껴지면 타입스크립트를 사용해라
// BAD
function combine(val1, val2) {
if (typeof val1 === 'number' && typeof val2 === 'number' ||
typeof val1 === 'string' && typeof val2 === 'string') {
return val1 + val2;
}
throw new Error('Must be of type String or Number');
}
// GOOD
function combine(val1, val2) {
return val1 + val2;
}
- 과도한 최적화를 지양해라.
- 최신 브라우저들은 런타임시에 많은 최적화 작업을 수행한다. 때문에 대부분 코드 최적화에 들이는 시간들이 낭비가 될 수 있다.
- 최적화가 부족한 곳을 알려주는 레퍼런스를 찾아 부족한 부분만 최적화 하자
// 오래된 브라우저의 경우 캐시되지 않은 `list.length`를 통한 반복문은 높은 코스트를 가졌습니다.
// 그 이유는 `list.length`를 매번 계산해야만 했기 때문인데, 최신 브라우저에서는 이것이 최적화 되었습니다.
for (let i = 0, len = list.length; i < len; i++) {
// ...
}
- 죽은 코드를 지워라
- 죽은 코드는 중복된 코드 만큼이나 좋지 않다.
- 호출되지 않는 코드가 있다면 지워라.
- 코드가 필요하면 버전 히스토리에서 찾아라
// BAD
function oldRequestModule(url) {
// ...
}
function newRequestModule(url) {
// ...
}
// GOOD
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');