[TypeScript] 렌즈 라이브러리를 사용하여 불변 객체의 필드 여러 군데 한꺼번에 업데이트 하기
이현수 책임
중첩된 불변 객체의 필드 여러 군데를 한꺼번에 업데이트 하려면?
지난 번에는 타입스크립트에서 중첩된 불변 객체의 필드 업데이트를 편리하게 해주는 렌즈 라이브러리를 소개했습니다.
이번 시간에는 필드 한 군데만 아니라 불변 객체의 여러 필드를 한꺼번에 업데이트하는 방법을 알려드립니다.
사전 지식
이 글을 보시기 전에 아래 내용에 대해서 알고 있는지 확인하십시오.
- 타입스크립트
- 불변 객체(Immutable Object)
- 렌즈(Lens, of Optics)
복습: 필드 하나 업데이트 하기
지난 시간에 배운 것을 복습해보겠습니다.
const prev_state: ST = {
level1: {
level2: {
level3: {
value: 'value'
}
}
}
}
참고: 실습 예제에서 다루는 최상위 불변 객체의 타입을 ST 라고 하겠습니다.
위와 같이 타입 ST
의 객체가 있을 때,
const new_state = {
...prev_state,
level1: {
...prev_state.level1,
level2: {
...prev_state.level1.level2,
level3: {
...prev_state.level1.level2.level3,
value: 'other_value'
}
}
}
}
이렇게 불편하게 안 해도 되고,
import { lens } from 'lens.ts'
const new_state = lens<ST>().level1.level2.level3.value.set('other_value')(prev_state)
이렇게 하면 된다고 배웠지요?
렌즈 라이브러리가 있으면 필드 하나 업데이트 하는 것은 이렇게 쉬웠습니다.
그러면 한꺼번에 서로 다른 여러 필드를 업데이트 하려면 어떻게 해야 할까요?
도전: 여러 군데 필드 변경하기
어떤 객체에서 다양한 위치의 여러 군데 필드를 업데이트 해보겠습니다.
const prev_state: ST = {
target1: 'target1',
target2: false,
level1: {
level2: {
target3: 123
}
}
}
target1
, target2
, target3
의 값을 임의의 다른 값으로 바꿔보겠습니다. 어떻게 해야할까요?
일단 렌즈 라이브러리 자체에는 그런 기능이 없습니다.
그래도 일단 해보겠습니다.
const target1_updated = lens<ST>().target1.set('another_target1')(prev_state)
const target2_updated = lens<ST>().target2.set(true)(target1_updated)
const target3_updated = lens<ST>().level1.level2.target3.set(456)(target2_updated)
// target3_updated
const new_state: ST = {
target1: 'another_target1',
target2: true,
level1: {
level2: {
target3: 456
}
}
}
자, 이렇게 하면 될까요?
문제점
원하는 결과는 얻을 수 있지만…
- 한꺼번에 하라고 했는데, 여러 번 끊어서 업데이트 했다.
- 중간 참조(target1_updated, target2_updated, …)를 만들어야 한다.
- 안 예쁘다
그렇습니다. 안 예쁜 게 가장 큰 문제입니다.
개선: reduce()를 응용해봅시다.
위에서 prev_state
와 중간 결과 target1_updated
, target2_updated
, …의 타입은 모두 ST
로 같습니다.
뭔가 패턴이 보이죠? 어떤 타입의 출력을 다시 입력으로 집어넣어서 같은 타입의 새 출력을 만듭니다.
접기(fold) 혹은 축소(reduce) 연산을 적용할 수 있습니다.
const new_state = [
lens<ST>().target1.set('another_target1'),
lens<ST>().target2.set(true),
lens<ST>().level1.level2.target3.set(456)
].reduce((state, apply) => apply(state), prev_state)
위에서 세 번에 걸쳐서 수행한 업데이트를 합성해서 하나로 만든 것입니다.
자세히 보시면, 배열에 넣은 lens<ST>(). ... .set(xxx)
부분에서 마지막에 state
를 apply
하지 않은 것을 알 수 있습니다.
apply
라 함은함수(xxx)
와 같이 매개변수를 넘겼다는 것입니다.그냥 마 함수 호출
즉 배열에 있는 값들은 모두 ST => ST
타입의 함수들입니다. 업데이트를 수행한 후에 ST
타입의 결과를 반환하는 함수들입니다.
reduce()
를 통해 이 함수들을 여러 군데의 필드를 업데이트하고 새 객체를 반환하는 하나의 함수로 합성했습니다.
네, 정확히 말하자면 함수로 합성한 게 아니라 바로 결과를 뽑아낸 것이고, 진짜로 합성된 함수를 얻으려면 아래와 같이 하면 됩니다. (따질까봐 다시 설명-_-)
function update_target1_target2_target3(
state: ST,
target1: string,
target2: boolean,
target3: number
): ST {
return [
lens<ST>().target1.set(target1),
lens<ST>().target2.set(target2),
lens<ST>().level1.level2.target3.set(target3)
].reduce((prev_state, apply) => apply(prev_state), state)
}
심화: 성능 고려
위에서 lens<ST>(). ...
이런 식으로 인라인으로 렌즈 함수를 사용했지만, 사실 아래와 같이 하는 게 권장됩니다.
const stLens = lens<ST>()
const target1Lens = stLens.target1
const target2Lens = stLens.target2
const target3Lens = stLens.level1.level2.target3
...
function update_target1_target2_target3(
state: ST,
target1: string,
target2: boolean,
target3: number
): ST {
return [
target1Lens.set(target1),
target2Lens.set(target2),
target3Lens.set(target3)
].reduce((prev_state, apply) => apply(prev_state), state)
}
성능 때문에 그렇습니다만(매번 함수 생성하지 않도록), 무시할 만하면 그냥 인라인으로 써도 될 것 같습니다.
결론
필드 여러 군데 업데이트도 문제가 없습니다.
써보세요.
감사합니다.
내용이 유익했다면 추천과 공유하기 부탁드립니다.