[쏙쏙 들어오는 함수형 코딩] Ch.03 ~ 04
3장에서는 함수형 프로그래밍의 가장 기본이 되는 개념인 액션, 계산, 데이터를 구분하는 방법을 배운다.
Chapter 3. 액션과 계산, 데이터의 차이를 알기
3장에서는 함수형 프로그래밍의 가장 기본이 되는 개념인 액션, 계산, 데이터를 구분하는 방법을 배운다.
1. 세 가지 분류
함수형 프로그래머는 모든 코드를 세 가지로 구분한다.
- 코들를 볼 때 단순히 “함수인가?”만 보는 것이 아니라, 그 코드가 실행 시점이나 횟수에 영향을 받는지, 입력에 따라 항상 같은 결과를 내는지, 혹은 단순한 사실인지 구분한다.
함수형 프로그래머는 액션보다 계산을, 계산보다 데이터를 선호한다.
2. 언제 어디서나 적용할 수 있다
장보기 예시로 보는 분류
단순해 보이는 "장보기" 과정도 나눠보면:
정리하면
- 액션 안에는 계산과 데이터가 숨어 있다 — 단순해 보이는 액션도 더 작은 단위로 나눌 수 있다
- 계산은 머릿속에서 일어난다 — "무엇을 살지 결정하는 것"처럼 자연스러워서 잘 안 보일 뿐
- 데이터를 먼저 찾아라 — 데이터를 찾으면 동작에 대해 많은 것을 알 수 있다
3. 쿠폰독 예시 — 새 코드에 함수형 사고 적용하기
친구 10명 이상 추천한 구독자에게 'best' 쿠폰을, 나머지에게는 'good' 쿠폰을 보내는 서비스.
전체 흐름 분류
구현 핵심 — 계산을 먼저, 액션은 마지막에
// 계산 1: 구독자 등급 결정
function subCouponRank(subscriber) {
if (subscriber.rec_count >= 10) return "best";
else return "good";
}
// 계산 2: 등급별 쿠폰 목록 선택
function selectCouponsByRank(coupons, rank) {
var ret = [];
for (var c = 0; c < coupons.length; c++) {
if (coupons[c].rank === rank)
ret.push(coupons[c].code);
}
return ret;
}
// 계산 3: 구독자 한 명의 이메일 생성
function emailForSubscriber(subscriber, goods, bests) {
var rank = subCouponRank(subscriber);
if (rank === "best")
return { from: "newsletter@coupondog.co", to: subscriber.email,
subject: "Your best weekly coupons inside", body: "..." };
else
return { from: "newsletter@coupondog.co", to: subscriber.email,
subject: "Your good weekly coupons inside", body: "..." };
}
// 계산 4: 전체 이메일 목록 생성
function emailsForSubscribers(subscribers, goods, bests) {
var emails = [];
for (var s = 0; s < subscribers.length; s++) {
emails.push(emailForSubscriber(subscribers[s], goods, bests));
}
return emails;
}
// 액션: 모든 것을 조합해서 실제로 실행
function sendIssue() {
var coupons = fetchCouponsFromDB(); // 액션
var goodCoupons = selectCouponsByRank(coupons, "good"); // 계산
var bestCoupons = selectCouponsByRank(coupons, "best"); // 계산
var subscribers = fetchSubscribersFromDB(); // 액션
var emails = emailsForSubscribers(subscribers, goodCoupons, bestCoupons); // 계산
for (var e = 0; e < emails.length; e++) {
emailSystem.send(emails[e]); // 액션
}
}// 계산 1: 구독자 등급 결정
function subCouponRank(subscriber) {
if (subscriber.rec_count >= 10) return "best";
else return "good";
}
// 계산 2: 등급별 쿠폰 목록 선택
function selectCouponsByRank(coupons, rank) {
var ret = [];
for (var c = 0; c < coupons.length; c++) {
if (coupons[c].rank === rank)
ret.push(coupons[c].code);
}
return ret;
}
// 계산 3: 구독자 한 명의 이메일 생성
function emailForSubscriber(subscriber, goods, bests) {
var rank = subCouponRank(subscriber);
if (rank === "best")
return { from: "newsletter@coupondog.co", to: subscriber.email,
subject: "Your best weekly coupons inside", body: "..." };
else
return { from: "newsletter@coupondog.co", to: subscriber.email,
subject: "Your good weekly coupons inside", body: "..." };
}
// 계산 4: 전체 이메일 목록 생성
function emailsForSubscribers(subscribers, goods, bests) {
var emails = [];
for (var s = 0; s < subscribers.length; s++) {
emails.push(emailForSubscriber(subscribers[s], goods, bests));
}
return emails;
}
// 액션: 모든 것을 조합해서 실제로 실행
function sendIssue() {
var coupons = fetchCouponsFromDB(); // 액션
var goodCoupons = selectCouponsByRank(coupons, "good"); // 계산
var bestCoupons = selectCouponsByRank(coupons, "best"); // 계산
var subscribers = fetchSubscribersFromDB(); // 액션
var emails = emailsForSubscribers(subscribers, goodCoupons, bestCoupons); // 계산
for (var e = 0; e < emails.length; e++) {
emailSystem.send(emails[e]); // 액션
}
}함수형 구현 순서: 데이터 → 계산 → 액션 순으로 구현한다. 제약이 가장 많은 것부터 만든다.
4. 이미 있는 코드에 함수형 사고 적용하기
액션은 코드 전체로 퍼진다
function figurePayout(affiliate) {
var owed = affiliate.sales * affiliate.commission;
if (owed > 100)
sendPayout(affiliate.bank_code, owed); // ← 액션 1개
}
function affiliatePayout(affiliates) {
for (var a = 0; a < affiliates.length; a++)
figurePayout(affiliates[a]); // ← figurePayout이 액션이므로 여기도 액션
}
function main(affiliates) {
affiliatePayout(affiliates); // ← 전체가 액션
}function figurePayout(affiliate) {
var owed = affiliate.sales * affiliate.commission;
if (owed > 100)
sendPayout(affiliate.bank_code, owed); // ← 액션 1개
}
function affiliatePayout(affiliates) {
for (var a = 0; a < affiliates.length; a++)
figurePayout(affiliates[a]); // ← figurePayout이 액션이므로 여기도 액션
}
function main(affiliates) {
affiliatePayout(affiliates); // ← 전체가 액션
}액션 하나가 그것을 호출하는 모든 함수를 액션으로 만든다. 마치 전염처럼 퍼진다.
자바스크립트에서 액션이 되는 것들
- 함수 호출:
alert(),console.log() - 메서드 호출: DOM 조작, AJAX 요청
- 생성자:
new Date()— 호출 시점에 따라 다른 값 - 변수 참조: 공유되고 변경 가능한 변수를 읽는 것
- 값 할당: 공유 변수에 값을 쓰는 것
5. 각 분류의 특징 정리
데이터
- 장점: 직렬화하기 쉽고, 동일성 비교가 가능하며, 여러 방법으로 해석 가능
- 단점: 해석이 반드시 필요 — 해석하지 않은 데이터는 그냥 바이트일 뿐
계산
- 장점: 테스트하기 쉽고, 언제 어디서나 실행 가능, 조합하기 쉬움
- 단점: 실행 전까지 결과를 알 수 없음 (블랙박스)
액션
- 장점: 외부 세계와 상호작용 — 소프트웨어를 실행하는 가장 중요한 이유
- 단점: 다루기 어렵고, 시점과 횟수에 의존, 코드 전체로 퍼짐
- 사용 원칙:
Chapter 4. 액션에서 계산 빼내기
1. MegaMart 코드 — 문제 파악
장바구니 합계를 보여주고, 무료 배송 아이콘을 표시하고, 세금을 계산하는 코드.
var shopping_cart = []; // 전역변수 — 액션
var shopping_cart_total = 0; // 전역변수 — 액션
function add_item_to_cart(name, price) { // 액션
shopping_cart.push({ name, price }); // 전역변수 변경
calc_cart_total();
}
function calc_cart_total() { // 액션
shopping_cart_total = 0;
for (var i = 0; i < shopping_cart.length; i++) {
shopping_cart_total += shopping_cart[i].price;
}
set_cart_total_dom();
update_shipping_icons();
update_tax_dom();
}var shopping_cart = []; // 전역변수 — 액션
var shopping_cart_total = 0; // 전역변수 — 액션
function add_item_to_cart(name, price) { // 액션
shopping_cart.push({ name, price }); // 전역변수 변경
calc_cart_total();
}
function calc_cart_total() { // 액션
shopping_cart_total = 0;
for (var i = 0; i < shopping_cart.length; i++) {
shopping_cart_total += shopping_cart[i].price;
}
set_cart_total_dom();
update_shipping_icons();
update_tax_dom();
}이 코드에는 계산이나 데이터가 없고 모두 액션이다.
2. 왜 문제인가 — 조지와 제나의 고민
테스트 담당 조지의 문제
1. 브라우저 세팅
2. 페이지 로드
3. 버튼 클릭
4. DOM 업데이트 대기
5. DOM에서 값 추출
6. 문자열 → 숫자 변환
7. 예상값과 비교1. 브라우저 세팅
2. 페이지 로드
3. 버튼 클릭
4. DOM 업데이트 대기
5. DOM에서 값 추출
6. 문자열 → 숫자 변환
7. 예상값과 비교비즈니스 규칙 total * 0.10 하나를 테스트하는데 이 과정이 전부 필요하다.
개발팀 제나의 문제
결제팀과 배송팀에서 코드를 재사용하려고 했지만 불가능했다.
- 전역변수
shopping_cart_total에 의존 → DB에서 읽어와야 하는 팀은 못 씀 - DOM을 직접 조작 → 영수증이나 운송장을 출력해야 하는 팀은 못 씀
- 리턴값이 없음 → 결과를 받을 방법이 없음
3. 입력과 출력 — 명시적 vs 암묵적
var total = 0;
function add_to_total(amount) { // 인자 — 명시적 입력
console.log("Old total: " + total); // 콘솔 출력 — 암묵적 출력
total += amount; // 전역변수 변경 — 암묵적 출력
return total; // 리턴값 — 명시적 출력
} // 전역변수 읽기 — 암묵적 입력var total = 0;
function add_to_total(amount) { // 인자 — 명시적 입력
console.log("Old total: " + total); // 콘솔 출력 — 암묵적 출력
total += amount; // 전역변수 변경 — 암묵적 출력
return total; // 리턴값 — 명시적 출력
} // 전역변수 읽기 — 암묵적 입력암묵적 입출력 = 부수 효과(side effect) 함수에 암묵적 입출력이 있으면 액션, 없으면 계산이다.
4. 계산 추출하기 — 3단계
- 계산 코드를 찾아 빼낸다 — 함수 추출하기(extract subroutine)
- 새 함수에서 암묵적 입출력을 찾는다
- 암묵적 입력은 인자로, 암묵적 출력은 리턴값으로 바꾼다
예시 1: calc_total() 추출
// 원래 — 전역변수를 읽고 쓰는 액션
function calc_cart_total() {
shopping_cart_total = 0;
for (var i = 0; i < shopping_cart.length; i++) {
shopping_cart_total += shopping_cart[i].price;
}
set_cart_total_dom();
update_shipping_icons();
update_tax_dom();
}
// 계산으로 추출
function calc_total(cart) { // 암묵적 입력 → 인자로
var total = 0;
for (var i = 0; i < cart.length; i++) {
total += cart[i].price;
}
return total; // 암묵적 출력 → 리턴값으로
}
// 호출부
function calc_cart_total() {
shopping_cart_total = calc_total(shopping_cart); // 리턴값을 전역에 할당
set_cart_total_dom();
update_shipping_icons();
update_tax_dom();
}// 원래 — 전역변수를 읽고 쓰는 액션
function calc_cart_total() {
shopping_cart_total = 0;
for (var i = 0; i < shopping_cart.length; i++) {
shopping_cart_total += shopping_cart[i].price;
}
set_cart_total_dom();
update_shipping_icons();
update_tax_dom();
}
// 계산으로 추출
function calc_total(cart) { // 암묵적 입력 → 인자로
var total = 0;
for (var i = 0; i < cart.length; i++) {
total += cart[i].price;
}
return total; // 암묵적 출력 → 리턴값으로
}
// 호출부
function calc_cart_total() {
shopping_cart_total = calc_total(shopping_cart); // 리턴값을 전역에 할당
set_cart_total_dom();
update_shipping_icons();
update_tax_dom();
}예시 2: calc_tax() 추출
// 원래 — 전역변수 의존
function update_tax_dom() {
set_tax_dom(shopping_cart_total * 0.10);
}
// 계산으로 추출
function calc_tax(amount) {
return amount * 0.10;
}
// 호출부
function update_tax_dom() {
set_tax_dom(calc_tax(shopping_cart_total));
}// 원래 — 전역변수 의존
function update_tax_dom() {
set_tax_dom(shopping_cart_total * 0.10);
}
// 계산으로 추출
function calc_tax(amount) {
return amount * 0.10;
}
// 호출부
function update_tax_dom() {
set_tax_dom(calc_tax(shopping_cart_total));
}예시 3: gets_free_shipping() 추출
// 원래 — 전역변수 의존
function update_shipping_icons() {
var buttons = get_buy_buttons_dom();
for (var i = 0; i < buttons.length; i++) {
var item = buttons[i].item;
if (item.price + shopping_cart_total >= 20) // ← 전역변수
buttons[i].show_free_shipping_icon();
else
buttons[i].hide_free_shipping_icon();
}
}
// 계산으로 추출
function gets_free_shipping(total, item_price) {
return item_price + total >= 20;
}
// 호출부
function update_shipping_icons() {
var buttons = get_buy_buttons_dom();
for (var i = 0; i < buttons.length; i++) {
var item = buttons[i].item;
if (gets_free_shipping(shopping_cart_total, item.price))
buttons[i].show_free_shipping_icon();
else
buttons[i].hide_free_shipping_icon();
}
}// 원래 — 전역변수 의존
function update_shipping_icons() {
var buttons = get_buy_buttons_dom();
for (var i = 0; i < buttons.length; i++) {
var item = buttons[i].item;
if (item.price + shopping_cart_total >= 20) // ← 전역변수
buttons[i].show_free_shipping_icon();
else
buttons[i].hide_free_shipping_icon();
}
}
// 계산으로 추출
function gets_free_shipping(total, item_price) {
return item_price + total >= 20;
}
// 호출부
function update_shipping_icons() {
var buttons = get_buy_buttons_dom();
for (var i = 0; i < buttons.length; i++) {
var item = buttons[i].item;
if (gets_free_shipping(shopping_cart_total, item.price))
buttons[i].show_free_shipping_icon();
else
buttons[i].hide_free_shipping_icon();
}
}5. add_item() 추출 — 카피-온-라이트 첫 등장
장바구니에 항목을 추가하는 로직도 계산으로 빼낼 수 있다.
// 원래 — 전역 배열을 직접 변경
function add_item_to_cart(name, price) {
shopping_cart.push({ name, price }); // 원본 변경
calc_cart_total();
}
// 계산으로 추출 — 카피-온-라이트 적용
function add_item(cart, name, price) {
var new_cart = cart.slice(); // 복사본 만들기
new_cart.push({ name, price }); // 복사본 변경
return new_cart; // 복사본 리턴
}
// 호출부
function add_item_to_cart(name, price) {
shopping_cart = add_item(shopping_cart, name, price); // 리턴값으로 교체
calc_cart_total();
}// 원래 — 전역 배열을 직접 변경
function add_item_to_cart(name, price) {
shopping_cart.push({ name, price }); // 원본 변경
calc_cart_total();
}
// 계산으로 추출 — 카피-온-라이트 적용
function add_item(cart, name, price) {
var new_cart = cart.slice(); // 복사본 만들기
new_cart.push({ name, price }); // 복사본 변경
return new_cart; // 복사본 리턴
}
// 호출부
function add_item_to_cart(name, price) {
shopping_cart = add_item(shopping_cart, name, price); // 리턴값으로 교체
calc_cart_total();
}여기서 처음으로 카피-온-라이트 패턴이 등장한다. 원본을 직접 수정하지 않고 복사본을 만들어 수정한 뒤 리턴하는 방식. 6챕터에서 이 개념을 본격적으로 다룬다.
6. 최종 코드 — 액션 vs 계산 정리
var shopping_cart = []; // A 전역변수 — 액션
var shopping_cart_total = 0; // A 전역변수 — 액션
function add_item_to_cart(name, price) { // A 액션
shopping_cart = add_item(shopping_cart, name, price);
calc_cart_total();
}
function calc_cart_total() { // A 액션 (DOM 조작 포함)
shopping_cart_total = calc_total(shopping_cart);
set_cart_total_dom();
update_shipping_icons();
update_tax_dom();
}
function update_shipping_icons() { // A 액션 (DOM 읽기/쓰기)
var buttons = get_buy_buttons_dom();
for (var i = 0; i < buttons.length; i++) {
var item = buttons[i].item;
if (gets_free_shipping(shopping_cart_total, item.price))
buttons[i].show_free_shipping_icon();
else
buttons[i].hide_free_shipping_icon();
}
}
function update_tax_dom() { // A 액션 (DOM 조작)
set_tax_dom(calc_tax(shopping_cart_total));
}
// ---- 아래는 모두 계산 ----
function add_item(cart, name, price) { // C 계산
var new_cart = cart.slice();
new_cart.push({ name, price });
return new_cart;
}
function calc_total(cart) { // C 계산
var total = 0;
for (var i = 0; i < cart.length; i++)
total += cart[i].price;
return total;
}
function gets_free_shipping(total, item_price) { // C 계산
return item_price + total >= 20;
}
function calc_tax(amount) { // C 계산
return amount * 0.10;
}var shopping_cart = []; // A 전역변수 — 액션
var shopping_cart_total = 0; // A 전역변수 — 액션
function add_item_to_cart(name, price) { // A 액션
shopping_cart = add_item(shopping_cart, name, price);
calc_cart_total();
}
function calc_cart_total() { // A 액션 (DOM 조작 포함)
shopping_cart_total = calc_total(shopping_cart);
set_cart_total_dom();
update_shipping_icons();
update_tax_dom();
}
function update_shipping_icons() { // A 액션 (DOM 읽기/쓰기)
var buttons = get_buy_buttons_dom();
for (var i = 0; i < buttons.length; i++) {
var item = buttons[i].item;
if (gets_free_shipping(shopping_cart_total, item.price))
buttons[i].show_free_shipping_icon();
else
buttons[i].hide_free_shipping_icon();
}
}
function update_tax_dom() { // A 액션 (DOM 조작)
set_tax_dom(calc_tax(shopping_cart_total));
}
// ---- 아래는 모두 계산 ----
function add_item(cart, name, price) { // C 계산
var new_cart = cart.slice();
new_cart.push({ name, price });
return new_cart;
}
function calc_total(cart) { // C 계산
var total = 0;
for (var i = 0; i < cart.length; i++)
total += cart[i].price;
return total;
}
function gets_free_shipping(total, item_price) { // C 계산
return item_price + total >= 20;
}
function calc_tax(amount) { // C 계산
return amount * 0.10;
}결과: 전체 코드에서 계산이 생겨났고, 비즈니스 규칙을 다른 팀에서도 재사용할 수 있게 됐다.
핵심 요점 정리
Chapter 3
- 함수형 프로그래머는 모든 코드를 액션, 계산, 데이터로 구분한다
- 액션은 실행 시점과 횟수에 의존한다 — 호출하는 함수도 모두 액션이 된다
- 계산은 같은 입력에 항상 같은 출력 — 테스트하기 쉽고 조합하기 쉽다
- 데이터는 이벤트에 대한 사실 — 직렬화, 비교, 다양한 해석이 가능하다
- 구현 순서: 데이터 → 계산 → 액션
Chapter 4
- 암묵적 입출력이 있으면 액션, 없으면 계산이다
- 암묵적 입력(전역변수 읽기) → 인자로 바꾼다
- 암묵적 출력(전역변수 쓰기, DOM 조작) → 리턴값으로 바꾼다
- 계산 추출 3단계: 코드 빼내기 → 암묵적 입출력 찾기 → 명시적으로 바꾸기
- 함수형 원칙을 적용하면 액션은 줄고 계산은 늘어난다
느낀점
함수형 프로그래밍 을 처음 접했을 때는 어려운 문법이나 낯선 개념이 먼저 떠오른다. 하지만 이 책에서는 코드를 액션, 계산, 데이터로 나누는 것부터 시작한다. 이 기준은 생각보다 실용적이고, 실제 프로젝트 코드에도 바로 적용해 보 수 있을 것 같다.
특히 기억에 남는 점은 액션을 줄이고 계산을 늘리는 이유이다. 계산은 입력과 출력이 명확해서 테스트하기 쉽고, 재사용하기도 좋다. 반면 액션은 실행 시점과 횟수에 영향을 받기 때문에 조심해서 다뤄야 한다.
기존에는 코드를 리팩터링할 때 “중복을 줄인다”거나 “함수를 작게 나눈다”정도만 생각했다. 그런데 앞으로는 코드를 작성할 때 다음과 같은 질문을 해봐야 겠다.
- 이 코드는 실행 시점에 따라 결과가 달라지는가?
- 이 함수는 전역변수나 외부 상태에 의존하고 있는가?
- 이 안에서 계산만 따로 분리할 수 있는가?
- 이 비즈니스 규칙을 DOM이나 API 없이 테스트할 수 있는가?
이런 질문을 하면서 코드를 작성하면 더 테스트하기 쉽고, 재사용하기 좋은 코드를 만들 수 있을 것 다.