Blog
Anki
소프트웨어 아키텍처

순수 함수와 함수형 프로그래밍

순수 함수란 무엇인가?

꼬리 질문
  • 순수 함수의 두 가지 필수 조건은 무엇인가?
  • 왜 순수 함수가 테스트하기 쉬운가?
  • 순수 함수가 아닌 경우를 어떻게 판별하는가?
답변 보기
  • 핵심 포인트 💡:
    • 동일한 입력에 대해 항상 동일한 출력 반환
    • 부수 효과(Side Effect) 없음 - 외부 상태 변경하지 않음
    • 예측 가능하고 테스트하기 쉬움
    • 함수형 프로그래밍의 핵심 개념
  • 순수 함수의 정의
    • 함수형 프로그래밍의 핵심 개념으로, 두 가지 조건을 만족하는 함수입니다
    • 조건 1: 참조 투명성 - 같은 인자를 전달하면 언제나 같은 값을 반환
      • 함수 외부의 상태나 시간에 영향을 받지 않음
      • 실행 시점과 무관하게 결과가 일정함
    • 조건 2: 부수 효과 없음 - 함수 실행이 외부 상태를 변경하지 않음
      • 전역 변수 수정 ❌
      • 파일 쓰기 ❌
      • 화면/콘솔 출력 ❌
      • 네트워크 요청 ❌
      • 데이터베이스 변경 ❌
  • 순수 함수 예시
// ✅ 순수 함수: 같은 입력에 같은 출력, 부수 효과 없음
function add(a, b) {
  return a + b;
}
 
add(2, 3); // 5
add(2, 3); // 5 - 항상 같은 결과
 
// ✅ 순수 함수: 배열을 변경하지 않고 새 배열 반환
function addItem(array, item) {
  return [...array, item]; // 원본 배열을 변경하지 않음
}
 
const original = [1, 2, 3];
const newArray = addItem(original, 4);
// original: [1, 2, 3] - 원본 유지
// newArray: [1, 2, 3, 4]
  • 비순수 함수 예시
// ❌ 비순수: 외부 변수에 의존 (부수 효과 발생)
let count = 0;
 
function increment() {
  count++; // 외부 상태 변경
  return count;
}
 
increment(); // 1
increment(); // 2 - 같은 입력(없음)에도 다른 출력
 
// ❌ 비순수: 부수 효과가 있는 함수
function addAndLog(a, b) {
  console.log(`Adding ${a} + ${b}`); // 부수 효과: 콘솔 출력
  return a + b;
}
  • 순수 함수의 장점
    • 테스트 용이성: 입력만 제어하면 되므로 단위 테스트 작성이 쉬움
    • 디버깅 편의성: 외부 상태를 추적할 필요 없이 입력과 출력만 확인
    • 병렬 처리 안전: 외부 상태 변경이 없어 동시 실행 가능
    • 재사용성: 독립적이어서 다양한 컨텍스트에서 사용 가능
    • 캐싱 최적화: 메모이제이션(memoization)으로 성능 향상 가능

출처: 순수 함수 (Pure Function) - minsone.github.io (opens in a new tab)

출처: 함수형 프로그래밍 - 위키백과 (opens in a new tab)

출처: 순수함수에 대해 (불변성과 Side effect) (opens in a new tab)


왜 모든 함수를 순수 함수로 만들 수 없는가?

꼬리 질문
  • 실제 애플리케이션에서 필수적인 부수 효과는 무엇인가?
  • 부수 효과 없이 프로그램을 작성할 수 있는가?
  • 함수형 프로그래밍은 부수 효과를 어떻게 다루는가?
답변 보기
  • 핵심 포인트 💡:
    • 프로그램이 외부 세계와 상호작용하려면 부수 효과 필수
    • I/O, 네트워크, DB 작업은 본질적으로 부수 효과를 동반
    • 함수형 프로그래밍은 부수 효과를 제거하는 것이 아니라 격리하고 관리하는 것
  • 부수 효과가 필수적인 상황들
    • 사용자 입력 처리
      • 키보드, 마우스 이벤트 리스닝
      • 폼 데이터 수집
    • 데이터 저장
      • 데이터베이스 쓰기
      • 로컬 스토리지 저장
      • 파일 시스템 쓰기
    • 네트워크 통신
      • REST API 호출
      • WebSocket 통신
      • GraphQL 쿼리
    • 화면 출력
      • DOM 조작
      • 콘솔 로그
      • 알림 표시
    • 시간/난수 의존
      • Date.now() 호출
      • Math.random() 사용
      • 타이머 설정
  • 실제 애플리케이션의 현실
