도서 리뷰 시리즈 - 함수형 자바스크립트 | 마이클 포거스
출처
『함수형 자바스크립트』 | 마이클 포거스 저 / 우정은 역 / 정경석 감수 | 한빛미디어 | 2014년 02월 01일
한 줄 리뷰
함수형 프로그래밍에 대해서 좀 더 깊이 있는 지식을 원하는 개발자에게 추천합니다.
1. 함수형 자바스크립트 소개
함수형 프로그래밍 시작하기
함수형 프로그래밍은 다음과 같은 한 문장으로 설명할 수 있다.
함수형 프로그래밍은 값을 추상화의 단위로 바꾸는 기능을 하며 결국 바뀐 값들로 소프트웨어 시스템이 만들어진다.
함수형 프로그래밍이 중요한 이유
객체 지향 프로그래밍
의 목표는 문제를 부분으로 잘게 나누는 것이라는 것을 동의할 것이다. 부품이 모여서 더 큰 부품이 될 수도 있고, 부품과 부품 집합을 이용해서 부품의 상호 동작과 값으로 시스템을 표현할 수 있다.
엄격한 함수형 프로그래밍
역시 문제를 함수라 불리는 부분으로 나눈다. 객체 지향에서는 문제를 명사
나 객체의 집합으로 나누는 반면 함수형에서는 같은 문제를 동사
나 함수의 그룹으로 나눈다. 객체 지향 프로그래밍과 마찬가지로 함수형 프로그래밍에서도 여러 함수를 붙이
거나 조립
해서 고수준 동작을 만든다.
함수형 부품으로 시스템을 만드는 방법 중에는 하나의 함수를 이용하거나 합성된 함수를 이용하여 값을 다른 값으로 변환
하는 방법이 있다.
객체 지향 스타일
을 엄격하게 준수하는 시스템에서 객체 간의 상호 작용이 발생하면 각 객체의 내부 값이 바뀌면서 전체 시스템의 상태가 바뀌는데 이때 많은 작은 변화가 융합되고, 잠재적으로 미묘한 변화가 일어날 수 있다. 이렇듯 서로 물고 물리는 상태 변화가 개념적으로 웹 변화
을 일으키게 되며 때로는 머리로 전체 과정을 이해하기 힘들 수 있다. 특히 작은 변경으로 큰 상태 변화가 발생할 수 있는 상황에서 새 객체를 추가하거나 새로운 시스템 기능을 추가해야 한다면 정말 난감하다.
이와 달리 함수형 프로그래밍에서는 관찰할 수 있는 상태 변화를 최소화하려고 애쓴다. 따라서 함수형 원칙을 고수하는 시스템에 새로운 기능을 추가할 때는 새로운 함수가 지역화되고 비파괴적인 데이터 전이 과정에서 어떻게 동작할 것인지를 파악하는 것이 핵심이다.
실용적인 함수형 프로그래밍은 어떤 시스템에서 상태 변화를 완전히 제거하는 것이 아니라 변이가 발생하는 지역을 가능한 최소화하는 것을 목표로 한다.
함수 - 추상화 단위
함수는 뷰에서 상세 구현을 숨김으로써 추상화를 달성할 수 있다. 함수를 추상화 단위로 이용하면 켄트 백의 테스트 주도 개발에서 제시한 주문을 따를 수 있다.
실행할 수 있게 한 다음, 올바로 동작하게 하고, 그 다음 빠르게 실행되도록 만들어라.
캡슐화와 은닉
수년간 객체 지향 프로로그램의 기초는 캡슐화라고 배워왔다. 객체 지향 프로그래밍에서 캡슐화란 일련의 정보와 그 정보를 조작할 수 있는 동작을 묶는 것을 가리킨다.
자바스크립트는 데이터와 관련 동작을 묶을 수 있는 객체 시스템을 제공한다. 그러나 때로는 특정 요소를 감출 목적으로 캡슐화를 사용하기도 하는 데 이를 데이터 은닉이라고 한다. 자바스크립트의 객체 시스템에서는 데이터 은닉을 직접적으로 제공하지 않으므로 보여주는 것처럼 클로저라는 것을 이용해 데이터를 감춘다.
함수형 방식에서는 클로저를 이용해서 대부분의 객체 지향 언어에서 제공하는 데이터 은닉 기능을 수행할 수 있다.
함수 - 동작 단위
데이터와 동작의 은닉은 함수를 추상화 단위로 사용하는 방법 중 한 가지일 뿐이다. 다른 방법으로는 기본 동작의 분산 단위를 저장하고 전달하는 방법을 제공할 수 있다. 배열 인덱싱 동작을 추상화하는 간단한 함수를 만들어 구현할 수 있다. 하지만 인덱스된 데이터 형식이 유효해야 함으로 형식을 판단하는 함수를 구현해야 한다.
자바스크립트에서 제공하는 또 다른 기본 동작 단위로 비교기(Comparator)
가 있다. 비교기는 두 값을 읽어 들여 첫 번째 값이 두번째 값보다 작으면 음수
, 크면 양수
, 같으면 0
을 반환하는 함수다.(Array.prototype.sort)
2. 일급 함수와 응용형 프로그래밍
함수를 일급 요소로 취급하는 것이 함수형 프로그래밍의 기본이다.
일급 함수의 특징
- 함수를 변수 또는 배열, 객체에 담을 수 있다.
- 함수를 리턴할 수 있다.(고차원 함수)
- 함수를 인자로 받을 수 있다.(고차원 함수)
컬렉션 중심 프로그래밍
컬렉션에 포함된 많은 아이템을 처리해야 할 때 함수형 프로그래밍의 진가를 발휘한다. 컬렉션 중심 프로그래밍의 핵심은 컬렉션을 처리하는 일반적인 처리 방법을 만들어서 재사용할 수 있는 포괄적인 함수 집합을 구축하는 데 있다.
3. 변수 스코프와 클로저
바인딩
바인딩은 자바스크립트의 이름에 값을 할당하는 행위를 가르킨다. 변수 할당, 함수 인자 사용, this 전달, 프로퍼티 할당 등의 과정에 해당한다.
클로저
클로저는 나중에 사용할 목적으로 정의된 스코프에 포함된 외부 바인딩을 캡쳐하는 함수다.
지역변수 캡쳐
const whatWasTheLocal = () => {
const captured = 'Oh hai'
return _ => `The local was: ${captured}`
}
const reportLocal = whatWasTheLocal()
reportLocal() // => 'The local was: Oh hai'
함수인자 캡쳐
const createScaleFunction = (factor) => {
return v => _.map(v, n => n * factor)
}
const scale10 = createScaleFunction(10)
scale10([1, 2, 3]) // => [10, 20, 30]
- 자유 변수 : 클로저가 캡쳐한 변수를 자유 변수라고 부른다.
- 셰도잉 : 항상 가장 가까운 변수 바인딩이 우선권을 갖아 동일한 변수명이 가려져 접근을 못하는 것을 말한다.
클로저 사용하기
클로저에서는 클로저가 만들어질 당시에 캡처한 값의 레퍼런스를 캡쳐한다. 새 변수를 만들어 isEven의 새 레퍼런드를 만들었으므로 클로저 isOdd에는 아무 영향이 없다.
const complement = pred => (...args) => !pred(...args)
let isEven = n => (n%2 === 0)
const isOdd = complement(isEven)
isOdd(2) // => false
isOdd(413) // => true
isEven = _ => false
isEven(10) // => false
isOdd(13) // => true
isOdd(12) // => false
추상화 도구 클로저
클로저는 비공개 접근을 제공할 뿐만 아니라 추상화 기법도 제공한다. 예를 들어 클로저를 이용해서 생성 시에 캡쳐되는 어떤 '설정'에 따라 다른 함수를 만들 수 있다.
const plucker = field => obj => (obj && obj[field])
const best = { title: 'Infinite Jest', author: 'DFW' }
const getTitle = plucker('title')
getTitle(best) // => 'Infinite Jest'
const books = [{title: 'Chthon'}, {stars: 5}, {title: 'Botchan'}]
const third = plucker(2)
third(books) // => {title: 'Botchan'}
_.filter(books, getTitle) // => [{title: 'Chthon'}, {title: 'Botchan'}]
4. 고차원 함수
이 장에서는 함수가 일급 요소라는 개념을 확장한다. 즉, 데이터 구조체 내부에 함수를 저장할 수 있을 뿐만 아니라 데이터로 함수를 전달할 수 있으며 함수로부터 반환 될 수 있음을 설명한다. 고차 함수는 상당하 구체적으로 정의할 수 있다.
- 고차원 함수는 일급이다.
- 함수를 인자를 받는다.
- 함수를 결과로 반환한다.
값 대신 함수를 사용하라
우선 가장 간단한 함수인 repeat부터 살펴보자. repeat는 횟수와 값을 받아서 중복된 값을 횟수만큼 갖는 배열을 만드는 함수이다.
const repeat = (times, value) => {
return _.map(_.range(times), () => value)
}
독립적으로 동작하는 repeat를 구현하는 것도 괜찮지만 반복성
이라는 일반성을 가지도록 repeat를 구현할 수 있다면 더 좋을 것이다. 즉, 어떤 숫자만큼 값을 반복하는 것도 괜찮지만 어떤 동작을 특정 횟수만큼 반복한다면 더 좋다.
const repeatedly = (times, fun) => _.map(_.range(times), fun)
repeatedly(3, () => Math.floor(Math.random() * 10) + 1)
repeatedly 함수는 함수형 스타일로 생각하면 어떤 효과를 거둘 수 있는지 잘 보여 준다. 값 대신 함수를 사용함으로써 반복성
이라는 새로운 가능성이 열렸다.
다른 함수를 반환하는 함수
상수를 반환하는 함수는 대부분의 함수형 프로그래밍에서 등장하는 유용한 기능이며 줄여서 k라고도 부른다.
const always = (value) => () => value
클로저의 일부 기능을 설명할 때 always를 유용하게 사용할 수 있다.
- 클로저는 한 개의 값이나 레퍼런스를 캡처한 다음에 항상 같은 값을 반환할 것이다.
const f = always(()=>{})
f() === f() // => true
- 새로운 클로저는 매번 다른 값을 캡처한다.
const g = always(()=>{})
g() === f() // => false
invoker라는 함수를 살펴보자. invoker함수는 메서드를 인자로 받으며 함수를 반환한다. 반환되는 함수는 주어진 객체에 인자로 받은 메서드를 호출한다.
const invoker = (name, method) => {
return (target, ...args) => {
if (!existy(target)) fail('Must provide a target')
const targetMethod = target[name]
return doWhen((existy(targetMethod) && method === targetMethod), () => {
return targetMethod.apply(target, args)
})
}
}
const rev = invoker('reverse', Array.prototype.reverse)
_.map([1, 2, 3], rev) // => [3, 2, 1]
객체에 특정 메서드를 직접 호출할 수도 있지만 함수형 스타일에서는 메서드를 호출할 대상을 인자로 받는 형식
을 선호한다. invoker는 상수를 반환하지 않고 기존 호출값에 따라 어떤 특별한 동작을 수행한다.
값을 바꿀 때 주의를 기울이자
자신이 반환할 값과 관련된 인자만을 활용하는 함수를 가리쳐 참조 투명성(Referential Transparency)이 있다고 표현한다.
참조 투명성은 함수가 기대하는 모든 값으로 함수 호출을 대체할 수 있는 함수라는 단순한 의미를 가진다. 내부 코드를 변경하는 클로저를 사용할 때 반환되는 값은 해당 클로저가 몇 번 호출되었느냐에 좌우되는 것이르므로 호출할 때마다 우리가 인위적으로 결과를 조작할 필요는 없다.
값이 존재하지 않는 상황을 지켜주는 함수: fnull
다음 코드처럼 곱셈 연산을 수행할 숫자 배열이 있다고 가정하자. 물론 숫자에 null을 곱하면 제대로 된 결과가 나올 리 없다.
_.reduce([1,2,3,null,5], (total, n) => total * n)
// => 0
문제를 일으키는 다른 상황으로 설정 객체를 입력으로 받고 입력값에 따라 어떤 동작을 수행하는 함수가 있다.
const doSomething = (config) => {
const lookup = defaults({ critical: 108 })
return lookup(config, 'critical')
}
doSomething({ whoCares: 42, critical: null })
위 두 가지 경우에 fnull을 적용하면 문제를 쉽게 해결할 수 있다. fnull이 반환하는 함수의 인자가 null이나 undefined면 이들을 기본값인자로 대치한다. fnull 구현 코드는 지금까지 보여 준 고차원 함수 중 가장 복잡한 편이다.
const fnull = (fun, ...defaults) => {
return (...args) => {
const newArgs = _.map(args, (e, i) => {
return existy(e) ? e : defaults[i]
})
return fun(...newArgs)
}
}
5. 함수로 함수 만들기
레고 블록으로 다양한 물건을 만들듯이 여러 함수를 연결해서 더 풍부한 기능의 함수를 조립하는 다양한 방법을 설명한다.
함수 조립의 핵심
한 개 이상의 함수를 이용해서 undefined가 아닌 다른 값을 반환하는 함수를 찾을 때까지 메서드 호출 시도를 반복해야 한다. 바로 다음에 소개할 dispatch는 지금까지 설명한 동작을 수행하는 함수이다.
const dispatch = (...fns) => {
const size = fns.length
return (target, ..args) => {
let ret = undefined
for(let i = 0; i < size; i++) {
const fn = fns[i]
ret = fn.apply(fn, construct(target, args))
if (existy(ret)) {
return ret
}
}
return ret
}
}
복잡해 보이긴 하지만 dispatch는 자바스크립트 함수의 다형성 정의
를 만족하는 함수다.
다양한 자료형에 속하는 것이 허가되는 성질을 가리킨다. 반댓말은 단형성으로 한가지 형태만 가지는 성질을 가리킨다. 다형성 체계를 가진 언어에서는, 범용 메소르 이름을 정의하여 형태에 따라 각각 적절한 변환 방식을 정의해둠으로써 객체의 종류와 상관없는 추상도가 높은 변한 형식을 구현할 수 있다.
dispatch는 구체적인 메서드 실행을 다른 함수에 위임한다. 예를 들면 언더스코어의 많은 함수 구현에서 다음과 같은 패턴이 반복되는 것을 발경할 수 있다.
- 대상이 존재하는지 확인한다.
- 네이티브 버전이 있는지 확인하여 있다면 그것을 사용한다.
- 네이티브 버전이 없다면 필요한 동작을 수행할 태스크를 구현한다.
- 가능하면 형식이 정해진 태스크를 만든다.
- 가능하면 인자가 명확한 태스크를 만든다.
- 가능하면 인자의 개수가 명확한 태스크를 만든다.
언더스코어의 _.map 함수는 지금까지 설명한 패턴을 명확하게 보여 준다.
_.map = _.collect = (obj, iterator, context) => {
const result = []
if (obj === null) return results
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context)
each(obj, (value, index, list) {
results[result.length] = iterator.call(context, value, index, list)
})
return results
}
문자열 형식을 문자열로 표현하는 함수를 만들어야 한다고 가정하자. dispatch를 활용하면 다음처럼 깔끔하게 원하는 기능을 구현할 수 있다.
const str = dispatch(
invoker('toString', Array.prototype.toString),
invoker('toString', String.prototype.toString)
)
str('a') // => a
str(_.range(10)) // => 0,1,2,3,4,5,6,7,8,9
stringReverse라는 함수를 이용해서 dispatch의 규칙에 관여할 수 있다.
const stringReverse = (s) => {
if (_.isString(s)) return undefined
return s.split('').reverse().join('')
}
stringReverse('abc') // => cba
stringReverse(1) // => undefined
const rev = dispatch(invoker('reverse', Array.prototype.reverse), stringReverse)
rev([1, 2, 3]) // => [3, 2, 1]
rev('abc') // => cba
다음과 같이 수동적으로 명령을 분류하는 switch문들 dispatch로 대체할 수 있다.
// AS-IS
const performCommandHardcoded = (command) => {
let result
switch (command.type) { ... }
return result
}
const isa = (type, action) => (obj) => {
if (type === obj.type) {
return action(obj)
}
}
// TO-BE
const performCommand = dispatch(
isa('notify', (obj) => notify(obj.message)),
isa('join', (obj) => changeView(obj.target))
)
커링
각각의 논리적 인자에 대응하는 새로운 함수를 반환하는 함수를 커리함수라고 한다.
const rightAwayInvoker = (method, target, ...args) => {
return method.apply(target, ...args)
}
rightAwayInvoker(Array.prototype.reverse, [1, 2, 3]) // => [3, 2, 1]
자동 커링 파라미터
자바스크립트는 인자의 개수와 부가적인 특화 인자의 개수가 정해져 있을 때가 많다. 커링이 발생하는 과정을 명시적으로 보여주고 한개의 인자만 받도록 강제할 수 있다.
const curry = (fn) => (arg) => fn(arg)
parseInt('11') // => 11
parseInt('11', 2) // => 3
['11', '11', '11'].map(parseInt)
// => [11, NaN, 3]
['11', '11', '11'].map(curry(parseInt))
// => [11, 11, 11]
부분 적용
부분적용은 부분적으로 실행을 마친 다음에 나머지 인자와 함께 즉시 실행한 상태가 되는 함수다.
const partial = (fn, ...arg1) => (...arg2) => fn(...arg1, ...arg2)
const sum = (a, b) => a + b
const over10Part1 = partial(sum, 10)
over10Part1(5) // => 15
부분 적용 사례 : 선행조건, 후행조건
- 선행조건 : 호출하는 함수에서 보장하는 조건
- 후행조건 : 선행조건이 지켜졌다는 가정 하에 함수 호출 결과를 보장하는 조건 선행조건과 후행조건의 관계를 '함수가 처리할 수 있는 데이터를 제공했을 때 함수는 특정 기준을 만족하는 결과를 반환할 것이가'로 설명할 수 있다.
validator('arg must be a map', aMap)(42)
const zero = validator('cannot be zero', n => 0 === n)
const number = validator('arg must be a number', _.isNumber)
const sqr = n => {
if (!number(n)) throw new Error(number.message)
if (zero(n)) throw new Error(zero.message)
return n * n
}
sqr(10) // => 100
sqr(0) // => Error: cannot be zero
sqr('') // => Error: arg must be a number
위 구현도 상당히 좋은 코드이지만 계산 로직과는 독립적으로 선행조건을 추가하도록 부분 적용을 이용할 수 있다.
const condition = (...validators) => {
return (fn, arg) => {
const errors = mapcat(isValid => isValid(arg) ? [] : [isValid.message], validators)
if (!_.isEmpty(errors)) {
throw new Error(errors.join(', '))
}
return fn(arg)
}
}
const sqrPre = condition(
validator('cannot be zero', complement(n => 0 === n)),
validator('arg must be a number', _.isNumber)
)
sqrPre(_.identity, 10) // => 10
sqrPre(_.identity, '') // => Error: arg must be a number
sqrPre(_.identity, 0) // => Error: arg must not be zero
partial를 적용하면 _.identity를 부분적용할 수 있다.
const validateCommand = condition(
validator('cannot be zero', complement(n => 0 === n)),
validator('arg must be a number', _.isNumber)
)
const createCommand = partial(validateCommand, _.identity)
createCommand(10) // => 10
createCommand('') // => Error: arg must be a number
createCommand(0) // => Error: arg must not be zero
함수의 끝을 서로 연결하는 함수 조립 방법
한쪽으로 데이터를 넣으면 반대편으로 완전히 새로운 데이터가 나올 수 있도록 함수들이 파이프라인을 이루고 있다면 가장 이상적인 함수형 프로그램이라고 할 수 있다.
!_.isString(name) // _.isString과 !사이에 파이프라인
함수형 조립은 여러 함수가 수행하는 데이터 변경을 모아서 데이터 체인을 이용해서 새로운 함수를 만든다.
const isntString = str => !.isString(str)
compose함수를 이용해서 함수를 조립하는 방법도 있다.
const isntString = _.compose(x => !x, _.isString)
! 연산자를 캡슐화할 수 있다.
const not = x => !x
const isntString = _.compose(not, _.isString)
6. 재귀
6.1 자신을 호출하는 함수
우선은 재귀를 이해하는 것이 왜 중요한지 세 가지 이유를 알아보자
- 재귀 솔루션은 일반적인 문제를 하위의 작은 문제로 분리한 다음에 하위 문제를 하나의 추상화로 만들어서 문제를 해결한다.
- 재귀는 변이 상태를 숨길 수 있다.
- 재귀로 게으름(laziness) 그리고 무한히 큰 구조로 구현할 수 있다.
재귀 함수를 만들 때는 다음 규칙을 지키는 것이 좋다고 알려져 있다(토우레즈키(Touretzky) 1990).
- 언제 멈출지 알아야 한다.
- 한 단계에서 무엇을 실행할지 결정한다.
- 문제를 더 작은 문제나 아니면 한 단계로 풀 수 있는 문제로 작게 분리한다.
6.2 상호 재귀 함수
서로를 호출하는 두 개 이상의 함수를 상호 재귀라고 한다. 다음에 소개할 짝수인지 홀수인지 검사하는 찬반형 함수는 서로를 호출하는 간단한 상호 재귀 함수 예제다.
const evenSteven = n => n === 0 ?
true :
oddJohn(Math.abs(n) - 1)
const oddJohn = n => n === 0 ?
false :
evenSteven(Math.abs(n) - 1)
evenSteven(4) // true
oddJohn(11) // true
- 6.2.1 재귀를 이용한 깊은 복제: 깊은 방식으로 객체를 복제할 때 재귀를 이용하면 효율적인 작업할 수 있다.
- 6.2.2 중첩된 배열 탐색: 중첩된 배열을 탐색하는 상황에서 유용하다.
6.3 너무 깊은 재귀!
자바스크립트 구현에서는 재귀 호출의 횟수에 제한이 있기 때문에 너무 많은 재귀 호출이 이루어지면 쉽게 스택 깨짐이 발생한다.
여기서는 트램펄린(Trampoline)이라 불리는 구조를 이용해서 스택 깨짐을 피하는 방법을 설명한다. 중첩 호출을 평탄화(flatten) 시킨 호출로 바꾸는 것이 트램펄린의 기본 원리다.
const partial = (fn, ...arg1) => (...arg2) => fn(...arg1, ...arg2)
const evenSteven = n => n === 0 ? true : partial(oddJohn, Math.abs(n) - 1)
const oddJohn = n => n === 0 ? false : partial(evenSteven, Math.abs(n) - 1)
const trampoline = (fun, ...args) => {
let result = fun(...args)
while (typeof result === "function") {
result = result()
}
return result
}
trampoline(oddJohn, 1000) // false
const evenSteven = n => n === 0 ?
true :
() => oddJohn(Math.abs(n) - 1)
const oddJohn = n => n === 0 ?
false :
() => evenSteven(Math.abs(n) - 1)
const trampoline = (fun, arg) => {
let result = fun(arg)
while (typeof result === "function") {
result = result()
}
return result
}
trampoline(evenSteven, 100000)
7. 순수성, 불변성, 변경 정책
함수형 프로그래밍은 단지 함수를 다루는 기법이 아니다. 함수형 프로그래밍은 소프트웨어 개발의 복잡성을 최소화하는 개발 방식을 추구한다. 프로그램에서 발생하는 상태 변화를 최소화하거나 아예 없애는 것은 복잡성을 줄일 수 있는 방법 중 하나다.
순수성
_.map과 같이 동작하는 함수는 '순수하다(pure)'라고 표현한다. 순수한 함수에는 다음과 같은 특징이 있다.
- 오직 인자만을 이용해서 계산 결과가 만들어진다.
- 외적 요소에 영향을 받는 데이터에 의존하지 않는다.
- 자신의 바디 외부의 상태를 변화시킬 수 없는 구조다.
순수성과 비순수성 구별하기
작은 범위에서 임의의 숫자를 반환하는 함수가 필요하다고 가정하자. 이 함수는 결괏값을 예측할 수 없으므로 명확한 규격명세를 통과 할 수 없다.
const randString = len => repeatedly(len, partial(rand, 10)).join('')
// => 2758483948
randString의 경우 문자를 생성하는 파트와 문자를 연결하는 파트로 구성되어 있다. 다음처음 두 개의 함수를 이용해서 순수한 부분과 비순수한 부분으로 나눈다.
const generateRandomChar = _ => rand(26).toString(36)
const generateString = (charGen, len) => repeatedly(len, charGen).join('')
generateString(generateRandomChar, 20)
partial를 이용해서 두 함수를 조립해 randomString을 만들 수 있다.
const randomString = partial(generateString, generateRandomChar)
이제 별도로 캡슐화한 순수한 부분만 독립적으로 테스트할 수 있다.
순수성과 멱등의 관계
멱등이란 어떤 동작을 여러번 실행한 결과가 한 번 실행한 결과가 같은 상황을 가리킨다.
변화 제어 정책
합리적으로 생각하면 모든 불필요한 변이를 제거할 수 있겠지만 머지않아 어떤 상태를 바꾸어야만 하는 상황이 반드시 올 것이다. 변이할 수 있는 객체를 주고받으면 객체를 바꿨을 때 전체 프로그램에 영향을 미칠 수 있다.
변화가 일어나는 것을 고립시킴으로써 변화의 범위를 제어할 수 있다. 즉, 임의의 객체를 직접 고치는 것보다는 컨테이너에 객체를 담아서 객체가 아닌 컨테이너를 고치는 방법이 바람직하다.
const container = contain({name: 'Lemonjon'})
container.set({name: 'Lemongrab'})
반면 다음에 컨테이너를 사용하지 않는 예제다.
const being = {name: 'Lemonjon'}
being.name = 'Lemongrab'
간접 접근 방법이 직접 변이 기법에 비해 더 큰 이득을 주는 것은 아니다. 하지만 간접 접근 개념을 좀 더 확장해서 함수 호출의 결과에서만 값이 바뀌도록 제한할 수 있다. 그리고 간접적인 함수를 추가함으로써 예측할 수 있는 함수에 의해 값이 바뀐다.
9. 클래스를 이용하지 않는 프로그래밍
믹스인
믹스인은 기능에 기존 기능 또는 새로운 기능을 섞어서 확장하는 기법이다. 함수를 조립해서 새로운 함수를 만든다는 개념과 일맥상통한다.
인자로 받은 객체를 문자열 표현으로 반환하는 polyToString이라는 함수가 있다고 가정하자.
const polyToString = obj => {
if (obj instanceof String) {
return obj
} else {
if (obj instanceof Array) {
return stringifyArray(Obj)
} else {
return obj.toString()
}
}
}
객체의 종류를 확인하는 여러 if문을 이용해서 polyToString을 구현할 수 있다. 그러나 더 다양한 객체를 처리하려면 polyToString 바디에 if문을 새로 추가해야 하는 데, 이는 바람직한 접근 방식이 아니다.
이번에는 dispatch를 이용해서 객체의 종류를 확인했다. 객체의 종류를 확인하는 기능을 함수로 추상화했으므로 나중에 쉽게 확장할 수 있는 가능성이 열렸다.
const polyToString = dispatch(
s => (_.isString(s) ? s : undefined),
s => (_.isArray(s) ? stringifyArray(s) : undefined),
s => s.toString()
)