쏙쏙 들어오는 함수형 코딩 - CHAPTER 14.
포스트
취소

쏙쏙 들어오는 함수형 코딩 - CHAPTER 14.

쏙쏙 들어오는 함수형 코딩

해당 포스트는 쏙쏙 들어오는 함수형 코딩을 학습하며 필요한 내용을 정리한 포스트입니다.




🌈 14. 중첩된 데이터에 함수형 도구 사용하기

이번 장에서 살펴볼 내용

  • 해시 맵에 저장된 값을 다루기 위한 고차 함수를 만들기
  • 중첩된 데이터를 고차 함수로 쉽게 다루는 방법
  • 재귀를 이해하고 안전하게 재귀를 사용하는 방법
  • 깊이 중첩된 엔티티에 추상화 벽을 적용해서 얻을 수 있는 장점

💻 객체를 다루기 위한 고차 함수

이전 챕터에서는 배열을 다루는 고차 함수로 작업해봤다.

객체를 다룰 수 있는 고차함수가 있다면 유용할 것 같다.




💻 update() 도출하기

🔻예시 코드

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
27
function incrementField(item, field){
  let value = item[field];
  let newValue = value + 1;
  let newItem = objectSet(item, field, newValue);
  return newItem;
}

function decrementField(item, field){
  let value = item[field];
  let newValue = value - 1;
  let newItem = objectSet(item, field, newValue);
  return newItem;
}

function doubleField(item, field){
  let value = item[field];
  let newValue = value * 2;
  let newItem = objectSet(item, field, newValue);
  return newItem;
}

function halveField(item, field){
  let value = item[field];
  let newValue = value / 2;
  let newItem = objectSet(item, field, newValue);
  return newItem;
}

모든 코드가 비슷하다는 것을 알 수 있다. 하려는 동작만 다르다.

동작은 함수 이름에 들어 있고, 그 이름으로 부른다.

이 함수들로 어떤 객체라도 바꿀 수 있는 함수를 도출한다면 중복을 많이 없앨 수 있다.

이 코드에서는 동시에 두 가지 리팩터링을 해야한다.

  • 함수 이름에 있는 암묵적인 인자를 암묵적 인자를 드러내기 리팩터링으로 동작 이름을 명시적인 인자로 바꾸기

  • 명시적으로 바꿔야 할 인자가 일반값이 아닌 동작이기 때문에 함수 본문을 콜백으로 바꾸기 리팩터링으로 동작을 함수 인자로 받도록 해야 함.

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

모든 동작을 고차 함수 하나로 합쳤다.

특정 필드를 바꾸는 함수가 아니기 떄문에 함수 이름에 field는 빼고 일반적인 이름으로 변경해주자.

1
2
3
4
5
6
function update(object, key, modify){
  let value = object[key];
  let newValue = modify(value);
  let newObject = objectSet(object, key, newValue);
  return newObject;
}

update()는 객체에 있는 값을 바꾼다.

바꿀 객체와 바꾸려는 키, 바꾸는 동작을 함수로 넘기면 된다.

objectSet()을 사용하기 때문에 카피-온-라이트 원칙을 따른다.




💻 리팩터링 : 조회하고 변경하고 설정하는 것을 update()로 교체하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 리팩터링 전
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;
  })
}

리팩터링 전 코드를 보면 전체 동작은 세 단계다.

1) 객체에서 값을 조회 2) 값을 바꾸기 3) 객체에 값을 설정(카피-온-라이트 사용)

🍳 조회하고 변경하고 설정하는 것을 update()로 교체하기 단계

이 리팩터링은 두 단계로 되어 있다.

1) 조회하고 바꾸고 설정하는 것을 찾는다. 2) 바꾸는 동작을 콜백으로 전달해서 update()로 교체한다.

단계 1: 조회하고 바꾸고 설정하는 것을 찾는다.

1
2
3
4
5
6
function halveField(item, field){
  let value = item[field]; // 설정
  let newValue = value /2; //바꾸기
  let newItem = objectSet(item, field, newValue);
  return newItem;
}