// 실제 웹 애플리케이션의 필수 부수 효과들
async function registerUser(userData) {
  // 부수 효과 1: 사용자 입력 검증 (DOM 읽기)
  const formData = document.getElementById('userForm').value;
 
  // 부수 효과 2: 네트워크 요청
  const response = await fetch('/api/users', {
    method: 'POST',
    body: JSON.stringify(userData)
  });
 
  // 부수 효과 3: 데이터베이스 저장
  const user = await response.json();
 
  // 부수 효과 4: UI 업데이트
  document.getElementById('message').textContent = '회원가입 완료!';
 
  // 부수 효과 5: 로컬 스토리지 저장
  localStorage.setItem('userId', user.id);
 
  return user;
}
  • 함수형 프로그래밍의 철학
    • ❌ 부수 효과를 완전히 제거하는 것이 목표가 아님
    • ✅ 부수 효과를 프로그램의 가장자리로 밀어내고 격리하는 것
    • ✅ 핵심 비즈니스 로직은 순수하게 유지
    • ✅ 부수 효과는 명확히 표시하고 최소화

출처: (번역) 함수형 프로그래밍이란 무엇인가? (opens in a new tab)

출처: 함수형 프로그래밍이란? (opens in a new tab)


순수 함수와 비순수 함수를 함께 사용하는 방법은?

꼬리 질문
  • 부수 효과를 프로그램 경계로 밀어낸다는 것의 의미는?
  • 불변성을 유지하는 구체적인 방법은?
  • React, Redux에서는 어떻게 적용되는가?
  • fp-ts 같은 함수형 라이브러리는 어떻게 도움이 되는가?
답변 보기
  • 핵심 포인트 💡:
    • 핵심 비즈니스 로직은 순수 함수로, 부수 효과는 프로그램 경계에 배치
    • 불변성(Immutability) 유지: 원본 데이터를 변경하지 않고 새 값 반환
    • 순수/비순수 함수를 명확히 분리하여 관리
    • IO, Either, Task 같은 타입으로 부수 효과 캡슐화
  • 패턴 1: 부수 효과를 프로그램의 경계로 밀어내기
    • 핵심 로직(Core Logic)은 순수 함수로 작성
    • 입출력, 네트워크, DB 작업은 프로그램의 진입점/출력부에만 배치
    • Functional Core, Imperative Shell 패턴
// ✅ 순수 함수: 비즈니스 로직만 담당
function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}
 
function applyDiscount(total, discountRate) {
  return total * (1 - discountRate);
}
 
function formatCurrency(amount) {
  return `₩${amount.toLocaleString()}`;
}
 
// ❌ 부수 효과: 프로그램 경계에 위치 (진입점)
async function checkout(cartId, discountCode) {
  // 부수 효과: DB에서 데이터 읽기
  const items = await database.getCartItems(cartId);
  const discount = await database.getDiscount(discountCode);
 
  // ✅ 순수 함수 체인: 핵심 로직
  const total = calculateTotal(items);
  const finalAmount = applyDiscount(total, discount.rate);
  const formatted = formatCurrency(finalAmount);
 
  // 부수 효과: 결과 저장 및 출력
  await database.saveOrder({ items, total: finalAmount });
  console.log(`결제 금액: ${formatted}`);
 
  return finalAmount;
}
  • 패턴 2: 불변성(Immutability) 유지
    • 객체나 배열을 직접 수정하지 않고 새로운 값 생성
    • map, filter, reduce 같은 불변 메서드 활용
    • Spread 연산자(...) 사용
// ❌ 나쁜 예: 원본 배열/객체 변경
function addItemBad(cart, newItem) {
  cart.items.push(newItem); // 부수 효과: 원본 변경
  cart.total += newItem.price;
  return cart;
}
 
// ✅ 좋은 예: 새 객체 반환
function addItemGood(cart, newItem) {
  return {
    ...cart,
    items: [...cart.items, newItem], // 새 배열 생성
    total: cart.total + newItem.price
  };
}
 
// ✅ 불변 메서드 체인
function filterExpensiveItems(items, maxPrice) {
  return items
    .filter(item => item.price <= maxPrice) // 새 배열 반환
    .map(item => ({ ...item, discounted: true })) // 새 객체들의 배열
    .sort((a, b) => a.price - b.price); // 정렬된 새 배열
}
  • 패턴 3: 순수 함수와 부수 효과 함수 명확히 분리
