이전내용 (chap.14)
문제: 객체의 값을 바꿔야 함 → 여러 코드에서 중복 다수 발생
해결책: [리팩터링] 조회, 변경, 설정을 update()로 교체하기
function update(object, key, modify) {
var value = object[key]; // 조회
var newValue = modify(value); // 변경 (콜백 함수 전달)
var newObject = objectSet(object, key, newValue); // 설정
return newObject; // 리턴
}
JavaScript
복사
function incrementField(item, field) {
var value = item[field]; // 조회
var newValue = value + 1; // 변경 (새로운 값 선언)
var newItem = objectSet(item, field, newValue); // 설정
return newItem; // 리턴
}
JavaScript
복사
function incrementField(item, field) {
// 중복 작업이 제거되어 간소화 됨
return update(item, field, function(value) {
return value + 1;
});
}
JavaScript
복사
중첩된 update() 시각화하기
문제: 도출한 update()는 중첩된 객체에는 적용 불가
해결책: 현재 코드 시각화 후 리팩터링 진행
// 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)
// [코드의 냄새] 함수 이름에 있는 암묵적 인자
function incrementSize(item) {
return update(item, 'options', function(options) {
return update(options, 'size', increment);
});
}
JavaScript
복사
// 명시적 인자로 받기 위해 인자 option 추가
// 함수 이름도 인자에 맞게 수정
function incrementOption(item, option) {
return update(item, 'options', function(options) {
// 암묵적 인자 때문에 하드코딩 되어 있던 코드 수정
return update(options, option, increment);
});
}
JavaScript
복사
2. 두 번째 리팩터링 (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
복사
// 하드 코딩 된 increment 대신 함수를 명시적 인자로 받도록 수정
// 함수 이름도 인자에 맞게 수정
function updateOption(item, option, modify) {
return update(item, 'options', function(options) {
// 암묵적 인자 때문에 하드코딩 되어 있던 코드 수정
return update(options, option, modify);
});
}
JavaScript
복사
update2() 도출하기
문제: 리팩터링 후 새로운 암묵적 인자 발생
해결책: [리팩터링] 암묵적 인자를 드러내기 필요
function updateOption(item, option, modify) {
return update(item, 'options', function(options) {
return update(options, option, modify);
});
}
JavaScript
복사
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() 작성
// [옵션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() 작성
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()을 불러주는 구조
// 숫자만큼 키를 인자로 받음
// 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
복사
// 숫자만큼 키를 인자로 받음
// update()내 숫자가 하나 작은 updateX-1()이 있음
function update3(obj, key1, key2, key3, modify) {
return update(object, key1, function(value1) {
return update2(value1, key2, key3, modify);
});
}
JavaScript
복사
// 숫자만큼 키를 인자로 받음
// 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와 키를 직접 전달
// 키의 개수와 순서가 중요하게 됨
function updateX(obj, depth, key1, key2, key3, modify) {
return update(obj, key1, function(value1) {
return updateX(value1, depth-1, key2, key3, modify);
});
}
JavaScript
복사
// 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. 종료 조건 추가
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
복사
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
복사
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
- 중첩 된 데이터는 고차함수와 재귀를 사용해 쉽게 다룰 수 있음
- 깊이 중첩 된 데이터에 추상화 벽 적용 시 여러 장점이 있음