← 글 목록

notes

How To Write Better Code

읽기 쉽고 유지보수 가능한 코드를 위한 설계·작성 원칙 메모.


같은 기능을 처음 짠 코드, 한 번 고친 코드, 다시 다듬은 코드 세 단계로 나눠 비교해 본 메모다. 내 코드를 리뷰할 때 여기 네 가지를 제일 먼저 본다.

  • 분기는 early return으로 줄이고, 반환 타입과 이름을 분명하게 둔다.
  • 숫자는 상수로 빼고, 옵션 기본값은 구조 분해로 받는다.
  • 콜백은 async/await로 펴고, 큰 함수는 검사와 저장처럼 역할별로 쪼갠다.

1. Logic (논리)

  • 권장하지 않는 코드
function toAccounting(n) {
	if (n < 0) {
		return '(' + Math.abs(n) + ')'
	} else if (n >= 0) {
		return n
	}
}

  • 개선된 코드
function numberToAccountingString(number) {
	if (number != null) {
		if (number < 0) {
			return `(${Math.abs(number)})`
		} else {
			return number.toString()
		}
	}
}
  • 조건이 나머지 전부라면 else if 대신 else로 끝낸다.
  • 한쪽은 문자열, 한쪽은 숫자를 돌려주던 걸 문자열로 맞췄다.
  • toAccounting(n)보다 numberToAccountingString(number)가 무엇을 받아 뭘 돌려주는지 바로 보인다.
  • 문자열 조립은 더하기 대신 템플릿 리터럴로 바꿨다.

  • 전문가의 코드
function numberToAccountingString(number) {
	if (number == null) return ''
	if (number < 0) return `(${Math.abs(number)})`
	return number.toString()
}
  • 예외 상황을 먼저 return으로 내보내면 들여쓰기와 괄호가 한 단계로 줄어든다.
  • 위에서 아래로 한 번만 읽어도 흐름이 보여서 고칠 때도 편하다.

2. Variable-manipulation (변수 사용)

  • 권장하지 않는 코드
function calculateTotal(items, options) {
	let t = 0
	items.forEach(i => {
		t += i.price * i.quan
	})
	t = t - t * (options.dis || 0)
	t = t * 1.1
	t = t + (options.ship || 5)
	return t
}

const testItems = [
	{ price: 15, quan: 2},
	{ price: 20, quan: 1},
	{ price: 5, quan: 4}
]

console.log(calculateTotal()) // error
console.log(calculateTotal(testItems)) // error
console.log(calculateTotal(undefined, {})) // error
console.log(calculateTotal([], {})) // 5
console.log(calculateTotal(testItems, {})) // 82
console.log(calculateTotal(testItems, { shipping: 0 })) // 77
console.log(calculateTotal(testItems, { discount: 0.75 })) // 24.25
console.log(calculateTotal(testItems, { shipping: 12 })) // 89
  • t, i, quan, dis만 봐서는 뭘 담는지 모른다.
  • 1.1, 5가 세금인지 배송비인지 코드만 봐서는 알 수 없다.
  • 인자가 빠지면 바로 에러가 난다. 옵션 이름(shipping, discount)도 함수 안 이름과 달라서 무시된다.

  • 개선된 코드
const TAX_RATE = 1.1
const SHIPPING_DEFAULT = 5

function calculateTotal(items, options = {}) {
	if (items == null || items.length === 0) return 0

	let total = 0
	items.forEach(item => {
	total += item.price * item.quantity
	})
	total = total - total * (options.discount || 0)
	total = total * TAX_RATE
	if (options.shipping !== 0) {
		total = total + (options.shipping || SHIPPING_DEFAULT)
	}
	return total
}

const testItems = [
	{ price: 15, quantity: 2},
	{ price: 20, quantity: 1},
	{ price: 5, quantity: 4}
]

console.log(calculateTotal()) // 0
console.log(calculateTotal(testItems)) // 82
console.log(calculateTotal(undefined, {})) // 0
console.log(calculateTotal([], {})) // 0
console.log(calculateTotal(testItems, {})) // 82
console.log(calculateTotal(testItems, { shipping: 0 })) // 77
console.log(calculateTotal(testItems, { discount: 0.75 })) // 24.25
console.log(calculateTotal(testItems, { shipping: 12 })) // 89
  • 변수 이름만 봐도 무엇을 계산하는지 보인다.
  • 세금과 기본 배송비를 상수로 빼서 의미가 드러난다.
  • 빈 배열이나 옵션이 없을 때도 0을 돌려주게 막아 뒀다.

  • 전문가의 코드
const TAX_RATE = 1.1
const SHIPPING_DEFAULT = 5

function calculateTotal(items, { shipping = SHIPPING_DEFAULT, discount = 0 } = {}) {
	if (items == null || items.length === 0) return 0

	const itemCost = items.reduce((total, item) => {
		return total + item.price * item.quantity
	}, 0)
	const discountRate = 1 - discount

	return itemCost * discountRate * TAX_RATE + shipping
}

const testItems = [
	{ price: 15, quantity: 2},
	{ price: 20, quantity: 1},
	{ price: 5, quantity: 4}
]
  • 기본값을 함수 인자에서 구조 분해로 받아서, 옵션 이름과 기본값이 한 줄에 보인다.
  • 합계는 forEach에 변수를 누적하는 대신 reduce로 한 번에 구한다.
  • 중간 값에 이름(itemCost, discountRate)을 붙이니 마지막 줄이 계산식처럼 읽힌다.