단계 2: update()로 교체한다.

1
2
3
4
5
function halveField(item, field){
  return update(item, field, function(value){
    return value / 2; //바꾸는 동작을 콜백으로 전달
  })
}

조회하고 변경하고 설정하는 것을 update()로 교체하기 리팩터링은 중첩된 객체에 적용하기 좋다.




💻 중첩된 update 시각화 하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let shirt = {
  name : 'shirt',
  price : 13,
  options : { // 객체 안에 객체가 중첩
    color : 'blue',
    size : 3 // options 객체 안에 있는 값을 꺼내야 한다.
  }
}

function increamentSize(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;
}// 리팩터링 했지만 뭔가 이상한데! 




💻 중첩된 데이터에 update() 사용하기

🍳 조회하고 변경하고 설정하는 것을 update()로 교체하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//원래 코드
function increamentSize(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 increamentSize(item){
  let options = item.options; // 조회
  let newOptions = update(options, 'size', increment); // 변경
  let newItem = objectSet(item, 'options', newOptions); // 설정
  return newItem;
}

한번 더 리팩터링할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 한 번 리팩터링한 코드
function increamentSize(item){
  let options = item.options; // 조회
  let newOptions = update(options, 'size', increment); // 변경
  let newItem = objectSet(item, 'options', newOptions); // 설정
  return newItem;
}

// 두 번 리팩터링한 코드 
function increamentSize(item){
  return update(item, 'options', function(options){
    return update(options, 'size', increment); 
  })
}

중첩된 객체에 중첩된 update를 사용할 수 있다는 중요한 사실을 알았다.

update()를 중첩해서 부르면 더 깊은 단계로 중첩된 객체에도 사용할 수 있다.




💻 updateOption() 도출하기

update() 안에서 update() 를 호출 하는 코드를 만들었따.

이 코드를 일반화해서 updateOption() 을 만들 수 있다.

1
2
3
4
5
function increamentSize(item){
  return update(item, 'options', function(options){
    return update(options, 'size', increment); 
  })
}

update()를 두 번 호출하고 size 데이터도 두 단계로 중첩된 것을 볼 수 있다.

데이터가 중첩된 만큼 update()를 호출해야 하는 것을 알 수 있다.

위 코드에서는 아직도 냄새가 난다. 함수 이름에 있는 암묵적 인자를 본문에서 두번이나 쓰고 있다.

이것을 해결해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//암묵적 option 인자
function increamentSize(item){
  return update(item, 'options', function(options){
    return update(options, 'size', increment); 
  })
}

//명시적 option 인자로 수정
function increamentOption(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); 
  })
}

이 함수는 제품(객체)과 옵션 이름, 옵션을 바꾸는 함수를 받는다.

1
2
3
4
5
6
function updateOption(item, option, modify){
  return update(item, 'options', function(options){
                      // 여전히 코드 냄새!!
    return update(options, option, modify); 
  }) 
}




💻 update2() 도출하기

새로운 암묵적 인자가 생겼다.

리팩터링을 한 번 더 리팩터링 하면 일반적인 함수 update2()를 도출할 수 있다.

1
2
3
4
5
6
function updateOption(item, option, modify){
  return update(item, 'options', function(options){
                      // 여전히 코드 냄새!!
    return update(options, option, modify); 
  }) 
}

세 번째 리팩터링

함수 이름, 인자를 일반적으로 바꾼다.

1
2
3
4
5
function update2(object, key1, key2, modify){
  return update(object, key1,, function(value1){
    return update(value1, key2, modify); 
  }) 
}

좀 더 일반적인 함수가 되었다. update2() 는 두 단계로 중첩된 어떤 객체에도 쓸 수 있는 함수.

그래서 함수를 쓸 때 두 개의 키가 필요하다.

1
2
3
4
5
function incrementSize(item){
  return undate2(item, 'options', 'size', function(size){
    return size + 1;
  })
}




💻 incrementSizeByName()을 만드는 네 가지 방법

알고 보니 객체가 한 번 더 중첩되어 있었다!