// ✅ 순수 함수: 데이터 변환만 담당
function processUserData(user) {
  return {
    ...user,
    name: user.name.toUpperCase(),
    email: user.email.toLowerCase(),
    registered: true,
    createdAt: new Date().toISOString() // 시간은 인자로 받는 게 더 순수하지만 실용적 타협
  };
}
 
function validateUser(user) {
  const errors = [];
  if (!user.email.includes('@')) errors.push('Invalid email');
  if (user.password.length < 8) errors.push('Password too short');
  return { valid: errors.length === 0, errors };
}
 
// ❌ 부수 효과: 별도 함수로 명확히 분리
async function registerUser(userData) {
  // 순수 함수로 검증
  const validation = validateUser(userData);
  if (!validation.valid) {
    throw new Error(validation.errors.join(', '));
  }
 
  // 순수 함수로 데이터 변환
  const processedUser = processUserData(userData);
 
  // 부수 효과: 저장 및 알림
  await database.save(processedUser); // 부수 효과
  await emailService.sendWelcome(processedUser.email); // 부수 효과
 
  return processedUser;
}
  • 패턴 4: React에서의 적용
// ✅ 순수 컴포넌트: 렌더링 로직
function ShoppingCart({ items }) {
  // 순수 함수로 계산
  const total = items.reduce((sum, item) => sum + item.price, 0);
  const itemCount = items.length;
 
  // 순수한 JSX 반환
  return (
    <div>
      <h2>장바구니 ({itemCount}개)</h2>
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.name} - ₩{item.price}</li>
        ))}
      </ul>
      <p>총액: ₩{total}</p>
    </div>
  );
}
 
// ❌ 부수 효과: useEffect로 격리
function ShoppingCartContainer() {
  const [items, setItems] = useState([]);
 
  // 부수 효과: 데이터 페칭
  useEffect(() => {
    fetch('/api/cart')
      .then(res => res.json())
      .then(data => setItems(data)); // 상태 업데이트
  }, []);
 
  // 순수 컴포넌트 렌더링
  return <ShoppingCart items={items} />;
}
  • 패턴 5: Redux에서의 적용
// ✅ 순수 함수: Reducer
function cartReducer(state = { items: [], total: 0 }, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.payload],
        total: state.total + action.payload.price
      };
    case 'REMOVE_ITEM':
      const newItems = state.items.filter(item => item.id !== action.payload);
      return {
        ...state,
        items: newItems,
        total: newItems.reduce((sum, item) => sum + item.price, 0)
      };
    default:
      return state;
  }
}
 
// ❌ 부수 효과: Middleware (Redux Thunk)
function fetchCart() {
  return async (dispatch) => {
    // 부수 효과: API 호출
    const response = await fetch('/api/cart');
    const items = await response.json();
 
    // 순수 함수 호출: dispatch
    dispatch({ type: 'SET_ITEMS', payload: items });
  };
}
  • 패턴 6: 함수형 라이브러리 패턴 (fp-ts)
    • IO 모나드: 부수 효과를 나타내는 타입으로 캡슐화
    • Either 모나드: 에러 처리를 타입으로 표현
    • Task 모나드: 비동기 작업 표현
    • tap 패턴: 부수 효과를 수행하되 원본 값 유지
import { pipe } from 'fp-ts/function';
import * as IO from 'fp-ts/IO';
import * as IOE from 'fp-ts/IOEither';
import * as Console from 'fp-ts/Console';
 
// ✅ 순수 함수: 데이터 변환
const processOrder = (order: Order) => ({
  ...order,
  total: order.items.reduce((sum, item) => sum + item.price, 0),
  processedAt: new Date().toISOString()
});
 
// IO로 부수 효과 캡슐화
const logOrder = (order: Order): IO.IO<void> =>
  Console.log(`Processing order ${order.id}`);
 
// tap 패턴: 부수 효과를 수행하되 원본 값 유지
const saveOrderWithLogging = (order: Order) =>
  pipe(
    IOE.of(order),
    IOE.tap(() => logOrder(order)), // 로깅 부수 효과
    IOE.tapIO(() => Console.log(`Total: ${order.total}`)) // 또 다른 부수 효과
  );
 
// 실행 시점에 부수 효과 발생
const result = saveOrderWithLogging(processOrder(order))();
// Either로 에러 처리를 순수하게
import * as E from 'fp-ts/Either';
 
const validateEmail = (email: string): E.Either<string, string> =>
  email.includes('@')
    ? E.right(email)
    : E.left('Invalid email');
 
