Search
🛠️

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

 이전내용 (chap.14)

문제: 객체의 값을 바꿔야 함 → 여러 코드에서 중복 다수 발생

 해결책: [리팩터링] 조회, 변경, 설정을 update()로 교체하기

 객체를 다루는 함수형 도구 update()
function update(object, key, modify) { var value = object[key]; // 조회 var newValue = modify(value); // 변경 (콜백 함수 전달) var newObject = objectSet(object, key, newValue); // 설정 return newObject; // 리턴 }
JavaScript
복사
  update() 적용 전
function incrementField(item, field) { var value = item[field]; // 조회 var newValue = value + 1; // 변경 (새로운 값 선언) var newItem = objectSet(item, field, newValue); // 설정 return newItem; // 리턴 }
JavaScript
복사
update() 적용 후
function incrementField(item, field) { // 중복 작업이 제거되어 간소화 됨 return update(item, field, function(value) { return value + 1; }); }
JavaScript
복사

 중첩된 update() 시각화하기

문제: 도출한 update()는 중첩된 객체에는 적용 불가

 해결책: 현재 코드 시각화 후 리팩터링 진행

 중첩 된 데이터(size)를 수정하는 코드
// item 객체를 받아 option 내 size를 올려주는 함수 // '조회 - 조회 - 변경 - 설정 - 설정'의 순서 => update()와 맞지 않음 function incrementSize(item) { var options = item.options; // 조회 var size = options.size; // 조회 var newSize = size + 1; // 변경 var newOptions = objectSet(options, 'size', newSize); // 설정 var newItem = objectSet(item, 'options', newOptions); // 설정 return newItem }
JavaScript
복사
 중첩 된 데이터 (객체)
var shirt = { name: "shirt", price: 13, // 중첩된 객체 option options: { color: "blue", // 중첩된 객체 내 값을 바꿔야 함 (size) size: 3 } };
JavaScript
복사

시각화 1) 키를 가지고 객체에서 값을 조회 (options)

 동작 코드
function incrementSize(item) { var options = item.options; //... }
JavaScript
복사
 데이터
var shirt = { name: "shirt", price: 13, options: { color: "blue", size: 3 } };
JavaScript
복사

시각화 2) 키를 가지고 객체에서 값을 조회 (options.size)

 동작 코드
function incrementSize(item) { // ... var size = options.size; //... }
JavaScript
복사
 데이터
var shirt = { name: "shirt", price: 13, options: { color: "blue", size: 3 } };
JavaScript
복사

시각화 3) 새로운 값을 생성

 동작 코드
function incrementSize(item) { // ... var newSize = size + 1; //... }
JavaScript
복사
 데이터 (변경 전)
size = 3
JavaScript
복사
 데이터 (변경 후)
newSize = 4
JavaScript
복사

시각화 4) 복사본 생성 (options)

 동작 코드
function incrementSize(item) { // ... var newOptions = objectSet(options, 'size', newSize); // 설정 // ... } // [주석]: 이전 챕터에서 도출한 함수 objectSet function objectSet(object, key, value) { var copy = Object.assign({}, object); copy[key] = value; return copy; }
JavaScript
복사
 데이터 (변경 전)
options: { color: "blue", size: 3 }
JavaScript
복사
 데이터 (변경 후)
// 기존 option과 참조값 다름 newOptions: { color: "blue", size: 4 }
JavaScript
복사

시각화 5) 복사본 생성 (item)

 동작 코드
function incrementSize(item) { // ... var newItem = objectSet(item, 'options', newOptions); // 설정 return newItem; }
JavaScript
복사
 데이터 (변경 전)
shirt = { name: "shirt", price: 13, options: { color: "blue", size: 3 } };
JavaScript
복사
 데이터 (변경 후)
newItem = { name: "shirt", price: 13, newOption: { color: "blue", size: 4 } };
JavaScript
복사

 시각화 이후 중첩된 데이터에 update() 사용하기

문제: 도출한 update()는 중첩된 객체에는 적용 불가

 해결책: 중첩된 데이터에도 사용할 수 있도록 리팩터링 필요

1. 첫 번째 리팩터링: update() 1차 적용

 리팩터링 적용 전
// '조회 - 조회 - 변경 - 설정 - 설정 function incrementSize(item) { var options = item.options; // 조회 var size = options.size; // 변경 var newSize = size + 1; // 설정 var newOptions = objectSet(options, 'size', newSize); var newItem = objectSet(item, 'options', newOptions); return newItem; }
JavaScript
복사
리팩터링 적용 후
function incrementSize(item) { var options = item.options; // [리팩터링] 조회, 변경, 설정을 update()로 교체 var newOptions = update(options, 'size', increment); var newItem = objectSet(item, 'options', newOptions); return newItem; }
JavaScript
복사

2. 두 번째 리팩터링: update() 2차 적용

 리팩터링 적용 전
function incrementSize(item) { // 조회 var options = item.options; // 변경 var newOptions = update(options, 'size', increment); // 설정 var newItem = objectSet(item, 'options', newOptions); return newItem; }
JavaScript
복사
 리팩터링 적용 후
// update() 중첩 호출 시 중첩 된 객체에 적용 가능 function incrementSize(item) { return update(item, 'options', function(options) { // [리팩터링] 조회, 변경, 설정을 업데이트로 변경 return update(options, 'size', increment); }); }
JavaScript
복사

 updateOption() 도출하기

문제: 도출한 함수 incrementSize()의 이름에 암묵적 인자 존재

 해결책: [리팩터링] 암묵적 인자를 드러내기 필요

‘암묵적 인자를 드러내기’ 리팩터링 복습
// 함수 이름에 있는 암묵적 인자가 함수에 두 부분에 존재 function incrementSize(item) { return update(item, 'options', function(options) { return update(options, 'size', increment); }); }
JavaScript
복사

1. 첫 번째 리팩터링 (size)

 암묵적 인자가 있는 코드 (size)
// [코드의 냄새] 함수 이름에 있는 암묵적 인자 function incrementSize(item) { return update(item, 'options', function(options) { return update(options, 'size', increment); }); }
JavaScript
복사
명시적 인자로 변경 (option)
// 명시적 인자로 받기 위해 인자 option 추가 // 함수 이름도 인자에 맞게 수정 function incrementOption(item, option) { return update(item, 'options', function(options) { // 암묵적 인자 때문에 하드코딩 되어 있던 코드 수정 return update(options, option, increment); }); }
JavaScript
복사

2. 두 번째 리팩터링 (increment)

 암묵적 인자가 있는 코드 (increment)
// [코드의 냄새] 함수 이름에 있는 암묵적 인자 increment function incrementOption(item, option) { return update(item, 'options', function(options) { return update(options, option, increment); }); } // [참고] 앞서 언급된 함수 increment function increment(value) { return value + 1; }
JavaScript
복사
명시적 인자로 변경 (modify)
// 하드 코딩 된 increment 대신 함수를 명시적 인자로 받도록 수정 // 함수 이름도 인자에 맞게 수정 function updateOption(item, option, modify) { return update(item, 'options', function(options) { // 암묵적 인자 때문에 하드코딩 되어 있던 코드 수정 return update(options, option, modify); }); }
JavaScript
복사

update2() 도출하기

문제: 리팩터링 후 새로운 암묵적 인자 발생

 해결책: [리팩터링] 암묵적 인자를 드러내기 필요

 암묵적 인자가 있는 코드 (option)
function updateOption(item, option, modify) { return update(item, 'options', function(options) { return update(options, option, modify); }); }
JavaScript
복사
명시적 인자로 변경 (key)
function update2(object, key1, key2, modify) { return update(object, key1, function(value1) { return update(value1, key2, modify); }); }
JavaScript
복사

3. 결과

function incrementSize(item) { var options = item.options; var size = options.size; var newSize = size + 1; var newOptions = objectSet(options, 'size', newSize); var newItem = objectSet(item, 'options', newOptions); return newItem }
JavaScript
복사
function incrementSize(item) { // update2는 2단계 중첩 객체에 범용적으로 사용 가능한 함수 return update2(item, 'options', 'size', function(size) { return size + 1; }); } shirt = { name: "shirt", price: 13, options: { color: "blue", size: 3 } };
JavaScript
복사

중첩된 객체에 쓸 수 있는 update2() 시각화하기

 [용어 설명 - 경로]: 중첩 된 객체의 값을 가리키는 시퀸스, 경로는 각 단계의 키를 포함

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

현재상황: 특정 이름의 제품 크기 옵션 증가 함수 구현 완료 - 두 번 중첩 됨

요청사항: ‘장바구니 안’ 특정 이름의 제품 크기 옵션 증가 함수 필요 - 세 번 중첩 됨

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

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

[옵션 3]: update()로 만들기

[옵션 4]: 조회하고 설정하는 것을 직접 만들기

 update3() 도출하기

문제: 장바구니 내에 있는 제품의 특정 옵션 값 변경 필요 - 세 번 중첩 됨

 해결책: 세번 중첩 된 객체에 사용할 수 있는 update3() 작성

 update3 작성 전
// [옵션2] update()와 update2()로 만들기 function incrementSizeByName(cart, name) { return update(cart, name, function(item) { return update2(item, 'options', 'size', function(size) { return size + 1; }); }); }
JavaScript
복사
update3 작성 후
// update3() 작성 function update3(object, key1, key2, key3, modify) { return update(object, key1, function(object2) { return update2(object2, key2, key3, modify); }); } // update3() 이용 장바구니 내 제품 옵션 변경 (삼중 중첩 객체) function incrementSizeByName(cart, name) { return update3(cart, name, 'options', 'size', function(size) { return size + 1; }); }
JavaScript
복사

 nestedUpdate() 도출하기

문제: update1()…updateN()에서의 공통 패턴 존재 (코드의 냄새)

 해결책: 중첩된 개수와 무관하게 쓸 수 있는 nestedUpdate() 작성

 updateX(): update() 안에 updateX-1()을 불러주는 구조

 코드 (update4)
// 숫자만큼 키를 인자로 받음 // update()내 숫자가 하나 작은 updateX-1()이 있음 function update4(obj, key1, key2, key3, key4, modify) { return update(obj, key1, function(value1) { return update3(value1, key2, key3, key4, modify); }); }
JavaScript
복사
 코드 (update2)
 코드 (update3)
// 숫자만큼 키를 인자로 받음 // update()내 숫자가 하나 작은 updateX-1()이 있음 function update3(obj, key1, key2, key3, modify) { return update(object, key1, function(value1) { return update2(value1, key2, key3, modify); }); }
JavaScript
복사
 코드 (update1)
// 숫자만큼 키를 인자로 받음 // update()내 숫자가 하나 작은 updateX-1()이 있음 function update1(obj, key1, modify) { return update(object, key1, function(value1) { return update0(value1, modify); }); }
JavaScript
복사

update0(): key 인자(경로) 없이 콜백 함수 실행

// 따로 키를 전달 받지 않음 // modify를 호출하는 함수 function update0(value, modify) { return modify(value); }
JavaScript
복사

 nestedUpdate() 도출 하기 (update3() 이용)

1. 암묵적 인자 수정

 함수명에 있는 암묵적 인자가 있음
// 함수명에는 숫자 X 존재 + X개의 key 인자 존재 function update3(obj, key1, key2, key3, modify) { return update(obj, key1, function(value1) { // X-1에 해당 update() 함수 호출 + 첫번째 key 제외 return update2(value1, key2, key3, modify); }); }
JavaScript
복사
재귀 방식으로 변경
// 일반적인 함수명 + 명시적으로 depth를 나타내는 인자 전달 function updateX(obj, depth, key1, key2, key3, modify) { return update(obj, key1, function(value1) { // 재귀 방식으로 호출 + depth-1을 전달 + 하나 줄어든 key return updateX(value1, depth-1, key2, key3, modify); }); }
JavaScript
복사

2. 정보 전달 방식 수정

  depth와 key 정보를 인자로 각각 전달
// depth와 키를 직접 전달 // 키의 개수와 순서가 중요하게 됨 function updateX(obj, depth, key1, key2, key3, modify) { return update(obj, key1, function(value1) { return updateX(value1, depth-1, key2, key3, modify); }); }
JavaScript
복사
depth와 key 정보를 배열로 전달
// key와 depth 정보를 배열로 전달 function updateX(object, keys, modify) { // update 호출을 위해 첫번째 key를 분리 var key1 = keys[0]; // 재귀함수 호출을 위해 나머지 키 분리 var restOfKeys = drop_first(keys); return update(object, key1, function(value1) { return updateX(value1, restOfKeys, modify); }); } // [참고] function drop_first(array) { var array_copy = array.slice(); array_copy.shift(); return array_copy; }
JavaScript
복사

3. 종료 조건 추가

  종료 조건에 대한 고려 없음 (update0)
function updateX(object, keys, modify) { // update0에 대한 별도 처리가 없음 var key1 = keys[0]; var restOfKeys = drop_first(keys); return update(object, key1, function(value1) { return updateX(value1, restOfKeys, modify); }); }
JavaScript
복사
종료 조건 추가 (update0)
function updateX(object, keys, modify) { // update0에 해당 되는 조건에는 단순히 modify 호출 (종료 조건) if(keys.length === 0) return modify(object); var key1 = keys[0]; var restOfKeys = drop_first(keys); return update(object, key1, function(value1) { return updateX(value1, restOfKeys, modify); }); }
JavaScript
복사

4. 함수 이름 변경

  이름 변경 전
function updateX(object, keys, modify) { if(keys.length === 0) return modify(object); var key1 = keys[0]; var restOfKeys = drop_first(keys); return update(object, key1, function(value1) { return updateX(value1, restOfKeys, modify); }); }
JavaScript
복사
이름 변경 후
// 일반적인 이름으로 변경 function nestedUpdate(object, keys, modify) { if(keys.length === 0) return modify(object); var key1 = keys[0]; var restOfKeys = drop_first(keys); return update(object, key1, function(value1) { return updateX(value1, restOfKeys, modify); }); }
JavaScript
복사

쉬는 시간

Q. 어떻게 함수가 자신을 부를 수 있나요?
Q. 재귀의 핵심은 무엇인가요? 이해하기 어려운 것 같습니다.
Q. 반복문을 사용할 수는 없나요? for 반복문이 이해하기 더 쉬운 것 같은데요.
Q. 재귀 호출은 위험한가요? 무한 반복에 빠지거나 스택이 바닥날 수 있나요?

최종 결과물 + 안전한 재귀 사용법

최종 결과물
function nestedUpdate(object, keys, modify) { if(keys.length === 0) return modify(object); var key1 = keys[0]; var restOfKeys = drop_first(keys); return update(object, key1, function(value1) { return nestedUpdate(value1, restOfKeys, modify); }); }
JavaScript
복사
안전한 재귀 사용법 (3 steps)
function nestedUpdate(object, keys, modify) { // 1. [종료 조건]: 경로 배열의 길이가 0일 때 // 종료 조건은 재귀가 멈춰야 하는 곳에 존재 // 종료 조건에는 더 이상 재귀가 없음 if(keys.length === 0) return modify(object); var key1 = keys[0]; // 2. [종료 조건에 다가가기]: 항목을 하나씩 없애며 종료 조건에 가까워짐 // 항목이 줄어 들지 않으면 '무한 반복에 빠질 가능성' var restOfKeys = drop_first(keys); return update(object, key1, function(value1) { // 3. [재귀 호출]: 함수가 함수 스스로를 호출 (최소 1회 이상) return nestedUpdate(value1, restOfKeys, modify); }); }
JavaScript
복사

nestedUpdate() 시각화하기

function nestedUpdate(object, keys, modify) { if(keys.length === 0) return modify(object); var key1 = keys[0]; var restOfKeys = drop_first(keys); return update(object, key1, function(value1) { return nestedUpdate(value1, restOfKeys, modify); }); }
JavaScript
복사

1. 조회 (shirt)

스택 (일반적인 방향 따름 - 책과 반대)

객체
cart
[”shirt”, “options”, “size”]

객체

cart { shirt { name: "shirt", price: 13 options { color: "blue", size: 3 } } }
JavaScript
복사

호출하는 것

// shirt cart var value1 = object[key1]; // 조회 nestedUpdate(value1, keys, modify); // 재귀 호출
JavaScript
복사
function update(object, key, modify) { var value = object[key]; SP >> var newValue = modify(value); var newObject = objectSet(object, key, newValue); return newObject; }
JavaScript
복사

2. 조회 (options)

객체
shirt
[“options”, “size”]
cart
[”shirt”, “options”, “size”]
shirt { name: "shirt", price: 13 options { color: "blue", size: 3 } }
JavaScript
복사
// options shirt var value1 = object[key1]; // 조회 nestedUpdate(value1, keys, modify); // 재귀 호출
JavaScript
복사

3. 조회 (size)

객체
options
[”size”]
shirt
[“options”, “size”]
cart
[”shirt”, “options”, “size”]
options { color: "blue", size: 3 }
JavaScript
복사
// size options var value1 = object[key1]; // 조회 var restOfKeys = [] nestedUpdate(value1, keys, modify); // 재귀 호출
JavaScript
복사

4. 종료 조건에 도달

객체
3
[]
options
[”size”]
shirt
[“options”, “size”]
cart
[”shirt”, “options”, “size”]
3 -> 4
JavaScript
복사
modify(object) // function increment(value) { return value + 1; }
JavaScript
복사

5. 재귀가 없기 때문에 스택 pop (options)

객체
options
[”size”]
shirt
[“options”, “size”]
cart
[”shirt”, “options”, “size”]
options-copy { color: "blue", size: 4 }
JavaScript
복사
// options size 4 objectSet(object, key1, newValue1)
JavaScript
복사
function objectSet(object, key, value) { var copy = Object.assign({}, object); copy[key] = value; return copy; }
JavaScript
복사

5. 재귀가 없기 때문에 스택 pop (shirt)

객체
shirt
[“options”, “size”]
cart
[”shirt”, “options”, “size”]
shirt-copy { name: "shirt", price: 13 options-copy { color: "blue", size: 4 } }
JavaScript
복사
// shirt options objectSet(object, key1, newValue1)
JavaScript
복사

6. 재귀가 없기 때문에 스택 pop (cart)

객체
cart
[”shirt”, “options”, “size”]
cart-copy { shirt-copy { name: "shirt", price: 13 options-copy { color: "blue", size: 4 } } }
JavaScript
복사
// cart shirt objectSet(object, key1, newValue1)
JavaScript
복사
Chrome 디버거를 위한 전체 코드

 재귀 함수가 적합한 이유

 배열: 차례대로 처리

1. 처음 부터 끝까지 순서대로 처리

2. 결과 배열에 처리한 항목 추가

 중첩 데이터: 깊은 단계로 들어가며 처리

1. 깊이 들어가며 값 조회

2. 가장 아래 단계 도달 시 값 바꾸기

3. 밖으로 나오며 값 설정 (복사본)

 깊이 중첩된 구조를 설계할 때 생각할 점

문제: depth가 깊어질 경우 객체의 를 기억하기 어려움

httpGet("http://my-blog.com/api/category/blog", function(blogCategory) { // 중첩 된 객체가 많아 사용을 위해 알아야 할 것이 너무 많음 renderCategory(nestedUpdate(blogCategory, ['posts', '12', 'author', 'name'], capitalize)); });
JavaScript
복사

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

 해결책: 추상화 벽에 함수를 만들고 의미 있는 이름 붙임

 개선점: 같은 작업을 하면서 알아야 할 데이터 구조 감소

 (복습) 추상화 벽

 개념: 세부 구현을 감춘 함수로 이루어진 계층

 장점: 쉽게 구현을 바꿀 수 있음, 코드를 읽고 쓰기 쉬어짐, 팀 간 조율이 줄어 듬, 주어진 문제에 집중

 추상화 벽에 함수를 만들고 의미 있는 이름을 붙여줌

  중첩 객체의 경로를 직접 전달 (기존 함수)
(...) nestedUpdate(blogCategory, // 변경할 객체 ['posts', '12', 'author', 'name'], // 경로 capitalize) // 할 일 (...)
JavaScript
복사
추상화 벽 사용 후
// 특징 1) 하는 일에 대해 명확한 이름 사용 // 특징 2) 세부 구조에 대해서는 콜백 함수에 맞김 // 특징 3) 데이터 구조에 대해서 추상화 벽 뒤로 숨김 function updatePostById(category, id, modifyPost) { return nestedUpdate(category, ['posts', id], modifyPost); } function updateAuthor(post, modifyUser) { return update(post, 'author', modifyUser); } function capitalizeName(user) { return update(user, 'name', capitalize); }
JavaScript
복사

[장점 1]: 기억해야 할 것이 4가지에서 3가지로 감소 (객체 키 4개 → 함수 3개)

[장점 2]: 동작의 이름이 있어 각 동작을 기억하기 쉬움

  중첩 객체의 경로를 직접 전달 (기존 함수)
nestedUpdate(blogCategory, ['posts', '12', 'author', 'name'], capitalize)
JavaScript
복사
추상화 벽 사용 후 (완성)
updatePostById(blogCategory, '12', function(post) { return updateAuthor(post, capitalizeUserName); });
JavaScript
복사

 [아쉬운 점]: 추상화벽 사용 시 장점도 있지만 결과물이 아름답지는 못함 (개인 생각)

앞에서 배운 고차 함수들

배열을 반복할 때 for loop 대신 사용하기

JavaScript 내장 고차함수는 배열을 효과적으로 다뤄 복잡한 계산에 유용
forEach, map, filter, reduce

중첩된 데이터를 효과적으로 다루기

깊이 중첩 된 데이터 변경을 위해서는 단계별 데이터를 모두 복사해야함
update(), nestedUpdate() 고차 함수 사용 시 특정값만 수술하듯이 변경 가능

카피-온-라이트 원칙 적용하기

카피-온-라이트 원칙 적용 시 함수 내 중복이 많아짐
withArrayCopy(), withObjectCopy() 사용시 카피-온-라이트 안에서 원하는 동작 실행 가능

try/catch 로깅 규칙을 코드화

wrapLogging은 함수의 리턴 값 그대로 리턴, 단 에러 발생 시 잡아서 로그 남김
wrapLogging은 어떤 함수에 다른 행동이 추가된 함수로 바꿔주는 좋은 예시

결론

SUMMARY - 중첩 된 데이터는 고차함수와 재귀를 사용해 쉽게 다룰 수 있음 - 깊이 중첩 된 데이터에 추상화 벽 적용 시 여러 장점이 있음