함수형 프로그래밍의 특징 정리

seungjun.dev·2025년 7월 26일
0
post-thumbnail

불변성

데이터가 한 번 생성되면 그 이후에는 변경할 수 없는 성질

  • 데이터를 수정해야 한다면, 원본을 직접 바꾸는(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)
profile
Web FE Dev | Microsoft Student Ambassadors Alumni

0개의 댓글