[기술 서적 후기 및 요약] 쏙쏙 들어오는 함수형 코딩
독후감 및 스터디 후기입니다.
![[기술 서적 후기 및 요약] 쏙쏙 들어오는 함수형 코딩](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1743129459556%2F14a7d741-d22e-4622-98e4-1857e449821e.png&w=3840&q=75)
쏙쏙 들어오는 함수형 코딩을 산 지도 어언 2년인 지 3년인 지 흘렀다. 당시 지인들과 스터디를 할 요량으로 구매하였으나 개발 뉴비였던 나는 기초적인 공부를 하는 것만 해도 버거운 입장이었기 때문에 책은 사두고 그대로 방치되는 사태가 발생했다.
그러던 25년, 마침 지속 가능한 코드에 관심이 많았고 마침 스터디 소재를 찾고 있었으며, 마침 관심있는 사람들이 있었던 지라 가볍게 스터디를 진행하기로 했다.
책소개
책제목 : 쏙쏙 들어오는 함수형 코딩
저자 : 에릭 노먼드
분량 : 19챕터, 500여 페이지
요약 :
함수형 사고를 기를 수 있도록 방향을 제시해주며 타임라인 다이어그램 등 문제를 해결하기 위한 방법을 제시해줌
스터디 진행 방식
각 회차별로 구성원들이 문제를 낸다.
깃헙 레포에 올리며 구성원들이 낸 문제를 서로 풀어본다.
푼 문제를 바탕으로 코드리뷰를 진행한다.
각 회차 분량만큼을 요약 발제하여 리마인드한다.
후기
이 책 어땠나요?
우선 이 책은 함수형 프로그래밍 그 자체를 딥다이브하게 다루지 않는다. 다만 함수형 프로그래밍에 필요한 관점과 사고를 처음부터 빌드업해나가는 형태로 책이 작성되어있다. 그래서 함수형 프로그래밍이라기 보단 리팩토링 책에 가깝다.
그 이유는 함수형 프로그래밍의 기법이나 심화적인 내용을 다루지 않는다는 점인데 표로 잠시 살펴보자.
| 내용을 다루고 있는가 | 내용을 다루고 있지 않은가 | |
| 순수함수 | ✅ | |
| 불변성 | ✅ | |
| 일급함수 | ✅ | |
| 고차함수 | ✅ | |
| 함수 합성 | ✅ | |
| 재귀 | ✅ | |
| 지연평가 | ✅ | |
| 커링 | ✅ | |
| 모나드 | ✅ | |
| 타입 이론 | ✅ | |
| 범주론 | ✅ | |
| 대수적 자료형 | ✅ | |
| 함수형 반응형 프로그래밍 | ✅ | |
| 병렬/동시성 프로그래밍 | ✅ | |
| 속성 기반 테스트 | ✅ | |
| 함수형 아키텍쳐 패턴 | ✅ | |
| 도메인 특화 언어 설계 | ✅ |
표에서도 알 수 있듯이 함수형 프로그래밍의 심화된 개념에 대해서는 아예 다루고 있지 않았다. 하지만 함수형 프로그래밍의 기본적 개념은 충실히 다루고 있음을 알 수 있다. 즉 이 책은 함수형 프로그래밍이 무엇인 지에 대해 알고싶은 사람에게 추천 가능한 입문서라는 얘기이다.
추천대상
앞서 말했 듯이 이 책은 말 그대로 함수형 프로그래밍에 대한 입문서다. 하지만 함수형 패러다임에 대한 서적으로만 접근하지 않아도 된다. 이유는 이 책이 제시하는 개념과 그 적용 방법은 더 좋은 코드, 지속 가능한 코드를 만들어내는 하나의 방법론을 찍먹하는 수준이기 때문. 즉 리팩토링 책으로 바라보고 접근해도 충분히 납득 가능한 수준에서 책은 진행된다
아래 코드는 함수형 프로그래밍 서적 초반부의 내용을 적용시켜 리팩토링한 코드다. 스터디 시 구성원과 아래처럼 문제를 주고 받아 리팩토링 하면 좋다. 코드리뷰까지 진행은 덤이다.
// 함수형 프로그래밍의 기초적인 개념을 적용하기 전
let totalScore = 0;
let reviewCount = 0;
function processReview(score: number): void {
totalScore += score;
reviewCount++;
updateReviewDisplay();
}
function updateReviewDisplay(): void {
const average = totalScore / reviewCount;
console.log(`현재 평균 리뷰 점수: ${average.toFixed(2)}`);
}
// 사용 예시
processReview(4);
processReview(5);
processReview(3);
// 적용하고 난 후
type DataType = {
totalScore: number;
reviewCount: number;
};
let data: DataType = {
totalScore: 0,
reviewCount: 0,
};
const dataStack = (score: number, data: DataType): DataType => {
return {
totalScore: data.totalScore + score,
reviewCount: data.reviewCount + 1,
};
};
const calculateAverage = (data: DataType): number => {
return data.totalScore / data.reviewCount;
};
const renderScoreAverage = (average: number): void => {
console.log(`현재 평균 리뷰 점수: ${average.toFixed(2)}`);
};
const processReview = (score: number, data: DataType): DataType => {
const newData = dataStack(score, data);
const average = calculateAverage(newData);
renderScoreAverage(average);
return newData;
};
data = processReview(4, data);
data = processReview(5, data);
data = processReview(3, data);
한 눈에 보기에 리팩토링 후가 코드가 길어졌다는 게 보이는데 반면 역할의 분리가 일어났으며 전역변수를 참조하던 것에서 인자로 값을 받는 형태로 변했다. 또한 일부 코드는 순수함수로 분리되었고 전역변수는 데이터로 관리된다. 이런 리팩토링으로 테스트는 용이해지고 디버깅하기 편해졌으며 재사용도 용이해졌다.
이게 함수형 프로그래밍이냐 아니냐를 떠나서 더 나은 코드를 작성하기 위한 준비가 가능하다.
그런 점에서 이 책의 추천 독자는 “지속 가능한 코드”에 관심이 있으며 함수형 패러다임에 관심이 가 입문하고 싶은 사람 이라고 할 수 있다.
인상 깊었던 부분
이 책의 가장 인상 깊은 점은 실제 프로덕트를 수정하는 상황에서 처럼 “메가마트”라는 가상의 프로덕트의 코드를 수정하는 식으로 진행한다는 점이다. 이를 통해 문제 상황에 대한 접근을 간접적으로 체험할 수 있도록 해준다는 점이 꽤 인상 깊다.
또한 그렇게 수정되는 코드는 점점 더 나은, 점점 더 지속 가능한 코드로 변모하기때문에 동일한 순서로 문제를 만들어가며 리팩토링 해나가면 어느덧 이 책에서 전달하는 코드 작성에 대한 방법을 체화할 수 있도록 해두었다. 예시가 명확하기 때문에 이해에 있어서도 상당히 쉬운 편.
또한 이러한 접근법때문에 실무나 사이드프로젝트에 적용시키기 굉장히 편하다. 책을 읽어가며 한챕터를 나갈 때마다 리팩토링을 고도화해갈 수 있기 때문에 책을 다 읽고 난 후 적용시켜본다 생각할 필요가 없다. 그냥 천천히 책을 읽어가며 실무/사이드에 바로 적용해나가면 된다.
아쉬운 점
함수형 프로그래밍에 대한 원론적인 얘기와 심화적인 부분까지 기대하면 아쉬울 수 밖에 없을 것 같다. 이 책이 리팩토링 관련된 책이다! 라고 하는 지점이 이 부분인데, 더 나은 코드를 작성하는데에는 도움이 되지만 함수형 패러다임에 입각한 프로그래밍에 대한 이해를 하기에는 책이 상당히 가볍고 내용도 그러하다.
하지만 반대로 훌륭한 입문서의 역할도 한다. 학습은 어려우면 흥미를 잃기 마련인데 이 책은 흥미를 돋워주고 그 흥미를 바탕으로 다음 스탭으로 발걸음 할 수 있도록 해준다.
마치며
이 다음 스터디는 객체지향 패러다임에 대한 찍먹을 할 예정이다. 궁극적으로 스터디가 멀티패러다임에 대한 학습까지 이어가는 걸 원하기 때문에 각 패러다임들을 가볍게 훑어보자는 생각.
아무튼 묵혀둔 책을 다 읽었다는 사실은 언제나 기분이 좋다.
이하는 요약이며 책의 대부분을 요약했기 때문에 이 책이 궁금하지만 살지 말지 고민되는 분은 슬쩍 읽어보고 사시면 됩니다. (물론 책에 없는 내용도 들어있고 책에 있는 내용도 안들어있는 요약입니다 ㅎㅎ)
함수형 프로그래밍의 정의
[!함수형 프로그래밍의 정의] 함수형 프로그래밍에서는 코드를 3가지 유형으로 나눕니다.
데이터 : 정적인 값
계산 : 부수 효과 없이 입력에만 의존하는 순수 함수
액션 : 부수 효과가 있는 코드
기존 학술적 정의에 따르면 수학 함수를 사용하고 부수 효과를 피하는 것이 특징인 프로그래밍 패러다임 혹은 부수 효과 없이 순수 함수만 사용하는 프로그래밍 스타일로 정의하지만
실제로 함수형 프로그래밍에서는 부수 효과 또한 주요한 개념이며 순수함수와 사이드 이펙트를 발생시키는 액션을 최대한 분리해 예측 가능성을 높이고 데이터 불변성을 지키며 테스트 용이성을 얻는 등의 효과를 기대할 수 있는 프로그래밍 기법입니다.
// 데이터: 정적이고 그 자체로 의미가 있음
const cart = {
items: [
{ name: "티셔츠", price: 15000, quantity: 2 },
{ name: "바지", price: 28000, quantity: 1 }
]
};
// 계산: 순수 함수, 같은 입력에 항상 같은 출력
function calculateTotal(cart) {
return cart.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
}
function applyDiscount(amount, discountRate) {
return amount * (1 - discountRate);
}
// 액션: 부수 효과가 있고, 실행 시점/횟수가 중요함
function checkout(cart) {
const total = calculateTotal(cart);
console.log(`결제 진행: ${total}원`); // 부수 효과: 콘솔 출력
saveOrderToDatabase(cart, total); // 부수 효과: DB 저장
sendConfirmationEmail(cart); // 부수 효과: 이메일 전송
return { orderId: generateOrderId(), total };
}
// 사용 예시
const total = calculateTotal(cart); // 계산: 안전하게 언제든 호출 가능
checkout(cart); // 액션: 호출 시점과 횟수가 중요
액션에서 계산 빼내기
| 암묵적 | 명시적 | |
| 입력 | - 전역 변수 읽기 |
- 공유된 상태 읽기
- DOM 요소 읽기
- 파일/데이터베이스 읽기
- 네트워크 요청으로 데이터 가져오기 등 | - 함수 인자 |
| 출력 | - 전역변수 변경
- 공유된 상태 변경
- DOM 요소 변경
- 파일/데이터베이스 쓰기
- 네트워크 요청 보내기
- 로그 출력
- 알림 표시 등 | - 리턴값 |
| 특징 | - 함수 동작 예측 어려움
- 테스트 하기 어려움
- 재사용성이 낮음
- 의존성이 숨겨져 있어 파악하기 어려움 | - 함수의 동작을 예측하기 쉬움
- 테스트 하기가 쉬움
- 재사용성이 높음
- 함수의 용도를 명확하게 알 수 있음 |
| 함수 분류 | - 액션 | - 계산 |
| 개선 방법 | - 암묵적 입력을 인자로 바꾸기
- 암묵적 출력을 리턴값으로 바꾸기
- 액션을 계산으로 리팩토링 하기 | |
[!계산의 추출 단계]
계산코드를 찾아 추출한다
추출된 함수에 암묵적 입력과 출력을 찾는다.
암묵적 입력은 인자로, 암묵적 출력은 리턴값으로 바꾼다
// 전역 변수
const a = 1;
const b = 2;
// 암묵적 입출력
const 암묵적_입출력 = () => {
console.log(a+b)
}
암묵적_입출력();
// 명시적 입출력
// 액션에서 계산을 추출
const 덧셈 = (a,b) => {
return a+b;
}
// 명시적 입력을 통한 리팩토링
const 명시적_입출력 = (출력) => {
console.log(출력)
}
명시적_입출력(덧셈(a,b))
좋은 액션 코드 만들기
[!좋은 액션에 대한 정의]
암묵적 입력과 출력을 최대한 줄인다.
액션과 관련 없는 코드를 제거해 작은 크기로 유지한다.
함수를 목적에 따라 분리한다. 단일 책임 원칙을 지킬 수 있도록 구성해본다.
계층 구조를 만든다. 상위 계층은 더 자주 바뀌는 코드로 하위 계층은 변경이 잘 안되는 코드로 나눈다.
// 좋은 액션 예시
function add_item_to_cart(name, price) { // 명시적 입력
var item = make_cart_item(name, price); // 계산
shopping_cart = add_item(shopping_cart, item); // 계산
var total = calc_total(shopping_cart); // 계산
set_cart_total_dom(total); // 액션
update_shipping_icons(shopping_cart); // 액션
update_tax_dom(total); // 액션
}
// 위 코드는 함수형 프로그래밍에서 이상적인 코드가 아니라 좋은 액션 코드이다.
// 관심사의 분리가 명확하고 암묵적 입력이 아닌 명시적 입력을 가지며 카피 온 라이트 패턴을 가진다.
불변성을 유지하기 위한 전략, Copy on write
[!카피-온-라이트의 3단계]
카피-온-라이트가 필요한 이유
함수형 프로그래밍의 핵심 개념 중 하나는 불변성입니다. 카피-온-라이트 전략으로 데이터를 쓸 때 데이터의 불변성을 유지할 수 있도록 합니다.
카피-온-라이트의 3단계
복사본 만들기
복사본 변경하기
복사본 리턴하기
// 배열에 대한 Copy-on-write const add_element_last = (array, element) => { const new_array = [...array]; // 복사본 만들기 new_array.push(element); // 복사본 바꾸기 return new_array; // 복사본 리턴하기 } // 객체에 대한 Copy-on-write const setPrice = (item, new_price) => { const item_copy = Object.assign({}, item); // 복사본 만들기 item_copy.price = new_price; // 복사본 바꾸기 return item_copy; // 복사본 리턴하기 }
신뢰할 수 없는 코드에서 불변성을 유지하기 위한 전략, 방어적 복사
[!방어적 복사란?] 방어적 복사는 신뢰할 수 없는 코드와 데이터를 주고받을 때 불변성을 유지하기 위한 기법입니다. 데이터가 안전지대에서 나가거나 들어올 때 깊은 복사(deep copy)를 수행합니다.
방어적 복사 규칙
데이터가 안전한 코드에서 나갈 때 복사하기: 불변성 데이터를 신뢰할 수 없는 코드로 전달하기 전에 깊은 복사본을 만들어 원본을 보호합니다.
안전한 코드로 데이터가 들어올 때 복사하기: 신뢰할 수 없는 코드에서 데이터가 들어올 때 즉시 깊은 복사본을만들어 사용합니다.
카피-온-라이트와 방어적 복사 비교
| 카피-온-라이트 | 방어적 복사 |
| 통제할 수 있는 데이터를 바꿀 때 사용 | 신뢰할 수 없는 코드와 데이터를 주고받을 때 사용 |
| 안전지대 내부에서 사용 | 안전지대의 경계에서 사용 |
| 얕은 복사(비용이 적게 듦) | 깊은 복사(비용이 많이 듦) |
예시 코드
// 신뢰할 수 없는 코드를 안전하게 사용하는 함수
function untrustedFunctionSafe(data) {
// 1. 데이터가 안전 지대에서 나갈 때 복사
var dataCopy = deepCopy(data);
// 신뢰할 수 없는 코드 호출
untrustedFunction(dataCopy);
// 2. 데이터가 안전 지대로 들어올 때 복사
return deepCopy(dataCopy);
}
계층형 설계 - 직접 구현 패턴
[!정의] 함수의 본문이 함수 시그니처가 나타내는 문제를 적절한 추상화 수준에서 해결하는 것
함수는 비슷한 추상화 수준에 있는 함수를 호출해야 함
함수가 다양한 추상화 수준을 호출하면 읽기 어려워짐
예시 코드 - 문제 코드
// 할인 적용 함수 (문제가 있는 구현)
function applyDiscount(cart) {
let total = 0;
// 장바구니 합계 계산 (저수준 구현)
for(let i = 0; i < cart.length; i++) {
total += cart[i].price * cart[i].quantity;
}
// 할인 규칙 적용 (비즈니스 룰)
if(total > 100) {
return total * 0.9; // 10% 할인
} else if(total > 50) {
return total * 0.95; // 5% 할인
}
return total;
}
예시 코드 - 개선 코드
// 1. 가장 낮은 계층: 유틸리티 함수
function calculateTotal(cart) {
let total = 0;
for(let i = 0; i < cart.length; i++) {
total += cart[i].price * cart[i].quantity;
}
return total;
}
// 2. 중간 계층: 할인 규칙 (비즈니스 로직)
function getDiscountRate(total) {
if(total > 100) {
return 0.9; // 10% 할인
} else if(total > 50) {
return 0.95; // 5% 할인
}
return 1.0; // 할인 없음
}
// 3. 상위 계층: 기능 통합
function applyDiscount(cart) {
const total = calculateTotal(cart);
const discountRate = getDiscountRate(total);
return total * discountRate;
}
위 코드를 토대로 직접 구현 패턴에 대한 설명
문제 코드의 경우 한개의 함수가 다양한 기능을 담아 단일 책임 상태가 아니었다.
내부의 로직을 들여다보았을 때 유틸함수(단순 계산)와 비즈니스 로직(도메인과 관련된 로직)이 내부적으로 작성되어있었다.
이것을 각 계층 별로 분리해낸 것이 개선코드
최상위 계층에서 하위 계층 함수를 활용해 통합 계층을 만들어냈다.
직접 구현 패턴에서 주의해야할 사항
계층형 설계는 코드를 추상화 계층으로 구성하기 때문에 함수 이름(시그니쳐)을 통해 의도를 명확히 해야한다.
또한 시그니쳐는 어떤 계층에 속할 지(속하는 지) 알려주는 요소이기도 하므로 좋은 네이밍이 필요하다.
계층형 설계 - 추상화 벽 패턴
[!정의] 추상화 벽은 세부 구현을 감추는 함수로 이루어진 계층으로, 사용자가 이 벽 아래의 구현 세부사항을 몰라도 상위 계층에서 코드를 작성할 수 있게 해준다.
추상화 벽에 있는 코드는
세부 구현 은닉 : 데이터 구조와 알고리즘 숨김
대칭적 독립성 : 추상화 벽 위의 코드는 아래를 모르고, 아래의 코드는 위를 모름
인터페이스 중심 : 사용이 편한 API를 제공
을 지키며 작성되게 된다
예시 코드 - 저수준 코드 (추상화 벽 아래)
// ===== 추상화 벽 아래 (저수준 구현) =====
// 실제 데이터 저장 방식 (배열 기반)
let usersArray = [];
function findUserIndexById(id) {
for (let i = 0; i < usersArray.length; i++) {
if (usersArray[i].id === id) {
return i;
}
}
return -1;
}
예시 코드 - 추상화 벽
// ===== 추상화 벽 (인터페이스) =====
// 데이터가 배열에 저장된다는 사실을 감춤
function addUser(user) {
usersArray.push(user);
}
function getUserById(id) {
const index = findUserIndexById(id);
return index !== -1 ? usersArray[index] : null;
}
function updateUser(id, updates) {
const index = findUserIndexById(id);
if (index !== -1) {
usersArray[index] = { ...usersArray[index], ...updates };
return true;
}
return false;
}
function deleteUser(id) {
const index = findUserIndexById(id);
if (index !== -1) {
usersArray.splice(index, 1);
return true;
}
return false;
}
예시 코드 - 고수준 코드 (추상화 벽 위)
// ===== 추상화 벽 위 (고수준 기능) =====
// 사용자 저장 방식을 모름
function registerUser(name, email) {
const id = Date.now(); // 간단한 ID 생성
const newUser = { id, name, email, registeredAt: new Date() };
addUser(newUser);
return id;
}
function makeUserAdmin(id) {
return updateUser(id, { isAdmin: true, adminSince: new Date() });
}
위 코드를 기준으로 설명하는 추상화 벽 패턴
고수준의 코드는 추상화 벽의 코드를 사용해 작성된다.
추상화 벽의 코드는 추상화 벽 아래 코드를 사용해 작성된다.
고수준의 코드는 추상화 벽 아래 코드를 알지 못한다.
추상화 벽 아래 코드는 고수준 코드를 알지 못한다.
이 둘을 잇는 역할은 추상화 벽이 담당한다.
각 수준 별 역할
고수준 코드 (추상화 벽 위): 비즈니스 로직이나 애플리케이션 기능 구현
추상화 벽 (중간 계층): 인터페이스 역할을 하는 함수들의 모음
저수준 코드 (추상화 벽 아래): 실제 구현 세부사항
계층형 설계 - 작은 인터페이스 패턴
[!정의] 인터페이스를 최소한으로 유지하면서 필요한 기능만 노출하는 설계 방식이다.
최소한의 인터페이스 : 꼭 필요한 기능만 노출하고 나머지는 숨김
응집력 있는 기능 : 인터페이스의 각 함수는 단일 목적을 가짐
상위 계층에서의 구현 : 가능한 한 인터페이스를 확장하지 않고 상위 계층에서 기능을 조합
예시 코드 - 안좋은 코드
// ===== 좋지 않은 접근법: 너무 많은 기능 노출 =====
function createLargeCalculator() {
// 내부 구현과 상태
let memory = 0;
return {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
square: (a) => a * a,
sqrt: (a) => Math.sqrt(a),
cube: (a) => a * a * a,
power: (a, b) => Math.pow(a, b),
storeInMemory: (value) => { memory = value; },
recallMemory: () => memory,
clearMemory: () => { memory = 0; },
// ... 더 많은 기능들
};
}
예시코드 - 작은 인터페이스 패턴 적용
// ===== 작은 인터페이스 패턴 적용 =====
function createCalculator() {
// 내부 구현 (동일)
let memory = 0;
// 핵심 기능만 노출
return {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b
};
}
예시코드 - 상위 계층에서 기능 추가
// ===== 상위 계층에서 추가 기능 구현 =====
function createScientificCalculator(calculator) {
return {
square: (a) => calculator.multiply(a, a),
cube: (a) => calculator.multiply(calculator.multiply(a, a), a),
power: (a, b) => Math.pow(a, b)
};
}
사용예시
const calc = createCalculator();
console.log(calc.add(5, 3)); // 8
const sciCalc = createScientificCalculator(calc);
console.log(sciCalc.square(4)); // 16
함수형 프로그래밍에서 일급 함수
일급 값과 일급 함수를 활용하면 코드의 중복을 줄이고 추상화 수준을 높일 수 있다.
고차 함수는 다양한 동작을 추상화하는 강력한 도구이다.
함수형 프로그래밍에서는 일급이 아닌 것을 일급으로 바꾸는 방법을 자주 사용한다.
코드의 문맥(context)을 함수로 추상화하면 코드의 재사용성이 높아진다.
// 함수를 일급 객체로 취급하여 변수에 할당(`tenPercentOff`, `twentyPercentOff` 등)
// 함수를 다른 함수의 인자로 전달(할인 전략을 `applyDiscount`에 전달)
// 익명 함수를 즉석에서 생성하여 사용(특별 할인에서)
// 고차 함수 패턴 활용(`applyDiscount`는 함수를 인자로 받는 고차 함수)
// 함수형 스타일로 데이터 변환(불변성을 유지하기 위해 원본 배열을 수정하지 않고 새 배열 반환)
// 할인 전략을 함수로 정의 (일급 함수의 특성)
const tenPercentOff = price => price * 0.9;
const twentyPercentOff = price => price * 0.8;
const thirtyPercentOff = price => price * 0.7;
const noDiscount = price => price;
// 고차 함수: 할인 전략을 인자로 받아 제품에 적용
const applyDiscount = (products, discountStrategy) => {
return products.map(product => ({
...product,
price: discountStrategy(product.price)
}));
};
// 테스트 데이터
const cart = [
{ name: '노트북', price: 1000 },
{ name: '헤드폰', price: 200 },
{ name: '키보드', price: 150 }
];
// 다양한 할인 전략을 쉽게 적용 가능
const cartWith10PercentDiscount = applyDiscount(cart, tenPercentOff);
const cartWith20PercentDiscount = applyDiscount(cart, twentyPercentOff);
// 특별 할인: 200달러 이상 상품만 30% 할인(함수를 즉석에서 생성하여 전달)
const specialDiscount = applyDiscount(cart, price =>
price >= 200 ? price * 0.7 : price
);
console.log('10% 할인:', cartWith10PercentDiscount);
console.log('20% 할인:', cartWith20PercentDiscount);
console.log('특별 할인:', specialDiscount);
일급 함수 II - 핵심 개념 정리
핵심 리팩터링 패턴
함수 본문을 콜백으로 바꾸기: 비슷한 구조의 함수들에서 다른 부분만 콜백으로 추출하여 고차 함수로 만듦
반복되는 코드 구조를 캡슐화하고 변하는 부분만 매개변수화
예:
try/catch구조에서 반복되는 에러 처리 로직을 고차 함수로 추출
암묵적 인자를 드러내기: 함수 이름에 숨어있는 값을 명시적 매개변수로 변환
- 함수 이름으로 구분하던 코드를 일급 값을 사용하는 코드로 개선
카피-온-라이트 패턴 구현
withArrayCopy(): 배열 조작을 안전하게 수행하는 고차 함수
function withArrayCopy(array, modify) { var copy = array.slice(); modify(copy); return copy; }기존: 모든 배열 조작 함수마다 복사-변경-리턴 코드 중복
개선: 하나의 함수로 중복 제거 및 최적화 가능
withObjectCopy(): 객체 조작을 위한 버전
function withObjectCopy(object, modify) { var copy = Object.assign({}, object); modify(copy); return copy; }
함수를 리턴하는 함수(함수 팩토리)
wrapLogging(): 어떤 함수에도 로깅 기능을 추가할 수 있는 함수 팩토리
function wrapLogging(f) { return function(arg) { try { return f(arg); } catch(error) { logToSnapErrors(error); } }; }사용:
var saveUserDataWithLogging = wrapLogging(saveUserDataNoLogging);장점: 수천 줄의 코드를 수동으로 try/catch로 감쌀 필요 없음
tryCatch(), wrapIgnoreErrors(): 에러 처리 패턴 추상화
function tryCatch(f, errorHandler) { try { return f(); } catch(error) { return errorHandler(error); } }makeAdder(): 숫자를 더하는 함수를 만드는 팩토리
function makeAdder(n) { return function(x) { return n + x; }; } // 사용: var increment = makeAdder(1);
주의사항
고차 함수는 강력하지만 과도하게 사용하면 코드 가독성이 저하될 수 있음
중복 제거와 추상화의 적절한 균형이 필요
실제 문제 해결에 도움이 되는지 항상 평가할 것
주요 함수형 메소드
map() 함수:
배열의 모든 항목에 함수를 적용하여 새로운 배열을 반환
원본 배열과 같은 길이의 배열을 생성하지만 각 항목은 변환
예: 고객 배열에서 모든 고객의 이메일 주소만 추출
filter() 함수:
조건(술어)에 맞는 항목만 선택하여 새로운 배열을 만듦
true/false를 반환하는 함수를 사용해 항목을 필터링
예: 우수 고객(3개 이상 제품 구매)만 선택
reduce() 함수:
배열의 모든 항목을 조합하여 단일 값으로 만듦
초깃값과 누적 함수를 사용하여 배열을 순회
데이터 요약, 합계 계산 등에 유용
세 함수 중 가장 강력하며, map()과 filter()도 reduce()로 구현 가능
중첩된 데이터를 다루는 방법 - 재귀 or 반복
| 특성 | 재귀 (Recursion) | 반복문 (Iteration) |
| 구현 방식 | 함수가 자기 자신을 호출하는 방식 | 루프(for, while 등)를 사용한 반복적 실행 |
| 상태 관리 | 주로 불변(immutable) 상태를 유지, 새로운 값을 반환 | 보통 가변(mutable) 상태를 활용하여 데이터 조작 |
| 코드 표현력 | 중첩된 데이터 구조에 대한 자연스러운 표현 가능 | 중첩 구조를 표현하기 위해 명시적인 스택이나 큐가 필요할 수 있음 |
| 가독성 | 복잡한 중첩 구조에서 더 간결하고 직관적인 코드 | 깊은 중첩 구조에서 복잡해질 수 있음 |
| 메모리 사용 | 호출 스택에 각 재귀 호출 정보가 쌓임, 깊은 중첩에서 스택 오버플로우 위험 | 일반적으로 일정한 메모리 사용, 스택 오버플로우 위험 낮음 |
| 성능 | 함수 호출 오버헤드로 인해 느려질 수 있음 (최적화 없는 경우) | 일반적으로 더 효율적인 CPU 사용 |
| 최적화 가능성 | 꼬리 재귀(tail recursion) 최적화를 통해 성능 향상 가능 | 컴파일러 수준에서 이미 최적화되어 있는 경우가 많음 |
| 함수형 순수성 | 부작용 없는 순수 함수로 구현하기 쉬움 | 상태 변경을 통한 구현이 일반적, 순수성 유지가 어려울 수 있음 |
| 병렬 처리 | 상태 공유가 적어 병렬화에 유리할 수 있음 | 공유 상태로 인해 병렬화에 어려움이 있을 수 있음 |
| 디버깅 | 깊은 호출 스택으로 인해 디버깅이 복잡할 수 있음 | 상태 변화를 추적하기 쉬워 디버깅이 상대적으로 용이 |
| 언어 지원 | 함수형 언어(Haskell, Clojure 등)에서 최적화 지원 | 대부분의 언어에서 기본적으로 지원 |
타임라인 추적하기
[!정의] 웹 개발 과정에서 타임라인 다이어그램은 시간에 따라 실행되는 액션의 순서를 나타내기 위한 중요한 도구로 특히 여러 비동기 작업이 동시에 일어나는 경우 발생하는 버그를 찾는 데 매우 유용하다.
타임라인 그리는 법
타임라인은 시간에 따른 액션의 순서를 표현
같은 타임라인에 있는 액션은 순서대로 실행되고 서로 다른 타임라인에 있는 액션은 실행순서가 보장되지 않을 수 있음
비동기 작업의 완료 시점은 점선으로 나타냄
자바스크립트는 싱글스레드 언어이므로 비동기 작업을 표현할 때는 타임라인이 분리됨
자바스크립트(타입스크립트) 개발자로서 타임라인은?
타임라인은 본질적으로 제어권(Control Flow)에 대한 얘기이므로 동기/비동기에 대한 이해가 중요하다.
뿐만 아니라 제어권을 개발자가 핸들링할 수 있는 제너레이터에 대한 이해도 중요해진다.
function* numGenerator(){
yield 1;
yield 2;
yield 3;
}
const generator = numGenerator();
generator.next();
console.log("또 다른 무언가 작업")
// 사용자가 제어권을 손쉽게 가져올 수 있음
if(어떤_작업_완료_여부){
generator.next();
}else{
어떤_작업을_완료시킬_작업();
}
generator.next();
동시성 기본형
[!정의] 자원을 안전하게 공유할 수 있는 재사용 가능한 코드로 여러 작업이 동시에 실행될 때 자원과 제어 흐름을 안전하게 관리하는 추상화된 메커니즘이다.
종류
Queue와 같은 자료구조형을 통한 자원 공유 및 실행 제어
제너레이터와 같이 함수의 실행을 제어할 수 있는 도구
Promise와 같은 비동기 동작
그 외
반응형 아키텍쳐
[!정의] 이벤트에 반응하여 동작하는 시스템을 구축하는 방식. 전통적 개념인 "X를 한 다음 Y를 한다"가 아니라 "X가 일어나면 Y를 한다"로 정의되며 원인과 결과(효과)를 분리하는 것이 중요하다.
감시자 패턴
어떤 일이 발생했을 때 특정 함수가 자동으로 실행될 수 있도록 하는 패턴
이벤트 핸들러 (특정 이벤트가 발생하면 콜백함수가 실행)
옵저버 (이벤트가 발생하면 구독된 것들에 대해 함수가 실행)
어니언 아키텍쳐
[!정의] 어니언 아키텍처는 현실 세계와 상호작용하기 위한 서비스 구조를 만드는 방법으로, 양파처럼 겹겹이 쌓인 계층 구조
주요 계층
인터랙션 계층: 가장 바깥쪽 계층으로, 외부 세계와 상호작용(데이터베이스, API 호출, 웹 요청 등)을 담당
도메인 계층: 중간 계층으로, 비즈니스 규칙과 도메인 로직을 정의하는 계산으로 구성되며 외부 의존성 없이 순수한 비즈니스 로직만을 포함
언어 계층: 가장 안쪽 계층으로, 언어 유틸리티와 라이브러리를 포함
핵심 원칙
현실 세계와의 상호작용은 인터랙션 계층에서만: 데이터베이스 접근, API 호출 등의 외부 상호작용은 인터랙션 계층에서만 이루어짐
안쪽 방향으로의 의존성: 외부 계층은 내부 계층에 의존할 수 있지만, 내부 계층은 외부 계층에 의존해서는 안 됨. 즉, 도메인 계층은 인터랙션 계층을 알지 못함.
계층 독립성: 각 계층은 다른 계층이 어떻게 구현되었는지 모름
두 아키텍쳐 비교표
| 특성 | 반응형 아키텍처 | 어니언 아키텍처 |
| 핵심 개념 | 이벤트에 반응하여 동작하는 시스템 | 계층화된 구조로 의존성 방향이 안쪽으로 향하는 시스템 |
| 주요 목적 | 순차적 액션의 순서를 뒤집어 코드의 복잡성 관리 | 비즈니스 로직을 외부 의존성으로부터 분리 |
| 구조 | 이벤트-핸들러 기반 | 계층형(인터랙션, 도메인, 언어 계층) |
| 처리 방식 | "X가 발생하면 Y를 한다" | 외부 계층에서 내부 계층으로 호출, 내부에서 외부로는 의존성 없음 |
| 주요 패턴 | 감시자(Watcher), 옵저버, 이벤트 핸들러 | 의존성 주입, 인터페이스 분리 |
| 상태 관리 | 반응형 상태 객체(ValueCell, FormulaCell 등) | 도메인 모델 내에서 캡슐화 |
| 적용 범위 | 주로 UI, 이벤트 처리 시스템 | 전체 애플리케이션 아키텍처 |
| 테스트 용이성 | 이벤트 기반으로 테스트 가능 | 도메인 로직이 순수 계산으로 분리되어 테스트 용이 |
| 변경 용이성 | 이벤트 핸들러 추가/수정이 용이 | 인터랙션 계층만 변경하여 외부 시스템 교체 가능 |
| 코드 복잡성 | 이벤트 체인이 복잡해질 수 있음 | 명확한 계층 분리로 복잡성 관리 |
| 사용 사례 | 웹 UI, 실시간 시스템, 사용자 인터페이스 | 엔터프라이즈 애플리케이션, 도메인 중심 시스템 |
| 타임라인 | 짧은 타임라인 여러 개로 분리 | 계층 간 호출 흐름으로 관리 |
| 확장성 | 이벤트 핸들러 추가로 확장 | 각 계층 내부에서 독립적으로 확장 |
| 장점 | 원인과 효과 분리, 유연한 타임라인 | 도메인 로직 보호, 외부 시스템 교체 용이 |
| 단점 | 이벤트 흐름 추적 어려움, 디버깅 복잡 | 초기 설계 복잡성, 엄격한 계층 분리 필요 |
| 함께 사용 | 어니언 아키텍처 내에서 반응형 패턴을 적용하여 시너지 효과 가능 |


![[트러블슈팅] 서버를 터뜨린 쟈그마한 함수](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1748357643167%2Ffc68d237-0bfe-4f79-8e28-15fd98c5710d.png&w=3840&q=75)
![[Daily Flow 1.] Validator 리팩토링](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1741161717995%2Fa62693a8-0f24-4dc9-8d80-cefe541ed663.png&w=3840&q=75)