1
2
3
4
5
6
7
8
9
10
let cart = {
  shirt : { 
    name : 'shirt',
    price : 13,
    options : {
      color : 'blue',
      size : 3
    }
  }
}

이 코드를 고치려면 update3()이 필요해 보이는데 …

이 문제를 해결할 수 있는 네 가지 방법을 살펴보자.

장바구니 안에 특정 이름을 가진 제품의 크기 옵션을 늘리고 싶을 때 사용할 함수를 incrementSizeByName()이라고 해보자.

이 함수는 장바구니와 제품의 이름을 받아서 해당 제품의 크기 옵션을 늘린다.

어떻게 구현할 수 있을까…?

🍳 옵션 1 : update()와 incrementSize()로 만들기

장바구니 안에 중첩된 제품을 다뤄야 한다.

update() 는 중첩된 객체의 값을 바꿀 수 있기 때문에 incrementSize() 함수를 만들면 된다.

1
2
3
function incrementSizeByName(cart, name){
  return update(cart, name, incrementSize);
}// 이미 있는 도구를 활용한 직관적인 방법.


🍳 옵션 2 : update()와 update2()로 만들기

1
2
3
4
5
6
7
8
function incrementSizeByName(cart, name){
  return update(cart, name, function(item){
    return update2(item, 'options', 'size', function(size){
      // update()안에 있는 update2()로 incrementSize()를 인라인으로 구현
      return size + 1;
    })
  })
}


🍳 옵션 3 : update()로 만들기

두 번 중첩된 update()로 update2()를 인라인으로 만들 수 있다.

1
2
3
4
5
6
7
8
9
function incrementSizeByName(cart, name){
  return update(cart, name, function(item){
    return update(item, 'options', function(options){
      return update(options, 'size', function(size){
        return size + 1; // update()만 불러서 인라인으로 구현
      })
    })
  })
}


🍳 옵션 4 : 조회하고 바꾸고 설정하는 것을 직접 만들기

1
2
3
4
5
6
7
8
9
10
function incrementSizeByName(cart, name){
  let item = cart[item];
  let options = item.options;
  let size = options.size;
  let newSize = size + 1;
  let newOptions = objectSet(options, 'size', newSize);
  let newItem = objectSet(item, 'options', newOptions);
  let newCart = objectSet(cart, name, newItem);
  return newCart;
}




💻 update3() 도출하기

옵션2의 코드를 가지고 암묵적 인자 드러내기 리팩터링을 통해 update3()을 만들어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 옵션 2
function incrementSizeByName(cart, name){
  return update(cart, name, function(item){ //
    return update2(item, 'options', 'size', function(size){ // 암묵적 인자
      return size + 1;
    })
  })
}

// 리팩터링 코드
function incrementSizeByName(cart, name){
  return update3(cart, name, 'options', 'size', function(size){
                      // 경로 3개    
    return size + 1;
  })
}

function update3(object, key1, key2, key3, modify){
  return update(object, key1, function(object2){
    return update2(object2, key2, key3, modify);
  });
}




💻 nestedUpdate() 도출하기

중첩된 개수에 상관없이 쓸 수 있는 nestedUpdate()를 만들어 보자.

패턴이 명확하지 않지만, 함수를 살펴보면 단서가 있다.

만약 updateX()를 만들려고 한다면 update() 안에 updateX-1()을 불러주면 된다.

다만 update0()는 두 가지 이유로 지금 패턴과 다르다.

하나는 사용하는 키가 없기 때문에 키가 한 개 필요한 update()를 부를 수 없다.

두 번째 이유는 X-1-1 이 되기 때문에 경로 길이를 표현할 수 없다.

조회나 설정을 하지 않고 그냥 변경만 하는 함수이므로, 찾으려는 값만 있으면 되기 때문에 update0()은 modify()를 그냥 호출하는 함수가 된다.

1
2
3
function update0(value, modify){
  return modify(value);
}

키의 개수와 동일하게 함수의 이름에 암묵적 인자가 포함되어 냄새가 난다.

