
대상 함수를 호출하는 함수는 무엇인지, 대상 함수가 호출하는 함수들은 또 무엇이 있는지, 대상 함수가 사용하는 데이터는 무엇인지를 잘 살펴보고 잘 맞지 않다고 판단되면 함수의 위치를 옮겨주도록 한다.
📚 중첩 함수를 최상위로 옮기기
다음은 GPS 추적 기록의 총 거리를 계산하는 함수이다.
const trackSummary = (points) => {
const radians = (degrees) => { // 라디안 값으로 변환 };
const distance = (p1, p2) => { // 두 지점의 거리 계산 };
const calculateDistance = () => {
let result = 0;
for (let i = 1; i < points.length; i++) {
result += distance(points[i - 1], points[i]);
}
return result;
};
const totalTime = 10000;
const totalDistance = calculateDistance();
const pace = totalTime / 60 / totalDistance;
return {
time: totalTime,
distance: totalDistance,
pace,
};
};
중첩 함수를 사용하다보면 숨겨진 데이터끼리 상호 의존하기가 쉽다. 중첩 함수를 최상위로 옮겨, 중첩 함수를 제거하자.
먼저, calculateDistance 함수를 최상위로 옮기자. calculateDistance 함수는 distance 와 points 를 사용하고 있다. points를 매개변수로 넘긴다.
const trackSummary = (points) => {
const radians = (degrees) => { ... };
const distance = (p1, p2) => { ... };
const totalTime = 10000;
const totalDistance = calculateDistance(points);
const pace = totalTime / 60 / totalDistance;
return {
time: totalTime,
distance: totalDistance,
pace,
};
};
// 함수 이동
const calculateDistance = (points) => {
let result = 0;
for (let i = 1; i < points.length; i++) {
result += distance(points[i - 1], points[i]);
}
return result;
};
다음은 distance 함수와 distance 함수가 의존하는 코드를 이동시키자.
const trackSummary = (points) => {
const totalTime = 10000;
const totalDistance = calculateDistance(points);
const pace = totalTime / 60 / totalDistance;
return {
time: totalTime,
distance: totalDistance,
pace,
};
};
// 함수 이동
const radians = (degrees) => { ... };
const distance = (p1, p2) => { ... };
const calculateDistance = (points) => {
let result = 0;
for (let i = 1; i < points.length; i++) {
result += distance(points[i - 1], points[i]);
}
return result;
};
calculateDistance 함수를 함수 의미에 맞게 변경하고, totalDistance 변수를 남겨둘 이유가 없으니 인라인해 제거하자.
const trackSummary = (points) => {
const totalTime = 10000;
const pace = totalTime / 60 / totalDistance(points); // 이름 변경
return {
time: totalTime,
distance: totalDistance(points), // 변수 인라인
pace,
};
};
const radians = (degrees) => { ... };
const distance = (p1, p2) => { ... };
const totalDistance = (points) => {
let result = 0;
for (let i = 1; i < points.length; i++) {
result += distance(points[i - 1], points[i]);
}
return result;
};
📚 다른 클래스로 옮기기
다음은 초과 인출 이자를 계산하는 Account 클래스다. 계좌 종류에 따라 이자 책정 알고리즘이 달라지도록 고쳐보자.
class Account {
constructor(accountType, daysOverdrawn) {
this.type = accountType;
this._daysOverdrawn = daysOverdrawn;
}
get overdraftCharge() {
// 초과 인출 이자 계산
if (this.type.isPremium) {
const baseCharge = 10;
if (this.daysOverdrawn <= 7) {
return baseCharge;
} else {
return baseCharge + (this.daysOverdrawn - 7) * 0.85;
}
} else {
return this.daysOverdrawn * 1.75;
}
}
}
계좌 종류 클래스 AccountType 을 새로 생성해, 마이너스 통장의 초과 인출 이자를 계산하는 overdraftCharge 함수를 옮겨보자.
isPremium은 this를 통해 호출하고, 옮긴 overdraftCharge 함수에 daysOverdrawn를 매개변수로 넣었다.
// 새 클래스 생성
class AccountType {
constructor(type) {
this._type = type;
}
get isPremium() {
return this._type === "Premium";
}
overdraftCharge(daysOverdrawn) {
if (this.isPremium) {
const baseCharge = 10;
if (daysOverdrawn <= 7) {
return baseCharge;
} else {
return baseCharge + (daysOverdrawn - 7) * 0.85;
}
} else {
return daysOverdrawn * 1.75;
}
}
}
이제 AccountType 클래스에 맞춰 Account 클래스를 변경한다.
Account 클래스의 overdraftCharge 게터에, AccountType 클래스에서 작성한 overdraftCharge 메서드를 호출하도록 한다.
class Account {
// ...
get overdraftCharge() {
// 위임 메서드
return this.type.overdraftCharge(this.daysOverdrawn);
}
// ...
}

한 레코드를 변경하려 할 때 다른 레코드의 필드까지 변경해야만 한다면,
구조체 여러 개의 정의된 똑같은 필드들을 갱신해야 한다면,
다른 위치로 필드를 옮기라는 신호다.
📜 절차
다음은 Customer 와 CustomerContract 클래스이다.
class Customer {
constructor(name, discountRate) {
this._name = name;
this._discountRate = discountRate;
this._contract = new CustomerContract(new Date());
}
get discountRate() {
return this._discountRate;
}
becomePreferred() {
this._discountRate += 0.03;
// ...
}
}
class CustomerContract {
constructor(startDate) {
this._startDate = startDate;
}
}
discountRate 필드를 Customer에서 CustomerContract로 옮겨보자.
먼저 Customer 클래스에서 discountRate 필드를 캡슐화한다. 여기서는 세터 속성이 아니라 메서드를 이용했다.
class Customer {
constructor(name, discountRate) {
this._name = name;
this._setDiscountRate(discountRate); // 세터 호출로 변경
this._contract = new CustomerContract(this.dateToday());
}
// ...
// 세터 메서드 추가
_setDiscountRate(aNumber) {
this._discountRate = aNumber;
}
becomePreferred() {
this._setDiscountRate(this.discountRate + 0.03); // 세터 호출로 변경
}
// ...
}
그 다음, CustomerContract 클래스에서 discountRate 필드와 접근자들을 추가한다.
class CustomerContract {
constructor(startDate, discountRate) {
this._startDate = startDate;
this._discountRate = discountRate; // 필드 추가
}
// 접근자 추가
get discountRate() { return this._discountRate; }
set discountRate(value) { this._discountRate = value; }
}
Customer의 접근자들이 새로운 필드를 사용하도록 수정한다.
class Customer {
constructor(name, discountRate) {
this._name = name;
this._contract = new CustomerContract(this.dateToday(), discountRate);
}
get discountRate() {
// contract 필드 이용
return this._contract.discountRate;
}
_setDiscountRate(aNumber) {
// contract 필드 이용
this._contract.discountRate = aNumber;
}
becomePreferred() {
// contract 필드 이용
this._setDiscountRate(this._contract.discountRate + 0.03);
}
}

특정 함수를 호출하는 코드가 나올 때마다 그 앞이나 뒤에서 똑같은 코드가 추가로 실행되는 모습을 보면, 그 반복되는 부분을 피호출 함수로 합치도록 한다.
📜 절차
문장 슬라이드하기를 적용해 근처로 옮긴다.함수로 추출한다.인라인한 후 원래 함수를 제거한다.다음은 사진 관련 데이터를 HTML로 내보내는 코드다.
emitPhotoData 함수를 두 곳에서 호출하고, 두 곳 모두 바로 앞에는 title 출력 코드가 나온다.
const renderPerson = (person) => {
const result = [];
result.push(`<p>${person.name}</p>`);
result.push(renderPhoto(person.photo));
result.push(`<p>제목: ${person.photo.title}</p>`);
result.push(emitPhotoData(person.photo));
return result.join("\n");
};
const photoDiv = (p) => {
return ["<div>", `<p>제목: ${p.title}</p>`, emitPhotoData(p), "</div>"].join("\n");
};
const emitPhotoData = (aPhoto) => {
const result = [];
result.push(`<p>위치: ${aPhoto.location}</p>`);
result.push(`<p>날짜: ${aPhoto.date.toDateString()}</p>`);
return result.join("\n");
};
// ...
title 을 출력하는 코드를 emitPhotoData 함수 안으로 옮겨 중복을 없애자.
가장 먼저 함수를 추출한다. 추출한 함수를 사용하도록 바꾼다.
// 함수 추출
const zznew = (p) => {
return [`<p>제목: ${p.title}</p>`, emitPhotoData(p)].join("\n");
};
const renderPerson = (person) => {
const result = [];
result.push(`<p>${person.name}</p>`);
result.push(renderPhoto(person.photo));
result.push(zznew(person.photo)); // 새로운 함수 호출
return result.join("\n");
};
const photoDiv = (p) => {
// 새로운 함수 호출
return ["<div>", zznew(p), "</div>"].join("\n");
};
// ...
호출자들을 모두 수정했다면 emitPhotoData 함수를 인라인하여 제거한다.
const zznew = (p) => {
return [
`<p>제목: ${p.title}</p>`,
`<p>위치: ${p.location}</p>`,
`<p>날짜: ${p.date.toDateString()}</p>`
].join("\n");
};
그리고 함수 이름을 바꾸는 것으로 마무리를 한다.
const emitPhotoData = (aPhoto) => {
return [
`<p>제목: ${aPhoto.title}</p>`,
`<p>위치: ${aPhoto.location}</p>`,
`<p>날짜: ${aPhoto.date.toDateString()}</p>`
].join("\n");
};
const renderPerson = (person) => {
const result = [];
result.push(`<p>${person.name}</p>`);
result.push(renderPhoto(person.photo));
result.push(emitPhotoData(person.photo)); // 함수명 바꾸기
return result.join("\n");
};
const photoDiv = (aPhoto) => {
// 함수명 바꾸기
return ["<div>", emitPhotoData(aPhoto), "</div>"].join("\n");
};
// ...

* 반대 리펙터링: 문장을 함수로 옮기기
여러 곳에서 사용하던 기능이 일부 호출자에게는 다르게 동작하도록 바뀌어야 한다면
달라진 동작을 함수에서 꺼내 해당 호출자로 옮기도록 한다.
📜 절차

이미 존재하는 함수와 같은 일을 하는 인라인 코드를 발견하면, 함수 호출로 변경한다.

관련 있는 코드들은 한데 모여 있는 것이 좋고, 더 나아가 관련 있는 코드들을 명확히 구분되는 함수로서 추출하도록 한다.
📜 절차

여러 일을 수행하는 반복문이라면 서로 다른 일들이 한 함수에서 이뤄지고 있다는 신호일 수 있다.
리팩터링과 최적화를 구분하도록 한다.
최적화는 코드를 깔끔히 정리한 이후에 수행하도록 하며, 반복문을 두 번 실행하는 것이 병목이라 밝혀지게 되면 그때 다시 하나로 합치도록 한다.
📜 절차
전체 급여와 가장 어린 나이를 계산하는 코드를 보자.
반복문을 보면 관련 없는 두 가지 계산을 수행하고 있다. 반복문 쪼개기를 이용해 리팩터링을 해보자.
const reportYoungestAgeAndTotalSalary = (people) => {
let youngest = people[0] ? people[0].age : Infinity;
let totalSalary = 0;
for (const p of people) {
if (p.age < youngest) {
youngest = p.age;
}
totalSalary += p.salary;
}
return `최연소: ${youngest}, 총 급여: ${totalSalary}`;
};
반복문을 복제하고, 중복된 코드는 제거한다.
const reportYoungestAgeAndTotalSalary = (people) => {
let youngest = people[0] ? people[0].age : Infinity;
let totalSalary = 0;
// 반복문 쪼개기
for (const p of people) {
if (p.age < youngest) {
youngest = p.age;
}
}
// 반복문 쪼개기
for (const p of people) {
totalSalary += p.salary;
}
return `최연소: ${youngest}, 총 급여: ${totalSalary}`;
};
더 나아가, 나뉜 반복문을 각각의 함수로 추출하도록 한다.
const reportYoungestAgeAndTotalSalary = (people) => {
const youngestAge = () => {
return Math.min(...people.map((p) => p.age));
};
const totalSalary = () => {
return people.reduce((total, person) => total + person.salary, 0);
};
return `최연소: ${youngestAge()}, 총 급여: ${totalSalary()}`;
};
가장 어린 나이를 계산하는 반복문에는 알고리즘 교체하기 기법을 적용했고, 전체 급여를 계산하는 반복문에는 반복문을 파이프라인으로 바꾸기 기법을 적용하여 리팩터링을 완료한다.

논리를 파이프라인으로 표현하면 이해하기 훨씬 쉬워진다.
📜 절차
다음은 인도에 자리한 사무실을 찾아서 도시명과 전화번호를 반환한다. 이 코드의 반복문을 파이프라인으로 바꿔보자.
const acquireData = (input) => {
const lines = input.split("\n");
let firstLine = true;
const result = [];
for (const line of lines) {
// 첫 줄은 건너띈다.
if (firstLine) {
firstLine = false;
continue;
}
// 빈 줄은 지운다.
if (line.trim() === "") {
continue;
}
// 문자열 배열로 변환한다.
const record = line.split(",");
if (record[1].trim() === "India") {
result.push({
city: record[0].trim(),
phone: record[2].trim()
});
}
}
return result;
};
먼저, 반복문에서 사용하는 컬렉션을 가리키는 별도 변수 loopItems를 생성한다.
그 다음 반복문에서 첫 if문은 데이터의 첫 줄을 건너뛰는 코드 조각이다.
slice 연산을 loopItems 에서 수행하고, 반복문 안의 if문과 firstLine 변수는 제거한다.
const acquireData = (input) => {
const lines = input.split("\n");
const result = [];
// loopItem 변수 추가, slice 연산 추가
const loopItems = lines.slice(1);
for (const line of loopItems) {
if (line.trim() === "") {
continue;
}
const record = line.split(",");
if (record[1].trim() === "India") {
result.push({
city: record[0].trim(),
phone: record[2].trim()
});
}
}
return result;
};
다음 작업은 빈줄 지우기다. 이 작업은 filter 연산으로 대체할 수 있다.
const acquireData = (input) => {
const lines = input.split("\n");
const result = [];
const loopItems = lines
.slice(1)
.filter((line) => line.trim() !== ""); // filter 연산 추가
for (const line of loopItems) {
const record = line.split(",");
if (record[1].trim() === "India") {
result.push({
city: record[0].trim(),
phone: record[2].trim()
});
}
}
return result;
};
map 연산을 이용해 여러 줄짜리 데이터를 문자열 배열로 변환하고, filter 연산을 수행해 인도에 위치한 사무실 레코드를 뽑아낸다.
마지막으로 map 연산으로 결과 레코드를 생성한다.
const acquireData = (input) => {
const lines = input.split("\n");
const result = [];
const loopItems = lines
.slice(1)
.filter((line) => line.trim() !== "")
.map((line) => line.split(","))
.filter((lineItems) => lineItems[1].trim() === "India")
.map((lineItems) => ({
city: lineItems[0].trim(),
phone: lineItems[2].trim()
})); // map, filter 연산 추가
for (const line of loopItems) {
const record = line; // split 함수 제거
result.push(line); // 레코드 제거
}
return result;
};
이제 loopItems를 result에 대입해, 원래 반복문을 제거하여 리팩터링을 완료한다.
const acquireData = (input) => {
const lines = input.split("\n");
return lines
.splice(1)
.filter((line) => line.trim() !== "")
.map((line) => line.split(","))
.filter((lineItems) => lineItems[1].trim() === "India")
.map((lineItems) => ({
city: lineItems[0].trim(),
phone: lineItems[2].trim()
}));
};

코드가 더 이상 사용되지 않게 됐다면 지워야 한다.
사용되지 않는 코드가 있다면 그 소프트웨어의 동작을 이해하는 데는 커다란 걸림돌이 될 수 있다. 이 코드의 동작을 이해하기 위해, 그리고 코드를 수정했는데도 기대한 결과가 나오지 않는 이유를 파악하기 위해 시간을 허비하게 되기 때문이다.
다시 필요해질 날이 오지 않을까 걱정할 필요 없이 버전 관리 시스템을 이용하면 된다. 죽은 코드를 주석 처리하는 방법도 있지만 버전 관리 시스템을 사용하게 된 이상, 더 이상은 필요하지 않다.