const validatePassword = (password: string): E.Either<string, string> =>
  password.length >= 8
    ? E.right(password)
    : E.left('Password too short');
 
// 순수한 함수 체인
const validateUser = (email: string, password: string) =>
  pipe(
    validateEmail(email),
    E.chain(() => validatePassword(password))
  );
  • 요약 및 베스트 프랙티스
    • ✅ 핵심 비즈니스 로직: 순수 함수로 작성
    • ✅ I/O, 네트워크, DB: 프로그램 경계(진입점/출력부)에 배치
    • ✅ 불변성 유지: Spread 연산자, map/filter/reduce 활용
    • ✅ 순수/비순수 함수 명확히 분리 및 명명
    • ✅ React: useEffect로 부수 효과 격리
    • ✅ Redux: Reducer는 순수, Middleware에서 부수 효과 처리
    • ✅ 함수형 라이브러리(fp-ts): 타입으로 부수 효과 명시적 관리
    • 트레이드오프: 100% 순수함을 추구하기보다 실용성과 가독성의 균형

출처: Jeremy's Blog - 함수형 프로그래밍이란 무엇인가? (opens in a new tab)

출처: fp-ts 공식 문서 - IO/Either/Task 패턴 (opens in a new tab)

출처: Functional Core, Imperative Shell Pattern (opens in a new tab)


불변성(Immutability)이 함수형 프로그래밍에서 중요한 이유는?

꼬리 질문
  • 불변성을 지키지 않으면 어떤 문제가 발생하는가?
  • JavaScript에서 불변성을 유지하는 방법은?
  • Immer 같은 라이브러리는 어떻게 도움이 되는가?
  • 성능 문제는 없는가?
답변 보기
  • 핵심 포인트 💡:
    • 불변성은 순수 함수의 핵심 전제 조건
    • 예측 가능한 상태 관리와 디버깅 용이성 제공
    • 원본 데이터 변경 대신 새로운 데이터 생성
    • React, Redux에서 변경 감지 최적화에 필수
  • 불변성이란?
    • 한번 생성된 데이터는 변경하지 않는 것
    • 데이터를 수정할 필요가 있으면 새로운 복사본을 생성
    • 원본 데이터는 항상 보존됨
  • 불변성을 지키지 않았을 때의 문제
// ❌ 문제 상황: 원본 배열 변경
const originalCart = [
  { id: 1, name: '사과', price: 1000 }
];
 
function addItem(cart, newItem) {
  cart.push(newItem); // 원본 변경!
  return cart;
}
 
const updatedCart = addItem(originalCart, { id: 2, name: '바나나', price: 1500 });
 
console.log(originalCart);
// [{ id: 1, name: '사과', price: 1000 }, { id: 2, name: '바나나', price: 1500 }]
// 😱 원본이 변경되어 버렸다!
 
console.log(originalCart === updatedCart); // true
// 같은 참조를 가리키므로 React는 변경을 감지하지 못함
// 실제 버그 발생 예시
function processOrders(orders) {
  const highPriorityOrders = orders.filter(o => o.priority === 'high');
  highPriorityOrders.sort((a, b) => b.amount - a.amount); // sort는 원본 변경!
 
  return {
    high: highPriorityOrders,
    all: orders // 😱 orders도 정렬되어 버림 (filter는 새 배열이지만 같은 객체 참조)
  };
}
  • JavaScript에서 불변성 유지 방법
    • Spread 연산자 (...)
// 배열 복사
const original = [1, 2, 3];
const newArray = [...original, 4]; // [1, 2, 3, 4]
// original은 [1, 2, 3] 유지
 
// 객체 복사
const user = { name: 'John', age: 30 };
const updatedUser = { ...user, age: 31 }; // { name: 'John', age: 31 }
// user는 { name: 'John', age: 30 } 유지
  • 불변 배열 메서드
const numbers = [1, 2, 3, 4, 5];
 
// ✅ 불변 메서드 (새 배열 반환)
const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10]
const evens = numbers.filter(n => n % 2 === 0); // [2, 4]
const sum = numbers.reduce((acc, n) => acc + n, 0); // 15
const sliced = numbers.slice(1, 3); // [2, 3]
const concatenated = numbers.concat([6, 7]); // [1, 2, 3, 4, 5, 6, 7]
 
