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만 보면 된다. 규칙이 늘어도 객체에 한 줄만 추가하면 된다.