From e98eefc4c858c3db100097c62eef0d778b21f853 Mon Sep 17 00:00:00 2001 From: SangBeom Park Date: Sun, 19 May 2024 19:25:01 +0900 Subject: [PATCH] =?UTF-8?q?=EC=83=81=EB=B2=94=2014=EC=9E=A5=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\354\203\201\353\262\224.md" | 315 ++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 "\354\261\225\355\204\260_14/\354\203\201\353\262\224.md" diff --git "a/\354\261\225\355\204\260_14/\354\203\201\353\262\224.md" "b/\354\261\225\355\204\260_14/\354\203\201\353\262\224.md" new file mode 100644 index 0000000..448595d --- /dev/null +++ "b/\354\261\225\355\204\260_14/\354\203\201\353\262\224.md" @@ -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) 동작의 이름이 있으므로 각각의 동작을 기억하기 쉽다. + +- 분류 안에 블로그 글이 있다는 것을 알고 있다. +- 이제 어떤 키에 들어 있는지 기억하지 않아도 된다. + +⇒ 잘 모르겠는데 흠.. 여긴 다시 보자 + +## 정리 + +- 보통 일반적인 반복문은 재귀보다 명확하다. + - 하지만 중첩된 데이터를 다룰 때는 재귀가 더 쉽고 명확하다. +- 재귀는 스스로 불렀던 곳이 어디인지 유지하기 위해 **`스택`**을 사용한다. + - 재귀 함수에서 스택은 중첩된 데이터 구조를 그대로 반영한다. +- 깊이 중첩된 데이터는 이해하기 어렵다. + - 깊이 중첩된 데이터를 다룰 때 모든 데이터 구조와 어떤 경로에 어떤 키가 있는지 기억해야 한다. +- 많은 키를 가지고 있는 깊이 중첩된 구조에 추상화 벽을 사용하면 알아야 할 것이 줄어든다. + - 추상화 벽으로 깊이 중첩된 데이터 구조를 쉽게 다룰 수 있다.