쏙쏙 들어오는 함수형 코딩 책 리뷰
"이렇게 복잡한 것이 필요한 것인가요?"
"저는 그냥 브라우저에서 작동하는 GUI를 만들려고 하는 것인데요"
웹 어플리케이션을 만들기 위해서는 다양한 복잡성을 다뤄야 합니다.
그럼 복잡성은 필연적일까요? 아닙니다.
복잡성은 바꾸지 않으려고 하는 선택들로부터 생깁니다.
그래서 우리에게는 복잡성을 다룰 수 있는 좋은 프로그래밍 기술이 필요합니다.
부수 효과와 순수 함수는 다음과 같다.
세금을 계산하여 UI를 업데이트하는 예시를 통해, 부수 효과와 순수 함수의 차이를 알아보자
let total = 10;
function updateTaxDom() {
setTaxDom(total * 0.1);
}
문제가 없어보이는 코드지만, 위의 코드는 재사용과 테스트가 힘들다.
위의 코드를 개선해보자
let total = 10;
function updateTaxDom() {
setTaxDom(calcTax(total));
}
function calcTax(amount) {
return amount * 0.1;
}
이제 위에서 말한 문제점은 calcTax
함수에서 해결되었다.
무슨 문제가 존재하였으며, 어떻게 해결되었을까?
테스트와 재사용성은 입출력과 관련이 있다.
위의 코드를 다시 살펴보면 무엇이 암묵적이고 명시적인지 확인할 수 있다.
function updateTaxDom() {
setTaxDom(calcTax(total);); // 암묵적 출력(부수효과 DOM 업데이트)
}
function calcTax(amount) { // 명시적 입력(인자)
return amount * 0.1 // 명시적 출력(리턴값)
}
위에서 다음을 알아보았다.
이렇게만 알아본다면 부수 효과는 나쁜것이라고 생각할 수 있지만, 부수효과는 프로그래밍의 본질이다
부수효과 예시는 다음의 것들이 있다.
따라서, 부수 효과 함수와 순수 함수를 잘 구분하고 분리하는 것이 중요하다.
액션, 계산, 데이터의 정의는 다음과 같다
액션, 계산, 데이터의 구분은 코드 뿐만 아니라, 일상에서 자주 하는 '장보기'에도 적용할 수 있다.
일반적인 장보기 과정을 살펴보면 다음과 같다.
위의 과정의 각각은 호출 횟수와 실행 시점에 의존하므로 모두 '액션'이다.
이와 같은 액션들에는 사실 데이터와 계산이 때로 우리 머리속에서 자동으로 일어난다.
이처럼, 어떤 행위에는 액션, 계산, 데이터를 구분하여 찾을 수 있고 풍부한 모델을 만들 수 있다.
또한, 부수 효과와 순수 함수에서 알아본 것 처럼 액션보다는 계산이 많을수록 테스트, 재사용, 유지보수가 용이하다.
다음의 절차를 통해 부수 효과를 일으키는 액션을 순수 함수인 계산으로 변경할 수 있다.
장바구니 기능을 하는 함수 예시를 통해 이를 알아보자
const shopping_cart = [];
let shopping_cart_total = 0;
function calc_cart_total() { // 액션
shopping_cart_total = 0;
for(let i = 0; i < shopping_cart.length; i++) {
const item = shopping_cart[i];
shopping_cart_total += item.price;
}
set_cart_total_dom(); // 액션
update_shipping_icons(); // 액션
update_tax_dom(); // 액션
}
위의 코드에서 먼저 액션에서 서브루틴을 추출하면 다음과 같다.
const shopping_cart = [];
let shopping_cart_total = 0;
function calc_cart_total() {
calc_total(); // 액션
set_cart_total_dom(); // 액션
update_shipping_icons(); // 액션
update_tax_dom(); // 액션
}
function calc_total() {
shopping_cart_total = 0; // 암묵적 출력 (shopping_cart_total)
for(let i = 0; i < shopping_cart.length; i++) { // 암묵적 입력 (shopping_cart)
const item = shopping_cart[i];
shopping_cart_total += item.price; // 암묵적 출력 (shopping_cart_total)
}
}
액션에서 서브루틴인 calc_total
을 추출했지만, 해당 함수는 아직 계산이 아니라 액션이다
그 이유는, 함수에 암묵적 입력과 암묵적 출력이 존재하여 코드 외부에 영향을 주기 때문이다.
따라서 서브루틴 함수를 액션에서 계산으로 바꾸면 다음과 같다.
const shopping_cart = [];
let shopping_cart_total = 0;
function calc_cart_total() {
shooping_cart_total = calc_total(shopping_cart); // 계산
set_cart_total_dom(); // 액션
update_shipping_icons(); // 액션
update_tax_dom(); // 액션
}
function calc_total(cart) { // 명시적 입력 (cart)
let total = 0; // 지역변수 사용
for(let i = 0; i < cart.length; i++) {
const item = cart[i];
total += item.price;
}
return total; // 명시적 출력 (total)
}
이처럼 다음과 같이 부수 효과를 일으키는 액션을 순수 함수인 계산으로 변경할 수 있다
계층형 설계는 소프트웨어를 계층으로 구성하는 기술이다.
함수의 계층형 설계는 각 계층의 함수를 바로 아래 계층에 있는 함수를 이용해 만드는 것이다.
함수의 각 계층에는 여러 목적이 존재할 수 있다.
계층형 설계 패턴과 도구는 다음과 같다.
계층형 설계를 통해 테스트, 재사용, 유지보수 등의 비기능적 요구사항을 만족할 수 있다.
언어에서 일급에 대한 정의는 다음과 같다
자바스크립트에서 일급과 일급이 아닌 것은 다음과 같다
일급인 것을 통해 코드를 다음과 같이 리팩토링 하여 재사용의 이점을 얻을 수 있다.
먼저, 함수 이름에 있는 암묵적 인자의 예시는 다음과 같다.
function setPriceByName(cart, name, price) {
const item = cart(name);
const newItem = objectSet(item, 'price', price);
const newCart = objectSet(cart, name, newItem);
return newCart;
}
function setQuantityByName(cart, name, quantity) {
const item = cart(name);
const newItem = objectSet(item, 'quantity', price);
const newCart = objectSet(cart, name, newItem);
return newCart;
}
function setTaxByName(cart, name, tax) {
const item = cart(name);
const newItem = objectSet(item, 'tax', price);
const newCart = objectSet(cart, name, newItem);
return newCart;
}
함수의 본문은 동일하지만, 변경하려는 값에 따라 함수가 존재한다.
이때, 변경하려는 값이 함수 이름에 존재하며 해당 값은 암묵적 인자이다.
만약 cart의 값을 변경하는 요구사항이 많아진다면, 함수의 개수도 그에 비례하여 증가한다.
따라서, 함수 이름에 존재하는 암묵적 인자를 일급 값인 함수 인자로 바꾸면 다음과 같다
function setFieldByName(cart, name, field, value) {
const item = cart(name);
const newItem = objectSet(item, 'field', value);
const newCart = objectSet(cart, name, newItem);
return newCart;
}
setFieldByName(cart, 'shoe', 'price', 13);
setFieldByName(cart, 'shoe', 'quantity', 3);
setFieldByName(cart, 'shoe', 'tax', 3.45);
이제 더 이상 cart의 값을 변경하는 요구사항이 많아져도, 함수는 재사용이 가능하다.
.
다음으로, 함수를 인자로 전달하여 고차함수 만드는 예시는 다음과 같다.
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
try {
fetchProduct(productId);
} catch (error) {
logToSnapErrors(error);
}
중요한 코드에 try/catch로 감싸서 로그를 남긴다면, try/catch가 계속해서 중복된다.
따라서, 함수를 인자로 전달하여 고차함수를 만들면 다음과 같다.
// v1
function withLogging(f) {
return {
try {
f();
} catch(error) {
logToSnapErrors(error);
}
}
}
withLogging(function() { saveUserData(user) });
withLogging(function() { fetchProduct(productId) });
함수를 인자로 넘겨서 중복을 없앴지만, 여전히 함수를 인자로 넘기는 부분에 중복이 존재한다.
따라서 해당 중복을 제거하기 위해 다음과 같이 처리할 수 있다.
// v2
function wrapLogging(f) {
return function(arg) {
try {
f(arg)
} catch(error) {
logToSnapErrors(error);
}
}
}
const saveUserDateWithLogging = wrapLogging(saveUserData);
saveUserDateWithLogging(user);
const fetchProductWithLogging = wrapLogging(fetchProduct);
fetchProductWithLogging(productId);
타임라인이란 시간에 따라 코드의 실행이 진행되는 것을 의미한다.
모든 프로그래밍 언어는 각각 암묵적으로 시간에 대한 모델을 가지고 있다.
시간 모델이란, 코드의 실행 순서와 반복에 대한 방식이며, 여러 방법이 존재한다.
하지만, 암묵적 시간 모델이란 실행 순서와 반복을 항상 동일하게 보장하지 않는다는 것이며, 이는 어플리케이션에서 필요한 실행 방식과 딱 맞을 일이 거의 없다.
복수의 타임라인에서 다음의 경우 문제가 발생할 가능성이 존재한다.
따라서, 복수의 타임라인에서 문제가 발생할 경우 다음과 같이 해결할 수 있다.
T.B.D