이렇게 하지 마라(Bad) vs. 이렇게 해라(Good) 버전의 쉬운 예제들로 정리된 클린 코드 예제이다.
예제들을 쭉 보면서 내가 자주 하는 실수만 따로 모아서 정리해 본다.
1. 변수
단락 평가(short circuiting)나 조건부 대신 default arguments 사용
Bad :
function loadPages(count?: number) {
const loadCount = count !== undefined ? count : 10;
// ...
}
Good :
function loadPages(count: number = 10) {
// ...
}
2. 함수
함수의 인자 (2개 또는 그보다 적을 수록 이상적)
인자의 개수는 1개나 2개가 이상적이며, 가능하다면 인자가 3개가 되지 않게 하는 게 좋다. 인자가 2개보다 많다면 보통은 함수가 너무 많은 일을 하고 있는 것이다. 그렇지 않은 경우 구조분해할당을 사용하는 게 좋다.
Bad :
function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) {
// ...
}
createMenu('Foo', 'Bar', 'Baz', true);
Good :
type MenuOptions = { title: string, body: string, buttonText: string, cancellable: boolean };
function createMenu(options: MenuOptions) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
함수는 한 가지 일만 해야 한다!
Bad :
function emailActiveClients(clients: Client[]) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
Good :
function emailActiveClients(clients: Client[]) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client: Client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
중복 코드 제거
기본 원칙 중의 기본 원칙이지만 예제가 훌륭해서 한 번 더 기록해둔다.
Bad :
function showDeveloperList(developers: Developer[]) {
developers.forEach((developer) => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};
render(data);
});
}
function showManagerList(managers: Manager[]) {
managers.forEach((manager) => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
Good :
class Developer {
// ...
getExtraDetails() {
return {
githubLink: this.githubLink,
}
}
}
class Manager {
// ...
getExtraDetails() {
return {
portfolio: this.portfolio,
}
}
}
type Employee = Developer | Manager;
function showEmployeeList(employee: Employee[]) {
employee.forEach((employee) => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const extra = employee.getExtraDetails();
const data = {
expectedSalary,
experience,
extra,
};
render(data);
});
}
Object.assign이나 spread operator(...)를 사용해 default 객체를 세팅
Bad :
type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };
function createMenu(config: MenuConfig) {
config.title = config.title || 'Foo';
config.body = config.body || 'Bar';
config.buttonText = config.buttonText || 'Baz';
config.cancellable = config.cancellable !== undefined ? config.cancellable : true;
// ...
}
createMenu({ body: 'Bar' });
Good :
// Object.assign
type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };
function createMenu(config: MenuConfig) {
const menuConfig = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);
// ...
}
createMenu({ body: 'Bar' });
// spread operator
function createMenu(config: MenuConfig) {
const menuConfig = {
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true,
...config,
};
// ...
}
flag 변수를 함수의 파라미터로 사용하지 말 것!
함수의 파라미터에 flag가 있다는 것은 함수가 하나의 일보다 많은 일을 하고 있다는 증거다. 함수는 한 가지 일만 해야 한다. 함수가 boolean을 기반으로 서로 다른 코드 경로를 따르고 있다면 함수를 분리해라.
Bad :
function createFile(name: string, temp: boolean) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
Good :
function createTempFile(name: string) {
createFile(`./temp/${name}`);
}
function createFile(name: string) {
fs.create(name);
}
Side Effect 피하기 #1
함수가 값을 받아 다른 값을 반환하는 것 이외의 다른 작업을 수행하면 부작용이 발생한다. 요점은 구조 없이 객체 간에 상태를 공유하고, 무엇이든 쓸 수 있는 변경 가능한 데이터 유형을 사용하며, 부작용이 발생하는 위치를 중앙 집중화하지 않는 것과 같은 일반적인 함정을 피하는 것이다.
Bad :
// Global variable referenced by following function.
let name = 'Robert C. Martin';
function toBase64() {
name = btoa(name);
}
toBase64();
// If we had another function that used this name, now it'd be a Base64 value
console.log(name); // expected to print 'Robert C. Martin' but instead 'Um9iZXJ0IEMuIE1hcnRpbg=='
Good :
const name = 'Robert C. Martin';
function toBase64(text: string): string {
return btoa(text);
}
const encodedName = toBase64(name);
console.log(name);
Side Effect 피하기 #2
브라우저와 Node.js는 자바스크립트만 처리하므로 실행 또는 디버깅하기 전에 모든 타입스크립트 코드를 컴파일해야 한다. 자바스크립트에서는 변경할 수 없는 값(immutable)과 변경 가능한 값(mutable)이 있다. 객체와 배열은 변경 가능한 값의 두 가지 종류이므로 함수에 매개변수로 전달할 때 신중하게 처리하는 것이 중요하다. 자바스크립트 함수는 객체의 속성을 변경하거나 배열의 내용을 변경할 수 있으므로 다른 곳에서 쉽게 버그를 일으킬 수 있다.
장바구니를 나타내는 배열을 매개변수로 받는 함수가 있다고 가정해 보자. 이 함수가 구매 품목을 추가하는 등 장바구니 배열을 변경하면 동일한 장바구니 배열을 사용하는 다른 모든 함수가 이 추가 사항의 영향을 받게 된다. 예를 들어 사용자가 '구매' 버튼을 클릭하면 네트워크 요청을 생성하고 장바구니 배열을 서버로 전송하는 구매 함수가 호출된다. 네트워크 연결 상태가 좋지 않기 때문에 구매 함수는 요청을 계속 재시도해야 한다. 그렇다면 네트워크 요청이 시작되기 전에 사용자가 실제로는 원하지 않는 품목의 '장바구니에 추가' 버튼을 실수로 클릭하면 어떻게 될까? 이 경우 네트워크 요청이 시작되면 장바구니 배열이 수정되었으므로 해당 구매 기능이 실수로 추가된 품목을 전송한다.
가장 좋은 해결책은 addItemToCart 함수가 항상 장바구니 배열을 복제하고 편집한 다음 복제본을 반환하는 것이다. 이렇게 하면 여전히 이전 장바구니를 사용하는 함수가 변경 사항의 영향을 받지 않을 수 있다.
이 접근 방식에 대해 두 가지 주의할 점이 있다.
1. 실제로 입력 객체를 수정하고 싶은 경우가 있을 수 있지만, 이 프로그래밍 방식을 채택하면 그런 경우는 매우 드물다는 것을 알게 될 것다. 대부분이의 경우 부작용 없이 리팩터링할 수 있다! (순수 함수 참조)
2. 큰 객체를 복제하는 것은 성능 측면에서 매우 비용이 많이 들 수 있다. 다행히도 이런 종류의 프로그래밍 방식을 빠르게 구현하고 객체와 배열을 수동으로 복제할 때보다 메모리 집약적이지 않은 훌륭한 라이브러리가 있기 때문에 실제로는 큰 문제가 되지 않는다.
Bad :
function addItemToCart(cart: CartItem[], item: Item): void {
cart.push({ item, date: Date.now() });
};
Good :
function addItemToCart(cart: CartItem[], item: Item): CartItem[] {
return [...cart, { item, date: Date.now() }];
};
조건문을 캡슐화하기
Bad :
if (subscription.isTrial || account.balance > 0) {
// ...
}
Good :
function canActivateService(subscription: Subscription, account: Account) {
return subscription.isTrial || account.balance > 0;
}
if (canActivateService(subscription, account)) {
// ...
}
조건문을 피하기
불가능한 작업처럼 보일 수 있다. 처음 이 말을 들었을 때 대부분의 사람들은 "if문 없이 어떻게 하죠?"라고 말한다. 다형성을 사용하면 많은 경우에 동일한 작업을 수행할 수 있다는 것이 첫 번째 질문에 대한 답변이다. 그 다음으로는 "좋긴 한데, 왜 그렇게 해야 하나요?"라고 두 번째 질문을 한다. 이 질문에 대한 답은 앞서 배운 클린 코드 개념인 함수는 한 가지 일만 해야 한다는 것이다. 만약 if문이 있는 클래스와 함수가 있다면, 함수가 한 가지 이상의 작업을 수행한다고 뜻이다. '한 가지 일만 해야 한다'는 것을 명심하라.
Bad :
class Airplane {
private type: string;
// ...
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();
default:
throw new Error('Unknown airplane type.');
}
}
private getMaxAltitude(): number {
// ...
}
}
Good :
abstract class Airplane {
protected getMaxAltitude(): number {
// shared logic with subclasses ...
}
// ...
}
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();
}
}
타입 검사 피하기
Bad :
function travelToTexas(vehicle: Bicycle | Car) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(currentLocation, new Location('texas'));
}
}
Good :
type Vehicle = Bicycle | Car;
function travelToTexas(vehicle: Vehicle) {
vehicle.move(currentLocation, new Location('texas'));
}
'스크랩' 카테고리의 다른 글
시니어 없이 주니어끼리 일하는 방법 (0) | 2024.01.21 |
---|---|
좋은 질문을 하는 법 (0) | 2023.01.11 |
[알고리즘] 하노이의 탑 - 재귀 (1) | 2022.12.24 |
클래스 기반 언어 vs. 프로토타입 기반 언어 (0) | 2022.12.06 |
불변성 Immutable 이란 무엇인가 (0) | 2022.12.05 |