-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[박상범] 챕터 14: 중첩된 데이터에 함수형 도구 사용하기 #65
The head ref may contain hidden characters: "\uCC55\uD13014/\uBC15\uC0C1\uBC94"
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,315 @@ | ||
# 14장 - 중첩된 데이터에 함수형 도구 사용하기 | ||
## 이번 장에서 살펴볼 내용 | ||
|
||
- 해시 맵에 저장된 값을 다루기 위한 고차 함수를 만들기 | ||
- 중첩된 데이터를 고차 함수로 쉽게 다루는 방법 | ||
- 재귀를 이해하고 안전하게 재귀를 사용하는 방법 | ||
- 깊이 중첩된 엔티티에 추상화 벽을 적용해서 얻을 수 있는 장점 | ||
|
||
## **객체를 다루기 위한 고차 함수** | ||
|
||
이전 챕터에서는 배열을 다루는 고차 함수로 작업해봤는데, | ||
|
||
객체를 다룰 수 있는 고차함수가 있다면 유용할 것 같다. | ||
|
||
--- | ||
|
||
## **update() 도출하기** | ||
|
||
이 코드에서는 동시에 두 가지 리팩터링을 해야한다. | ||
|
||
- 함수 이름에 있는 암묵적인 인자를 암묵적 인자를 드러내기 리팩터링으로 동작 이름을 명시적인 인자로 바꾸기 | ||
- 명시적으로 바꿔야 할 인자가 일반값이 아닌 동작이기 때문에 함수 본문을 콜백으로 바꾸기 리팩터링으로 동작을 함수 인자로 받도록 해야 함 | ||
|
||
```jsx | ||
// as-is | ||
function incrementField(item, field){ | ||
let value = item[field]; | ||
let newValue = value + 1; | ||
let newItem = objectSet(item, field, newValue); | ||
return newItem; | ||
} | ||
|
||
// to-be | ||
function incrementField(item, field){ | ||
return updateField(item, field, function(value){ | ||
return value + 1; | ||
}) | ||
} | ||
|
||
function updateField(item, field, modify){ | ||
let value = item[field]; | ||
let newValue = modify(value); | ||
let newItem = objectSet(item, field, newValue); | ||
return newItem; | ||
} | ||
``` | ||
|
||
위 updateField 함수를 더 일반적인 이름으로 바꿔보자 | ||
|
||
```jsx | ||
function update(object, key, modify){ | ||
let value = object[key]; | ||
let newValue = modify(value); | ||
let newObject = objectSet(object, key, newValue); | ||
return newObject; | ||
} | ||
``` | ||
|
||
update()는 객체에 있는 값을 바꾼다. | ||
|
||
바꿀 객체와 바꾸려는 키, 바꾸는 동작을 함수로 넘기면 된다. | ||
|
||
objectSet()을 사용하기 때문에 카피-온-라이트 원칙을 따른다. | ||
|
||
## **리팩터링 : 조회하고 변경하고 설정하는 것을 update()로 교체하기** | ||
|
||
```jsx | ||
// 리팩터링 전 | ||
function incrementField(item, field){ | ||
let value = item[field]; // 조회 | ||
let newValue = value + 1; // 변경 | ||
let newItem = objectSet(item, field, newValue); // 설정 (카피 온 라이트) | ||
return newItem; | ||
} | ||
|
||
//리팩터링 후 | ||
function incrementField(item, field){ | ||
return updateField(item, field, function(value){ | ||
return value + 1; | ||
}) | ||
} | ||
``` | ||
|
||
## **조회하고 변경하고 설정하는 것을 update()로 교체하기 단계** | ||
|
||
이 리팩터링은 두 단계로 되어 있다. | ||
|
||
1. 조회하고 바꾸고 설정하는 것을 찾는다. | ||
2. 바꾸는 동작을 콜백으로 전달해서 update()로 교체한다. | ||
|
||
```jsx | ||
// **단계 1: 조회하고 바꾸고 설정하는 것을 찾는다** | ||
function halveField(item, field){ | ||
let value = item[field]; // 조회 | ||
let newValue = value / 2; // 변경 | ||
let newItem = objectSet(item, field, newValue); // 설정 | ||
return newItem; | ||
} | ||
|
||
// 단계 2: update()로 교체한다. | ||
function halveField(item, field){ | ||
return update(item, field, function(value){ | ||
return value / 2; //바꾸는 동작을 콜백으로 전달 | ||
}) | ||
} | ||
``` | ||
|
||
## **중첩된 데이터에 update() 사용하기** | ||
|
||
```jsx | ||
// 요구사항 객체 | ||
let shirt = { | ||
name : 'shirt', | ||
price : 13, | ||
options : { // 객체 안에 객체가 중첩 | ||
color : 'blue', | ||
size : 3 // options 객체 안에 있는 값을 꺼내야 한다. | ||
} | ||
} | ||
``` | ||
|
||
```jsx | ||
//원래 코드 | ||
function incrementSize(item){ | ||
let options = item.options; | ||
let size = option.size; // 조회 | ||
let newSize = size + 1; // 변경 | ||
let newOptions = objectSet(options, 'size', newSize); // 설정 | ||
let newItem = objectSet(item, 'options', newOptions); | ||
return newItem; | ||
} | ||
|
||
// 한 번 리팩터링 후 | ||
function incrementSize(item){ | ||
let options = item.options; // 조회 | ||
let newOptions = update(options, 'size', increment); // 변경 | ||
let newItem = objectSet(item, 'options', newOptions); // 설정 | ||
return newItem; | ||
} | ||
|
||
// 두 번 리팩터링한 코드 | ||
function incrementSize(item){ | ||
return update(item, 'options', function(options){ | ||
return update(options, 'size', increment); | ||
}) | ||
} | ||
``` | ||
|
||
update()를 중첩해서 부르면 더 깊은 단계로 중첩된 객체에도 사용할 수 있다. | ||
|
||
## **updateOption() 도출하기** | ||
|
||
위 리팩토링한 코드에서는 아직도 냄새가 난다. | ||
|
||
함수 이름에 있는 암묵적 인자를 본문에서 두번이나 쓰고 있다. | ||
|
||
이 코드를 일반화해서 updateOption() 을 만들어 보자 | ||
|
||
```jsx | ||
//암묵적 option 인자 | ||
function incrementSize(item){ | ||
return update(item, 'options', function(options){ | ||
return update(options, 'size', increment); | ||
}) | ||
} | ||
|
||
//명시적 option 인자로 수정 | ||
function incrementOption(item, option){ | ||
return update(item, 'options', function(options){ | ||
return update(options, option, increment); | ||
}) | ||
} | ||
|
||
//명시적 modify 인자로 수정 | ||
function updateOption(item, option, modify){ | ||
// 여전히 코드 냄새 진동함 | ||
return update(item, 'options', function(options){ | ||
return update(options, option, modify); | ||
}) | ||
} | ||
``` | ||
|
||
## **update2() 도출하기** | ||
|
||
암묵적 인자를 제거해보자. 어떻게 하면 될까? | ||
|
||
함수 이름, 인자를 일반적인 이름으로 변경하면 된다. | ||
|
||
```jsx | ||
function update2(object, key1, key2, modify){ | ||
return update(object, key1, function(value1){ | ||
return update(value1, key2, modify); | ||
}) | ||
} | ||
``` | ||
|
||
## update3() 도출하기 | ||
|
||
알고보니 객체가 한번 더 중첩되어 있었음;;; | ||
|
||
```jsx | ||
let cart = { | ||
shirt : { | ||
name : 'shirt', | ||
price : 13, | ||
options : { | ||
color : 'blue', | ||
size : 3 | ||
} | ||
} | ||
} | ||
``` | ||
|
||
책에선 방법을 여러가지 해결방법을 설명해줌. 근데 update3이 의미가 있나? 또 중첩되면 어쩔건데? update3()은 임시방편일 뿐, nestedUpdate 함수를 사용하자. | ||
|
||
## **nestedUpdate() 도출하기** | ||
|
||
```jsx | ||
function nestedUpate(object, keys, modify){ | ||
if(keys.length === 0){ | ||
return modify(object); // 종료 조건(경로의 길이가 0일 때) | ||
} | ||
let key1 = keys[0]; | ||
let restOfKeys = drop_first(keys); // 종료 조건에 가까워지게 항목을 하나씩 없앰 | ||
return update(object, key1, function(value1){ | ||
return nestedUpate(value1, restOfKeys, modify); //재귀호출 | ||
}); | ||
} | ||
``` | ||
|
||
## **안전한 재귀 사용법** | ||
|
||
재귀는 for나 while 반복문처럼 무한 반복에 빠질 수 있음 | ||
|
||
### **1. 종료 조건** | ||
|
||
재귀를 멈추려면 **종료 조건**(base case)이 필요하다. | ||
|
||
종료 조건은 재귀가 멈춰야 하는 곳에 있어야 한다. 더는 재귀를 호출하지 않으므로 그 위치에서 재귀가 종료된다. | ||
|
||
### **2. 재귀 호출** | ||
|
||
재귀 함수는 최소 하나의 **재귀 호출**이 있어야 한다. | ||
|
||
재귀 호출이 필요한 곳에서 재귀 호출을 해야 한다. | ||
|
||
### **3. 종료 조건에 다가가기** | ||
|
||
재귀 함수를 만든다면 최소 하나 이상의 인자가 점점 줄어들어야 한다. | ||
|
||
그래야 종료 조건에 가까워질 수 있다. | ||
|
||
- 예를 들어, 종료 조건이 빈 배열이라면 각 단계에서 배열 항목을 없애야 한다. | ||
|
||
각 재귀 호출에서 한 단계씩 종료 조건에 가까워진다면 결국 종료 조건과 일치해 재귀 함수가 끝나게 된다. | ||
|
||
가장 좋지 않은 것은 재귀 호출에 같은 인자를 그대로 전달하는 것이다. 이렇게 되면 무한 반복에 빠질 가능성이 높아진다. | ||
|
||
## 깊이 중첩된 구조를 설계할 때 생각할 점 | ||
|
||
깊이 중첩된 데이터에 nestedUpdate()를 쓰려면 긴 키 경로가 필요하다. | ||
|
||
키 경로가 길면 중간 객체가 어떤 키를 가졌는지 기억하기 어렵다. 중첩된 각 단계의 데이터 구조를 모두 기억해야 한다 | ||
|
||
기억해야할 것이 너무 많을 때 추상화 벽을 사용하면 구체적인 것을 몰라도 된다 | ||
|
||
## **깊이 중첩된 데이터에 추상화 벽 사용하기** | ||
|
||
```jsx | ||
// 주어진 ID로 블로그를 변경하는 함수 | ||
function updatePostById(category, id, modifyPost){ // 명확한 함수 이름 | ||
return nestedUpdate(category, ['posts', id], modifyPost); | ||
// ['posts', id] => 분류의 구조 같은 구체적인 부분은 추상화 벽 뒤로 숨김 | ||
// modifyPost => 블로그 글 구조에 대해서는 콜백에 맡김 | ||
} | ||
|
||
// 글쓴이를 수정하는 함수 | ||
function updateAuthor(post, modifyUsers){ | ||
return update(post, 'author', modifyUser); | ||
} | ||
|
||
// 사용자 이름을 대문자로 바꾸는 함수 | ||
function capitalizeName(user){ | ||
return update(user, 'name', capitalize); | ||
} | ||
|
||
// 모두 함친 함수 | ||
updatePostById(blogCategory, '12', function(post){ | ||
return updateAuthor(post, capitalizeUserName); // capitalize 할 때 키를 몰라도 됨 | ||
}); | ||
``` | ||
|
||
더 좋아진 점이 두 가지 정도가 있다. | ||
|
||
1) 네 가지에서 세 가지로 줄었다는 점 | ||
|
||
- 기억해야 할 것이 하나 줄었기 때문에 좋아졌다고 할 수 있다. | ||
|
||
2) 동작의 이름이 있으므로 각각의 동작을 기억하기 쉽다. | ||
|
||
- 분류 안에 블로그 글이 있다는 것을 알고 있다. | ||
- 이제 어떤 키에 들어 있는지 기억하지 않아도 된다. | ||
|
||
⇒ 잘 모르겠는데 흠.. 여긴 다시 보자 | ||
|
||
## 정리 | ||
|
||
- 보통 일반적인 반복문은 재귀보다 명확하다. | ||
- 하지만 중첩된 데이터를 다룰 때는 재귀가 더 쉽고 명확하다. | ||
- 재귀는 스스로 불렀던 곳이 어디인지 유지하기 위해 **`스택`**을 사용한다. | ||
- 재귀 함수에서 스택은 중첩된 데이터 구조를 그대로 반영한다. | ||
- 깊이 중첩된 데이터는 이해하기 어렵다. | ||
- 깊이 중첩된 데이터를 다룰 때 모든 데이터 구조와 어떤 경로에 어떤 키가 있는지 기억해야 한다. | ||
- 많은 키를 가지고 있는 깊이 중첩된 구조에 추상화 벽을 사용하면 알아야 할 것이 줄어든다. | ||
- 추상화 벽으로 깊이 중첩된 데이터 구조를 쉽게 다룰 수 있다. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 요부분은 공감이 안되긴 하더라구요