암묵적 인자를 들어내기 리팩터링을 해보자

1
2
3
4
5
6
7
              //X        // X만큼의 키
function update3(object, key1, key2, key3, modify){
  return update(object, key1, function(value1){
    return update2(object2, key2, key3, modify);
              // X-1      // 첫 번째 키는 제외
  });
}

depth라는 인자를 추가해보자.

1
2
3
4
5
function updateX(object, depth, key1, key2, key3, modify){
  return update(object, key1, function(value1){
    return updateX(value1, depth-1, key2, key3, modify);
  });     //재귀호출      depth-1 전달
}

깊이와 키 개수를 어떻게 맞출 수 있을까?

depth 인자와 실제 키 개수는 달라질 수 있어서 버그가 생길 것 같다.

키의 개수와 순서가 중요하다는 점이 단서가 된다.

모든 키를 배열로 넘긴다면 depth 인자는 배열의 길이가 된다.

1
2
3
4
5
6
7
function updateX(object, keys, modify){
  let key1 = keys[0]; // 첫번째 키로 update() 호출
  let restOfKeys = drop_first(keys); // 나머지 키로 재귀 함수를 호출
  return update(object, key1, function(value1){
    return updateX(value1, restOfKeys, modify);
  });     //재귀호출      
}

update0()를 제외하면 같은 패턴을 가지고 있기 때문에 updateX()로 바꿔 쓸 수 있다.

update0()는 update() 함수를 호출하지 않는데 어떻게 해야할까..

keys 배열의 길이가 0일 때는 키가 없다는 것이므로 이 경우에는 modify()를 호출해주고 그렇지 않은 경우에는 updateX()를 호출하면 될 것같다.

1
2
3
4
5
6
7
8
9
10
function updateX(object, keys, modify){
  if(keys.length === 0){
    return modify(object);  // 재귀호출 없음
  }
  let key1 = keys[0]; // 첫번째 키로 update() 호출
  let restOfKeys = drop_first(keys); // 나머지 키로 재귀 함수를 호출
  return update(object, key1, function(value1){
    return updateX(value1, restOfKeys, modify);
  });     //재귀호출      
}

이렇게 해주면 키 길이에 상관없이 쓸 수 있는 updateX()함수가 생겼다.

updateX()라는 이름보다 nestedUpdate() 라는 이름을 사용하는 것이 더 일반적이다.

객체와 중첩된 객체의 값을 가르키는 키 경로와 바꿀 함수를 인자로 받는다.

그리고 빠져나오는 모든 경로에 있는 객체의 복사본을 만든다

1
2
3
4
5
6
7
8
9
10
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 updateX(value1, restOfKeys, modify);
  });     //재귀호출      
}




😀 안전한 재귀 사용법

재귀는 for나 while 반복문처럼 무한 반복에 빠질 수 있다.

가이드를 잘 지켜서 문제가 생기지 않도록 하자

🍳 1. 종료 조건

재귀를 멈추려면 종료 조건(base case)이 필요하다.

종료 조건은 재귀가 멈춰야 하는 곳에 있어야 한다. 더는 재귀를 호출하지 않으므로 그 위치에서 재귀가 종료된다.

1
2
3
4
5
6
7
8
9
10
function updateX(object, keys, modify){
  if(keys.length === 0){ // 종료 조건
    return modify(object);  // 재귀호출 없음
  }
  let key1 = keys[0]; 
  let restOfKeys = drop_first(keys); 
  return update(object, key1, function(value1){
    return updateX(value1, restOfKeys, modify);
  });    
}

종료 조건은 확인하기 쉽다.

보통 배열 인자가 비었거나 점점 줄어드는 값이 0이 되었거나, 찾아야 할 것이 없을 때 종료 조건이 된다. 종료 조건은 만들기 쉽다.


🍳 2. 재귀 호출

재귀 함수는 최소 하나의 재귀 호출이 있어야 한다.

재귀 호출이 필요한 곳에서 재귀 호출을 해야 한다.