// ❌ 가변 메서드 (원본 변경) - 사용 주의!
numbers.push(6); // 원본 변경
numbers.pop(); // 원본 변경
numbers.sort(); // 원본 변경
numbers.reverse(); // 원본 변경
numbers.splice(1, 1); // 원본 변경
  • 중첩 객체 불변 업데이트
// ❌ 얕은 복사의 함정
const user = {
  name: 'John',
  address: {
    city: 'Seoul',
    zipCode: '12345'
  }
};
 
const updated = { ...user };
updated.address.city = 'Busan'; // 😱 중첩 객체는 같은 참조!
 
console.log(user.address.city); // 'Busan' - 원본도 변경됨
 
// ✅ 깊은 복사
const properUpdate = {
  ...user,
  address: {
    ...user.address,
    city: 'Busan'
  }
};
 
console.log(user.address.city); // 'Seoul' - 원본 유지
console.log(properUpdate.address.city); // 'Busan'
  • React에서 불변성의 중요성
// React는 참조 동일성(===)으로 변경 감지
function ShoppingCart() {
  const [items, setItems] = useState([]);
 
  // ❌ 잘못된 방법: 원본 변경
  const addItemWrong = (newItem) => {
    items.push(newItem); // 원본 변경
    setItems(items); // React가 변경을 감지하지 못함 (같은 참조)
  };
 
  // ✅ 올바른 방법: 새 배열 생성
  const addItemRight = (newItem) => {
    setItems([...items, newItem]); // 새 배열 생성
  };
 
  // ✅ 함수형 업데이트
  const addItemBest = (newItem) => {
    setItems(prevItems => [...prevItems, newItem]);
  };
 
  return (/* ... */);
}
  • Redux에서 불변성
// ✅ Redux Reducer는 반드시 불변성 유지
function todosReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      // ✅ 새 배열 반환
      return [...state, action.payload];
 
    case 'TOGGLE_TODO':
      // ✅ map으로 새 배열 생성
      return state.map(todo =>
        todo.id === action.payload.id
          ? { ...todo, completed: !todo.completed }
          : todo
      );
 
    case 'DELETE_TODO':
      // ✅ filter로 새 배열 생성
      return state.filter(todo => todo.id !== action.payload.id);
 
    default:
      return state;
  }
}
  • Immer 라이브러리로 편리하게 불변성 유지
import produce from 'immer';
 
// ❌ 복잡한 중첩 업데이트 (순수 JS)
const state = {
  users: [
    { id: 1, name: 'John', todos: [{ id: 1, text: 'Learn React', done: false }] }
  ]
};
 
const newState = {
  ...state,
  users: state.users.map(user =>
    user.id === 1
      ? {
          ...user,
          todos: user.todos.map(todo =>
            todo.id === 1
              ? { ...todo, done: true }
              : todo
          )
        }
      : user
  )
};
 
// ✅ Immer로 간단하게
const newStateWithImmer = produce(state, draft => {
  // draft는 변경 가능한 것처럼 작성
  const user = draft.users.find(u => u.id === 1);
  const todo = user.todos.find(t => t.id === 1);
  todo.done = true;
  // Immer가 자동으로 불변 업데이트 수행
});
  • 성능 고려사항
    • 구조적 공유(Structural Sharing)
      • 변경되지 않은 부분은 기존 참조 재사용
      • 전체를 복사하지 않아 메모리 효율적
    • 얕은 비교 최적화
      • React.memo, useMemo는 참조 비교로 재렌더링 방지
      • 불변성이 있어야 정확한 변경 감지 가능
    • 성능 측정 후 최적화
      • 대부분의 경우 불변성 유지의 오버헤드는 미미
      • 정말 큰 데이터셋에서만 성능 이슈 발생 가능
      • Immer, Immutable.js 같은 라이브러리로 최적화
// React.memo는 props 참조 비교
const TodoItem = React.memo(({ todo }) => {
  console.log('Rendering TodoItem');
  return <li>{todo.text}</li>;
});
 
// ✅ 불변 업데이트 시 변경된 항목만 재렌더링
const todos = [
  { id: 1, text: 'Learn React' },
  { id: 2, text: 'Learn Redux' }
];
 
const newTodos = todos.map(todo =>
  todo.id === 1
    ? { ...todo, text: 'Learn React Hooks' } // 새 객체
    : todo // 같은 참조 재사용
);
// id: 1인 TodoItem만 재렌더링

출처: 불변성 - React 공식 문서 (opens in a new tab)

출처: Immer 공식 문서 (opens in a new tab)

출처: Redux - 불변 업데이트 패턴 (opens in a new tab)