3. async (비동기)

  • 권장하지 않는 코드
const readline = require('readline')
const readlineInterface = readline.createInterface({
	input: process.stdin,
	output: process.stdout
})

readlineInterface.question("What is your name? ", name => {
	readlineInterface.question("What is your job? ", job => {
		readlineInterface.question("How old are you? ", age => {
			console.log("Hello " + name + ". You are a " + age + " year old " + job + ".")
			readlineInterface.close()
		})
	})
})
  • 질문이 하나 늘 때마다 들여쓰기가 한 칸씩 깊어진다.

  • 개선된 코드
const readline = require('readline')
const readlineInterface = readline.createInterface({
	input: process.stdin,
	output: process.stdout
})

async function main() {
	const name = await askQuestion(readlineInterface, "What is your name? ")
	const job = await askQuestion(readlineInterface, "What is your job? ")
	const age = await askQuestion(readlineInterface, "How old are you? ")

	console.log(`Hello ${name}. You are a ${age} year old ${job}.`)
	readlineInterface.close()
}
main()

function askQuestion(readlineInterface, question) {
	return new Promise(resolve => {
		readlineInterface.question(question, answer => {
			resolve(answer)
		})
	})
}
  • 코드 자체는 오히려 길어졌다.
  • 대신 async/await 덕에 질문 순서가 위에서 아래로 그대로 읽힌다.

  • 전문가의 코드
// askQuestion.js

const readline = require('readline')

function askQuestion(question) {
	const readlineInterface = readline.createInterface({
		input: process.stdin,
		output: process.stdout
	})
	return new Promise(resolve => {
		readlineInterface.question(question, answer => {
			resolve(answer)
			readlineInterface.close()
		})
	})
}

module.exports = askQuestion
const askQuestion = require('./pro/askQuestion')

async function main() {
	const name = await askQuestion("What is your name? ")
	const job = await askQuestion("What is your job? ")
	const age = await askQuestion("How old are you? ")

	console.log(`Hello ${name}. You are a ${age} year old ${job}.`)
}
main()
  • readline 생성과 정리를 askQuestion.js 안으로 숨겨서, 호출하는 쪽은 질문 문자열만 넘긴다.

4. fat-functions

  • 권장하지 않는 코드
const { updateUser, createUser } = require('./api/users')

function saveUser(user) {
	const errors = []
	if (user.username) {
		if (user.username.length < 3) {
			errors.push("Username must be 3 or more characters")
		}
	} else {
		errors.push("Username is required")
	}
	
	if (user.password) {
		if (user.password.length < 8) {
			errors.push("Password must be 8 or more characters")
		}
	} else {
		errors.push("Password is required")
  }

	if (errors.length > 0) {
		errors.forEach(error => console.error(error))
		return
	}

	if (user.id == null) {
		console.log('Created User')
		createUser(user)
	} else {
		console.log('Updated User')
		updateUser(user)
	}
}

const user = {
	username: 'WDS',
	password: 'password'
}
  • 검사, 에러 출력, 생성과 수정까지 함수 하나가 다 한다.
  • if와 else가 격쳐서 어느 조건이 어느 에러로 이어지는지 따라가기 힘들다.

  • 개선된 코드
const { updateUser, createUser } = require('./api/users')

function saveUser(user) {
	if (user.id == null) {
		console.log('Created User')
		createUser(user)
	} else {
		console.log('Updated User')
		updateUser(user)
	}
}

function validateUser(user) {
	return [
		...validateUsername(user.username),
		...validatePassword(user.password)
	]
}

function validateUsername(username) {
	const errors = []
	if (!username) errors.push('Username is required')
	if (username != null && username.length < 3) errors.push('Username must be 3 or more characters')
	return errors
}

function validatePassword(password) {
	const errors = []
	if (!password) errors.push('Password is required')
	if (password != null && password.length < 8) errors.push('Password must be 8 or more characters')
	return errors
}

const user = {
	username: null,
	password: ''
}

const errors = validateUser(user)
if (errors.length > 0) {
	errors.forEach(error => console.error(error))
	return
}

saveUser(user)
  • 저장(saveUser)과 검사(validateUser)를 나누고, 검사도 필드별 함수로 쪼갰다.

  • 전문가의 코드
const { updateUser, createUser } = require('./api/users')
const { validationMessages, printErrors } = require('./pro/validation')

function saveUser(user) {
	if (user.id == null) {
		console.log('Created User')
		createUser(user)
	} else {
		console.log('Updated User')
		updateUser(user)
	}
}

function validateUser(user) {
	const validations = {
		username: {
			required: true,
			length: 3
		},
		password: {
			required: true,
			length: 8
		}
	}

	const errors = validationMessages(validations, user)

	return {
		valid: Object.values(errors).every(messages => messages.length === 0),
		errors: errors
	}
}

const user = {
	id: 1,
	username: '',
	password: 'password'
}


const {errors, valid} = validateUser(user)
if (valid) {
	saveUser(user)
} else {
	printErrors(errors)
}
  • 검사 규칙을 객체로 적어 두고, 메시지 만드는 로직은 validation 모듈로 뻨다.
  • 그 모듈 안은 복잡해도, 호출하는 쪽은 valid와 errors만 보면 된다. 규칙이 늘어도 객체에 한 줄만 추가하면 된다.