1
2
3
4
5
6
7
8
9
10
function updateX(object, keys, modify){
  if(keys.length === 0){
    return modify(object);  
  }
  let key1 = keys[0]; 
  let restOfKeys = drop_first(keys); // 남은 키가 하나 감소
  return update(object, key1, function(value1){
    return updateX(value1, restOfKeys, modify);
  });     //재귀호출      
}


🍳 종료 조건에 다가가기

재귀 함수를 만든다면 최소 하나 이상의 인자가 점점 줄어들어야 한다.

그래야 종료 조건에 가까워질 수 있다.

  • 예를 들어, 종료 조건이 빈 배열이라면 각 단계에서 배열 항목을 없애야 한다.

각 재귀 호출에서 한 단계씩 종료 조건에 가까워진다면 결국 종료 조건과 일치해 재귀 함수가 끝나게 된다.

가장 좋지 않은 것은 재귀 호출에 같은 인자를 그대로 전달하는 것이다. 이렇게 되면 무한 반복에 빠질 가능성이 높아진다.




🤔 깊이 중첩된 데이터에 추상화 벽 사용하기

깊이 중첩된 데이터에 nestedUpdate()를 쓰려면 긴 키 경로가 필요하다.

키 경로가 길면 중간 객체가 어떤 키를 가졌는지 기억하기 어렵다. 중첩된 각 단계의 데이터 구조를 모두 기억해야 한다.

문제를 해결 하기 위해서는 같은 작업을 하면서 알아야 할 데이터 구조를 줄이는 것이다.

추상화 벽에 함수를 만들고 의미있는 이름을 붙여서 해결할 수 있다.

추상화 벽을 만들 때는 사용하려는 데이터의 이해도를 높일 수 있는 방향으로 만들어야 한다.

예시 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 주어진 ID로 블로그를 변경하는 함수
function updatePostById(category, id, modifyPost){
  return nestedUpdate(category, ['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);
});

더 좋아진 점이 두 가지 정도가 있다.

1) 네 가지에서 세 가지로 줄었다는 점

  • 기억해야 할 것이 하나 줄었기 때문에 좋아졌다고 할 수 있다.

2) 동작의 이름이 있으므로 각각의 동작을 기억하기 쉽다.

  • 분류 안에 블로그 글이 있다는 것을 알고 있다.
  • 이제 어떤 키에 들어 있는지 기억하지 않아도 된다.
  • 글쓴이도 마찬가지




💻 요점 정리

  • update() 는 일반적인 패턴을 구현한 함수형 도구다.
    • update() 를 사용하면 객체 안에서 값을 꺼내 변경하고 다시 설정하는 일을 수동으로 하지 않아도 된다.
  • nestedUpdate() 는 깊이 중첩된 데이터를 다루는 함수형 도구다
    • 바꾸려고 하는 값이 어디 있는지 가르키는 키 경로만 알면 중첩된 데이터를 쉽게 바꿀 수 있다.
  • 보통 일반적인 반복문은 재귀보다 명확하다.
    • 하지만 중첩된 데이터를 다룰 때는 재귀가 더 쉽고 명확하다.
  • 재귀는 스스로 불렀던 곳이 어디인지 유지하기 위해 스택을 사용한다.
    • 재귀 함수에서 스택은 중첩된 데이터 구조를 그대로 반영한다.
  • 깊이 중첩된 데이터는 이해하기 어렵다.
    • 깊이 중첩된 데이터를 다룰 때 모든 데이터 구조와 어떤 경로에 어떤 키가 있는지 기억해야 한다.
  • 많은 키를 가지고 있느 ㄴ깊이 중첩된 구조에 추상화 벽을 사용하면 알아야 할 것이 줄어든다.
    • 추상화 벽으로 깊이 중첩된 데이터 구조를 쉽게 다룰 수 있다.




📚 레퍼런스

Normand, 김은민, and Normand, Eric. (쏙쏙 들어오는) 함수형 코딩 : 심플한 코드로 복잡한 소프트웨어 길들이기 / 에릭 노먼드 지음 ; 김은민 옮김 (2022). Print.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.