더 좋은 개발자가 되기 위한 발돋움으로 SOPT의 웹파트에서 진행하는 Clean Code 스터디에 들어가게 되었다! ✨
나는 심지어 JavaScript 언어만 겨우 아는 프론트 초짜이다. 본격적으로 React를 공부하면서 clean code 스터디와 병행한다면 좀 더 나은 코드를 짤 수 있을 것 같다.
참고한 자료는 Robert C. Martin의 저서인 Clean Code를 JavaScript에 맞추어 정리된 자료이다. 무료이니 자바스크립트로 클린코드를 공부하는 사람들에게는 도움이 될 것이라고 생각한다: 링크
개발자라면 모름지기 clean code를 짜야한다는 것은 개발자를 꿈꾸는 사람, 혹은 개발 직군에서 이미 일해본 사람이라면 다 알 것이다. 깨끗하고 가독성이 좋은 코드를 짜는 것은 나 자신뿐만 아니라 함께 일하는 개발자 동료들을 위한 배려이다.
다른 사람들의 코드를 읽을 때 'WTF'이 나오지 않는 코드를 보고 클린코드라고도 할 수 있다.
'WTF is that...?'
'WTF did you do here...?'
우리는 우리 뿐만이 아니고 다른 사람이 훗날 우리의 코드를 읽을 때 'WTF'이 나오지 않는 코드를 작성해야한다.
이런 좋은 코드는 3R원칙을 지킨다: Refactorability, Reusability, and Readability
즉, 리팩토링 가능성, 재사용성, 가독성이다. 내 코드가 이 세가지 원칙을 잘 지키는지 생각하며 코드를 쓰는 것이 곧 clean code를 작성하는 것이다.
변수명이 길어도 괜찮다. 동료와 훗날의 나 자신을 위해 변수의 이름을 명확하게 하자.
//좋지 않은 예
cosnt ddmmyyyy = new Date();
//좋은 예
const date = new Date();
중요한 점은 우리가 쓰는 코드는 읽기 쉽고, 찾기 쉬운 변수명을 사용해야한다는 것이다. 변수명은 결국 우리가 쓰는 코드를 쉽게 설명하고 그 코드의 내용이 어떠한지 알려주는 역할을 해야한다. 그 코드를 처음보는 사람도 쉽게 이해할 수 있는 방식으로 코드를 짜야한다. 동사를 사용하여 변수명을 짜는 것이 좋다. 예를 들어, Boolean인 변수라면 is
로 시작하는 변수명을 정하는 것처럼 말이다.
// 좋지 않은 예
function paintCar(car) {
car.carColor = "Red";
}
// 좋은 예
function paintCar(car) {
car.Color = "Red";
}
Functions should do one thing. They should do it well. They should do it only. — Robert C. Martin (Uncle Bob)
함수의 인자 (Function arguments)의 개수를 줄이는 것은 굉장히 중요하다. 함수를 테스트하는 것을 훨씬 쉽게 만들어주기 때문이다. 대체적으로 1개, 2개의 인자만 있어도 충분하다. 만약 3개 이상의 인자를 받는다면 그 함수는 너무 많은 것을 하고 있는 것이다.
함수가 기대하는 속성을 더 명확하기 위해서는 자바스크립트가 제공하는 ES2015/ES6의 비구조화 구문을 사용하는 것이 좋다.
비구조화 할당은 배열(array) 또는 객체(object)의 속성을 해체하여 그 값을 개별 변수(variable)에 담을 수 있게 해주는 자바스크립트의 표현식이다.
이를 사용하는 것에는 다음과 같은 장점이 있다.
- 어떤 사람이 함수의 signature(인자의 타입, 반환되는 타입)을 봤을 때, 어떤 속성이 사용되는지 바로 알 수 있다.
- 비구조화는 인자로 넘겨지는 객체의 원래 값들을 미리 복제해줄 수 있고, 이는 원하지 않는 결과(side effect)가 일어나는 것을 방지한다.
- Linter를 사용한다면 불필요한 속성/인자에 대한 경고를 해줄 수 있다. 이는 비구조화 없이는 불가능하다.
// 좋지 않은 예
function createMenu(title, body, buttonText, cancellable) {
...
}
createMenu("Foo", "Bar", "Baz", true);
// 좋은 예
function createMenu({ title, body, buttonText, cancellable }) {
...
}
createMenu({
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
});
함수의 역할은 단 한개여야한다! 이는 개발에서 가장 중요한 개념이기도 하다. 함수가 한 가지의 역할보다 더 많은 것을 한다면 코드를 짜는 것, 테스트하는 것, 그리고 로직을 이해하는 것 모두가 어려워진다. 반면에 모든 함수가 단 한가지의 역할만 하도록 함수의 역할을 잘 구분해둔다면, 우리의 코드는 가독성도 높아지고, 더 좋은 코드가 되기 위한 리팩토링 과정도 더 쉽게 거칠 수 있다.
// 좋지 않은 예
function emailClients(clients) {
clients.forEach(client => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
- 한 함수에서 active한 client를 찾는 역할, 그리고 그 client에게 이메일하는 역할,
총 두가지를 다 담당하고 있다.
// 좋은 예
function emailActiveClients(clients) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
- 위의 emailClients 함수의 두가지 역할을 각기 다른 함수 두개가 담당하도록 하였다.
마찬가지로 추상화된 함수의 이름이 여러가지를 내포하고 있다면 그 또한 각기 다른 역할별로 분리하여야한다.
같은 로직을 가지고 있는 코드가 있다면 그 코드에 문제가 생겼을 때 고쳐야하는 부분이 여러 군데라는 것을 의미한다. 이는 유지보수 측면에서 매우 좋지 않은 코드이다. 하나의 함수/모듈/클래스 등을 이용하여 최대한 추상화를 하는 것이 좋다.
다만 잘 추상화하지 못한 코드는 안 하느니만 못한 코드가 될 수 있으므로 위에서 언급한 클린 코드의 특징을 따를 수 있도록 해야한다.
지금까지 함수는 한개의 역할만 해야한다고 설명했었다. 하지만 함수의 인자, 매개변수가 boolean인 경우에는 함수가 두가지 이상의 역할을 한다는 것을 암시한다. 아래의 예시를 보자.
// ❌ Avoid using boolean flags in functions
distance(pointA, pointB, true)
// ✅ Instead, split the logic in two separate functions
distanceInKilometers(pointA, pointB)
distanceInMiles(pointA, pointB)
심지어 이 코드는 가독성도 좋지 않다.
distance(pointA, pointB, true)
대체 무엇이 true인 것인가? 이것을 알아내기 위해서는 안 그래도 복잡한 코드를 더 뜯어봐야한다. 만약 단순히 true
가 아니고 isKilometers=true
여도 여전히 이해하기 어렵다. 만약 isKilometers=false
면 어떡할 것인가...?
아래의 코드를 보자. 전역 변수를 사용하고 있다. 어떤 함수에서 그 전역 변수를 쓰게 된다면 그 변수가 의도하지 않았던 형태로 바뀌는 side effect가 일어날 수 있다.
// 좋지 않은 예
let name = 'Ryan McDermott';
function splitIntoFirstAndLastName() {
name = name.split(' ');
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];
// 좋은 예
function splitIntoFirstAndLastName(name) {
return name.split(' ');
}
const name = 'Ryan McDermott';
const newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];
전역 객체는 JavaScript에서 거의 무조건 좋지 않은 practice이다. 외부 library와 충돌할 수도 있기 때문이다. 이미 존재하는 객체를 extend해야한다면 이미 존재하는 객체의 prototype 체인에 함수를 만들어주는 대신, ES 클래스와 상속을 이용하자.
// 좋지 않은 예:
Array.prototype.myFunc = function myFunc() {
...
};
// 좋은 예:
class SuperArray extends Array {
myFunc() {
...
}
}
아래의 코드에서 나쁜 예를 보자. 조건 문이 길게 늘어져있다. 이 조건문을 좋은 예처럼 shouldShowSpinner라는 함수로 캡슐화를 하면 가독성이 더 좋아진 것을 볼 수 있다.
// 나쁜 예
if (fsm.state === "fetching" && isEmpty(listNode)) {
// ...
}
// 좋은 예
function shouldShowSpinner(fsm, listNode) {
return fsm.state === "fetching" && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
함수 안에서 조건문을 쓴다는 것 자체가 함수가 여러개의 역할을 가지고 있다는 것을 암시한다. 그래서 이를 반드시 분리하여야한다.
// 나쁜 예
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();
}
}
}
// 좋은 예
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();
}
}
JavaScript는 타입이 정해져 있지 않아 어떤 타입의 인자던 받을 수 있다. 하지만 이런 자유도 때문에 우리는 우리가 원하는 타입의 인자가 함수에 사용되는 것을 확인하기 위해 타입 체크가 필요하다고 생각한다.
그럼에도 불구하고 타입 체킹은 권장되지 않는 사항이다.
그 이유는 자바스크립트의 타입 체킹이 가진 함정이 너무 많기 때문이다.
이 블로그글에서 자바스크립트의 타입 체킹 방법인 typeof
와 instanceof
의 괴상한 현상...을 확인할 수 있다.
예를 들어,
null
,array
를typeof
을 사용하여 타입체크를 해보면 둘다object
타입이라는 것을 알 수 있다.
(array
가 array
로 나오지 않는다면 타입체킹을 그럼 왜 하는거야...?)
NaN
은number
타입이다. 하지만 아래와 같은 말도 안되는 수학을 사용한다면?5 * undefined; // => NaN
(5*undefined가 어떻게 NaN, 즉
number
타입이란말인가...)
이렇게 자바스크립트의 타입 체킹이 믿음직스럽지 않은 구석이 많다는 것을 알 수 있다. 타입 체크는 다음과 같은 방법으로 피할 수 있다.
- 일관성 있는 API를 사용하는 것
- TypeScript를 사용하는 것