함수형 프로그래밍의 특징 정리
함수형 프로그래밍의 특징인 불변성, 순수 함수, 클로저, 참조 투명성 정리
프로그래밍 언어론불변성
데이터가 한 번 생성되면 그 이후에는 변경할 수 없는 성질
- 데이터를 수정해야 한다면, 원본을 직접 바꾸는(mutate) 대신 변경 사항이 적용된 새로운 복사본을 만들어 반환
왜 중요한가?
- 예측 가능성 향상
- 원본 데이터가 절대 변하지 않으므로 프로그램의 상태를 추적하기 쉬워짐
- 안전한 상태 관리
- React에서는 상태(state)의 불변성을 핵심 원칙으로 삼는다
- 상태가 변경될 때마다 새로운 객체를 생성하면, 이전과 현재 상태를 간단히 참조 비교(
===)하는 것만으로 변화를 감지하고 화면을 효율적으로 업데이트할 수 있음
예시
// 나쁜 예: 원본 배열을 직접 변경 (Mutation)
const numbers = [1, 2, 3];
const addNumber_bad = (arr, num) => {
arr.push(num); // 원본 배열(numbers)을 직접 수정함
return arr;
};
const newNumbers_bad = addNumber_bad(numbers, 4);
console.log(newNumbers_bad); // [1, 2, 3, 4]
console.log(numbers); // [1, 2, 3, 4] <- 원본이 오염됨!
// 좋은 예: 새로운 배열을 반환 (Immutability)
const originalNumbers = [1, 2, 3];
const addNumber_good = (arr, num) => {
// 스프레드 문법(...)을 사용해 원본의 복사본을 만들고, 새 요소를 추가
return [...arr, num];
};
const newNumbers_good = addNumber_good(originalNumbers, 4);
console.log(newNumbers_good); // [1, 2, 3, 4]
console.log(originalNumbers); // [1, 2, 3] <- 원본이 그대로 보존됨!
순수함수와 부작용
순수함수의 조건
- 동일한 입력에 대해 항상 동일한 출력을 반환
- 함수 외부에 어떠한 영향(Side Effects)도 주지 않음
- 부작용이 없다고 표현
- 전역 변수 수정, API 요청, 콘솔 출력 등 함수 범위(scope) 밖의 세상을 변경하는 모든 행위를 포함
왜 중요한가
- 테스트 용이성: 순수 함수는 입력값만 제어하면 출력이 항상 보장됨
- 코드 이해 용이: 함수가 하는 일이 명확히 그 이름과 입출력에만 한정됨
// 나쁜 예: 부작용이 있는 함수 (Impure Function)
let taxRate = 0.1;
const calculatePrice_bad = (price) => {
// 함수 외부의 변수(taxRate)에 의존하고, 이를 직접 수정함
taxRate = 0.2; // 부작용(Side Effect) 발생!
return price * (1 + taxRate);
};
// 좋은 예: 순수 함수 (Pure Function)
const calculatePrice_good = (price, tax) => {
// 필요한 모든 값은 인자로 전달받고, 외부 상태를 변경하지 않음
return price * (1 + tax);
};
console.log(calculatePrice_good(1000, 0.1)); // 1100
// 몇 번을 호출하든, 입력이 같으면 결과도 항상 같다.
참조 투명성
어떤 함수 호출을 그 함수의 결과값으로 대체해도 프로그램의 동작에 아무런 영향을 주지 않는 성질
순수 함수를 사용할 때 자연스럽게 따라오는 성질
왜 중요한가
- 논리적 추론 가능: 복잡한 코드 덩어리를 분석하고 리팩토링하는 과정을 매우 단순하게 함
- 최적화: 어차피 결과가 같다면, 함수를 굳이 매번 실행하지 않고 이전에 계산된 결과를 재사용하는 등의 최적화가 가능
const add = (a, b) => a + b; // 순수 함수
// 아래 두 줄의 코드는 완전히 동일하게 동작합니다.
const result1 = add(2, 3) + 5; // add(2, 3)이라는 표현식
const result2 = 5 + 5; // add(2, 3)을 그 결과값인 5로 대체
console.log(result1 === result2); // true
// add(2, 3)은 참조 투명성을 가집니다.
클로저 (Closure)
코드 블록과 그 블록이 정의된 문맥의 상수나 변수에 대한 참조를 함께 묶은 것
- 즉, 함수가 자기 자신이 생성된 환경(스코프)를 "기억"하고, 그 환경에 있던 변수들에 계속해서 접근할 수 있는 능력
- 외부 함수의 실행이 끝나서 그 생명주기가 다했더라도, 내부 함수는 여전히 외부 함수의 변수에 접근할 수 있다
- 함수와 다르게 이름이 필수가 아니며, 코드 내에서 직접 전달하거나 변수/상수에 할당할 수 있어 **일급 객체(First-class citizen)**로 취급된다.
왜 중요한가
- 상태의 은닉: 클로저는 private 변수를 만드는 가장 흔한 방법
- 이를 통해 상태를 안전하게 캡슐화하고, 오직 허용된 함수를 통해서만 상태를 변경하도록 제어
- 함수형 프로그래밍의 기반: 함수를 반환하고 조합하는 다양한 패턴들이 클로저를 통해 구현됨
예시
// makeCounter 함수는 counter 함수를 반환하고 생을 마감한다.
const makeCounter = () => {
let privateCount = 0; // '비공개' 변수
// 반환되는 이 내부 함수가 바로 클로저다.
// 이 함수는 자신이 생성될 때의 환경(privateCount)을 기억한다.
return () => {
privateCount += 1;
console.log(privateCount);
};
};
const counter = makeCounter(); // counter 변수는 클로저 함수를 가리킴
counter(); // 1
counter(); // 2
counter(); // 3
// 외부에서는 privateCount에 직접 접근할 수 없다.
// console.log(privateCount); // ReferenceError: privateCount is not defined
함수와 비교
| 구분 | 함수 | 클로저 |
|---|---|---|
| 개념 | 코드 블록 | 자신이 선언된 환경을 기억하는 함수 |
| 변수 생명 주기 | 함수 실행 시 생성, 종료 시 소멸 | 외부 함수의 변수라도, 클로저가 참조하면 계속 유지됨 |
| 상태 유지 | 호출 간 상태를 유지하지 않음(Stateless) | 상태를 유지할 수 있음 (Stateful) |
| 주요 용도 | 특정 기능의 재사용 | 데이터 은닉(private 변수 흉내), 상태 유지, 함수 조합 |
클로저 선언과 축약 표현
클로저 선언
예를 들어서, x+y 동작을 한다면 기본적으로는 이렇게 선언한다.
const adder(x) => {
// x는 adder 함수의 외부 변수
return (y) => {
// 반환되는 이 함수가 바로 클로저
return x + y;
};
};
const add5 = adder(5); // x가 5로 고정된 클로저
console.log(add5(10)); // 15 (5 + 10)
축약 표현
화살표 함수를 사용해 간결하게 만들 수 있다.
const adder = (x) => (y) => x + y; // 축약
const add5 = adder(5);
console.log(add5(10)); // 15 (5 + 10)