해당 포스트는
쏙쏙 들어오는 함수형 코딩
을 학습하며 필요한 내용을 정리한 포스트입니다.
🌈 11. 일급 함수 2
이번 장에서 살펴볼 내용
함수 본문을 콜백으로 바꾸기 리팩터링 심화
함수를 리턴하는 함수가 가진 강력한 힘을 이해하기
고차 함수에 익숙해지기 위해 여러 고차 함수를 만들어보기
💻 코드 냄새 하나와 리팩터링 두 개
리팩터링으로 코드에 중복을 없애고 더 좋은 추상화를 만들었었다.
그 과정에서 일급 값과 고차 함수를 만들었다.
다시 정리해보면
🍳 코드의 냄새: 함수 이름에 있는 암묵적 인자.
함수 본문에서 사용하는 어떤 값이 함수 이름에 나타난다면 함수 이름에 있는 암묵적 인자
는 코드 냄새다.
특징
거의 똑같이 구현된 함수가 있다.
함수 이름이 구현에 있는 다른 부분을 가르킨다.
🍳 리팩터링: 암묵적 인자를 드러내기
함수 이름에 있는 암묵적 인자를 명시적인 함수 인자로 바꾸려면?
➡ 암묵적 인자 드러내기 리팩터링
➡ 암묵적 인자가 일급 값이 되도록 함수에 인자를 추가
단계
함수 이름에 있는 암묵적 인자를 확인
명시적인 인자를 추가
함수 본문에 하드 코등된 값을 새로운 인자로 바꾼다.
함수를 호출하는 곳을 고친다.
🍳 리팩터링: 함수 본문을 콜백으로 바꾸기
함수 본문을 콜백으로 바꾸기 리팩터링으로 함수 본문에 어떤 부분(비슷한 함수에 있는 서로 다른 부분)을 콜백으로 바꾼다.
단계
본문에서 바꿀 부분의 앞부분과 뒷부분을 확인
리팩터링 할 코드를 함수로 빼낸다.
빼낸 함수의 인자로 넘길 부분을 또 다른 함수로 빼낸다.
💻 카피-온-라이트 리팩터링하기
6장에서 살펴본 카피-온-라이트 패턴에 중복이 많아보인다.
함수 본문을 콜백으로 바꾸기 리팩터링이 해결해줄 수 있을 것 같다.
🍳 리팩터링: 함수 본문을 콜백으로 바꾸기
본문과 본문의 앞부분과 뒷부분 확인
함수 빼내기
콜백 빼내기
🍳 카피-온-라이트 단계
복사본을 만든다. (앞부분)
복사본을 변경한다. (본문)
복사본을 리턴한다. (뒷부분)
💻 배열에 대한 카피-온-라이트 리팩터링
🍳 1. 본문과 앞부분, 뒷부분을 확인하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function arraySet(array, idx, value) {
let copy = array.slice(); // 앞부분: 복사본을 만든다.
copy[idx] = value; // 본문: 복사본을 변경한다.
return copy; // 뒷부분: 복사본을 리턴한다.
}
// 비슷한 함수
function push(array, elem) {
let copy = array.slice(); // 앞부분: 복사본을 만든다.
copy.push(elem); // 본문: 복사본을 변경한다.
return copy; // 뒷부분: 복사본을 리턴한다.
}
// 비슷한 함수
function drop_last(array) {
let array_copy = array.slice(); // 앞부분: 복사본을 만든다.
array_copy.pop(); // 본문: 복사본을 변경한다.
return array_copy; // 뒷부분: 복사본을 리턴한다.
}
// 비슷한 함수
function drop_first(array) {
let array_copy = array.slice(); // 앞부분: 복사본을 만든다.
array_copy.shift(); // 본문: 복사본을 변경한다.
return array_copy; // 뒷부분: 복사본을 리턴한다.
}
➡️ 복사하고 리턴하는 것은 같으므로 arraySet()
함수로 리팩터링 해보자.
🍳 2. 함수 빼내기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 원래 코드
function arraySet(array, idx, value) {
let copy = array.slice(); // 함수로 빼낸다.
copy[idx] = value;
return copy;
}
// 함수로 빼낸 코드
function arraySet(array, idx, value) {
return withArrayCopy(array);
}
function withArrayCopy(array) {
let copy = array.slice();
copy[idx] = value; // 아직 정의되지 않은 부분
return copy;
}
아직 idx와 value가 정의되지 않았으므로 코드의 동작이 이뤄지지 않는다.
🍳 3. 콜백 빼내기
콜백은 배열을 변경하는 일을 하므로 modify
라고 해본다.
🔻 원래 코드
1
2
3
4
5
6
7
8
9
function arraySet(array, idx, value) {
return withArrayCopy(array);
}
function withArrayCopy(array) {
let copy = array.slice();
copy[idx] = value; // 본문을 인자로 만들어 전달한다.
return copy;
}
🔻 콜백으로 빼낸 코드
1
2
3
4
5
6
7
8
9
10
11
function arraySet(array, idx, value) {
return withArrayCopy(array, function(copy) {
copy[idx] = value;
});
}
function withArrayCopy(array, modify) {
let copy = array.slice();
modify(copy)
return copy;
} // 카피-온-라이트 원칙을 따르고 재사용할 수 있는 함수
🍳 리팩터링으로 얻은 것
표준화된 원칙
새로운 동작에 원칙을 적용할 수 있음
여러 개를 변경할 때 최적화
🍳 다른 함수에도 적용해보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function push(array, elem) {
return withArrayCopy(array, function(copy) {
copy.push(elem);
});
}
function drop_last(array) {
return withArrayCopy(array, function(copy) {
copy.pop();
});
}
function drop_first(array) {
return withArrayCopy(array, function(copy) {
copy.shift();
});
}
💻 함수를 리턴하는 함수
🍳 예시 상황
1
2
3
4
5
6
7
8
9
10
11
12
13
try {
saveUserData(user);
// 이 한 줄만 다르고 나머지는 모든 코드에서 중복이 된다.
} catch (error) {
logSnapErrors(error);
}
try {
fetchProduct(productId);
// 이 한 줄만 다르고 나머지는 모든 코드에서 중복이 된다.
} catch (error) {
logSnapErrors(error);
}
🍳 해결 방법 1: 반복되는 코드 캡슐화하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function withLogging(f) { // 반복되는 코드를 캡슐화한다.
try {
f();
} catch (error) {
logSnapErrors(error);
}
}
withLogging(function() {
saveUserData(user);
});
withLogging(function() {
fetchProduct(productId);
});
로그를 남기기 위한 일반적인 시스템이 생겼으나, 여전히 두 가지 문제가 있다.
어떤 부분에 로그를 남기는 것을 깜빡할 수 있다.
모든 코드에 수동으로
withLogging()
함수를 적용해야 한다.
🍳 코드를 감싸지 않고 그냥 함수를 호출할 수 있다면?
🔻 원래 코드
1
2
3
4
5
6
7
8
9
10
11
try {
saveUserData(user);
} catch (error) {
logSnapErrors(error);
}
try {
fetchProduct(productId);
} catch (error) {
logSnapErrors(error);
}
🔻 이름을 명확하게 바꿈
1
2
3
4
5
6
7
8
9
10
11
12
// 로그를 남기지 않음을 알 수 있도록 명확한 이름
try {
saveUserDataNoLogging(user);
} catch (error) {
logSnapErrors(error);
}
try {
fetchProductNoLogging(productId);
} catch (error) {
logSnapErrors(error);
}
🔻 로그를 남기는 함수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 함수를 호출할 때 로그가 남을 것이라고 예상할 수 있음
function saveUserDataWithLogging(user) {
try {
saveUserDataNoLogging(user);
} catch (error) {
logSnapErrors(error);
}
}
function fetchProductWithLogging(user) {
try {
fetchProductNoLogging(productId);
} catch (error) {
logSnapErrors(error);
}
}
그러나, 아직 본문(logSnapErrors(error);
)이 계속 중복되는 문제가 있음
🍳 익명 함수로 만들고 인자 이름도 조금 더 일반적인 이름으로 바꿔보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function(arg) { // 앞부분
try { // 본문
saveUserDataNoLogging(arg);
} catch (error) {
logSnapErrors(error); // 뒷부분
}
}
function(arg) { // 앞부분
try { // 본문
fetchProductNoLogging(arg);
} catch (error) {
logSnapErrors(error); // 뒷부분
}
}
🍳 함수 본문을 콜백으로 바꾸기 리팩터링을 적용해보자
함수에 콜백 인자를 추가하는 대신 이 함수를 새로운 함수로 감싼다.
1
2
3
4
5
6
7
8
9
10
11
12
13
function wrapLogging(func) {// 함수를 인자로 받음
return function(arg) { // 나중에 실행됨.
try {
func(arg);
} catch(error) {
logSnapErrors(error);
}
}
}
let saveUserDataWithLogging = wrapLogging(saveUserDataNoLogging)
// 리턴값을 변수에 할당해 이름을 붙인다.
// 로그를 남기지 않는 함수를 변환하디 위해 wrapLoggin() 함수 호출
wrapLogging()
함수는 func
함수를 받아서 try/catch
구문으로 감싼 함수를 리턴한다. 이제 로그를 남기지 않는 버전을 로그를 남기는 버전으로 쉽게 바꿀 수 있다. 어떤 함수라도 슈퍼 파워를 줄 수 있게 되었다.
1
2
let saveUserDataWithLogging = wrapLogging(saveUserDataNoLogging)
let fetchProductWithLogging = wrapLogging(fetchProductNoLogging)
중복도 없앴고, 어떤 함수라도 같은 방식으로 로그를 남기는 함수로 쉽게 바꿀 수 있어졌다.
💻 요약
- 고차 함수로 패턴이나 원칙을 코드로 만들 수 있다.
- 고차 함수를 사용하지 않는다면 일일이 수작업을 해야한다.
- 고차 함수는 한번 정의하고 필요한 곳에 여러 번 사용할 수 있다.
- 고차 함수로 함수를 리턴하는 함수를 만들 수 있다.
- 리턴 받은 함수는 변수에 할당해서 이름이 있는 일반 함수처럼 쓸 수 있다.
- 고차 함수를 사용하면서 잃는 것도 있다.
- 고차 함수는 많은 중복 코드를 없애 주지만 가독성을 해칠 수 있다.
- 잘 익히고 적절한 곳에 써야 한다.
📚 레퍼런스
Normand, 김은민, and Normand, Eric. (쏙쏙 들어오는) 함수형 코딩 : 심플한 코드로 복잡한 소프트웨어 길들이기 / 에릭 노먼드 지음 ; 김은민 옮김 (2022). Print.