전체 목록
설계Hard#50

함수형 프로그래밍의 핵심 개념(순수 함수, 불변성, 고차 함수)을 설명하고 실무 적용 예를 들어주세요.

#설계#함수형프로그래밍#FP
힌트

사이드 이펙트 없음, 데이터 불변, 함수를 값으로 다루는 특성을 생각해보세요.

정답 및 해설

함수형 프로그래밍의 핵심 개념(순수 함수, 불변성, 고차 함수)을 설명하고 실무 적용 예를 들어주세요.

함수형 프로그래밍(Functional Programming, FP)은 프로그램을 순수 함수들의 조합으로 구성하는 프로그래밍 패러다임입니다. 상태 변화와 사이드 이펙트를 최소화하고, 데이터를 불변으로 다루는 것이 핵심입니다. JavaScript/TypeScript에서는 완전한 함수형 언어는 아니지만 FP 개념을 부분적으로 적용하여 코드의 예측 가능성과 테스트 용이성을 크게 높일 수 있습니다.

순수 함수 (Pure Function)

개념

순수 함수는 두 가지 조건을 만족하는 함수입니다.

  1. 동일한 입력 → 항상 동일한 출력 (참조 투명성)
  2. 사이드 이펙트 없음 (외부 상태를 변경하지 않음)

순수 함수 vs 비순수 함수

// ❌ 비순수 함수 - 외부 상태에 의존
let tax = 0.1

function calculateTotal(price: number): number {
  return price * (1 + tax)  // 외부 변수 tax에 의존
  // tax가 변하면 같은 입력에도 다른 결과
}

// ❌ 비순수 함수 - 사이드 이펙트 발생
let totalRevenue = 0

function processOrder(price: number): number {
  totalRevenue += price  // 외부 상태 변경
  console.log(`처리됨: ${price}`)  // 콘솔 출력 = 사이드 이펙트
  return price * 1.1
}

// ✅ 순수 함수 - 동일 입력 → 동일 출력, 사이드 이펙트 없음
function calculateTotal(price: number, taxRate: number): number {
  return price * (1 + taxRate)  // 외부 상태 불필요
}

// 동일 입력 → 항상 동일 결과
console.log(calculateTotal(1000, 0.1))  // 항상 1100
console.log(calculateTotal(1000, 0.1))  // 항상 1100

// ✅ 순수 함수 - 배열 처리
function addItem(items: string[], newItem: string): string[] {
  return [...items, newItem]  // 원본 배열 수정 없이 새 배열 반환
}

const fruits = ['사과', '바나나']
const moreFruits = addItem(fruits, '딸기')
console.log(fruits)      // ['사과', '바나나'] (원본 불변)
console.log(moreFruits)  // ['사과', '바나나', '딸기']

순수 함수의 이점

// 테스트가 매우 쉬움 - 입출력만 검증하면 됨
describe('calculateTotal', () => {
  it('세금 포함 합계를 계산한다', () => {
    expect(calculateTotal(1000, 0.1)).toBe(1100)
    expect(calculateTotal(2000, 0.2)).toBe(2400)
    // 외부 상태 설정 필요 없음, mock 불필요
  })
})

// 참조 투명성 - 함수를 결과값으로 대체 가능
// calculateTotal(1000, 0.1) === 1100 이 항상 성립
// 코드 추론이 쉬워짐

// 메모이제이션 최적화 가능
const memoize = <T extends unknown[], R>(fn: (...args: T) => R) => {
  const cache = new Map<string, R>()
  return (...args: T): R => {
    const key = JSON.stringify(args)
    if (cache.has(key)) return cache.get(key)!
    const result = fn(...args)
    cache.set(key, result)
    return result
  }
}

const memoizedCalculate = memoize(calculateTotal)
// 동일 인자는 캐시에서 즉시 반환 (순수 함수이므로 캐싱 안전)

불변성 (Immutability)

개념

불변성은 데이터를 직접 수정하지 않고, 수정이 필요한 경우 새로운 데이터를 반환하는 것입니다.

객체/배열 불변 처리

// ❌ 가변(Mutable) - 원본 데이터 직접 수정
const user = { name: '김철수', age: 25, role: 'user' }

function promoteToAdmin(user: User): void {
  user.role = 'admin'  // 원본 객체 변경 → 참조하는 곳 모두 영향
}

promoteToAdmin(user)
console.log(user.role)  // 'admin' - 원본 변경됨

// ✅ 불변(Immutable) - 새 객체 반환
function promoteToAdmin(user: User): User {
  return { ...user, role: 'admin' }  // 새 객체 반환, 원본 불변
}

const adminUser = promoteToAdmin(user)
console.log(user.role)      // 'user' - 원본 그대로
console.log(adminUser.role) // 'admin' - 새 객체

// 중첩 객체의 불변 처리
const state = {
  user: { name: '김철수', address: { city: '서울', zip: '12345' } },
  cart: ['item1', 'item2']
}

// ❌ 중첩 객체 직접 수정
state.user.address.city = '부산'  // 원본 변경

// ✅ 중첩 불변 처리 (Spread Operator)
const newState = {
  ...state,
  user: {
    ...state.user,
    address: {
      ...state.user.address,
      city: '부산'
    }
  }
}

// ✅ Immer 라이브러리로 불변 처리 간소화
import produce from 'immer'

const newState = produce(state, draft => {
  draft.user.address.city = '부산'  // 마치 직접 수정처럼 작성
  // 내부적으로 불변 처리됨
})

배열 불변 처리 패턴

const items = [1, 2, 3, 4, 5]

// ❌ 원본 배열 변경 메서드
items.push(6)          // 원본 변경
items.splice(0, 1)     // 원본 변경
items.sort()           // 원본 변경

// ✅ 새 배열 반환 메서드 (불변)
const added = [...items, 6]                          // push 대신
const removed = items.filter((_, i) => i !== 0)     // splice 대신
const sorted = [...items].sort()                     // sort 전에 복사

// 항목 업데이트
const updatedItems = items.map(item =>
  item === 3 ? 99 : item  // 3을 99로 교체
)

// 항목 삽입
const insertedItems = [
  ...items.slice(0, 2),  // 앞부분
  99,                     // 삽입할 항목
  ...items.slice(2)       // 뒷부분
]

Redux Reducer - 불변성의 실무 예시

// Redux Reducer는 반드시 불변성을 지켜야 함
interface CartState {
  items: CartItem[]
  total: number
}

// ❌ 잘못된 Reducer - 원본 상태 변경
function cartReducer(state: CartState, action: Action): CartState {
  if (action.type === 'ADD_ITEM') {
    state.items.push(action.payload)  // 원본 상태 변경!
    return state  // 동일 참조 반환 → React가 변경 감지 못함
  }
  return state
}

// ✅ 올바른 Reducer - 새 상태 반환
function cartReducer(state: CartState, action: Action): CartState {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.payload],
        total: state.total + action.payload.price
      }
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload.id),
        total: state.total - action.payload.price
      }
    case 'UPDATE_QUANTITY':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: action.payload.quantity }
            : item
        )
      }
    default:
      return state
  }
}

고차 함수 (Higher-Order Function)

개념

고차 함수는 함수를 인자로 받거나, 함수를 반환하는 함수입니다.

내장 고차 함수

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// map: 각 요소를 변환하여 새 배열 반환
const doubled = numbers.map(n => n * 2)
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

// filter: 조건에 맞는 요소만 새 배열로 반환
const evens = numbers.filter(n => n % 2 === 0)
// [2, 4, 6, 8, 10]

// reduce: 배열을 하나의 값으로 누산
const sum = numbers.reduce((acc, n) => acc + n, 0)
// 55

// 조합하여 사용
const sumOfSquaredEvens = numbers
  .filter(n => n % 2 === 0)         // [2, 4, 6, 8, 10]
  .map(n => n * n)                   // [4, 16, 36, 64, 100]
  .reduce((acc, n) => acc + n, 0)   // 220

// find, every, some
const firstOver5 = numbers.find(n => n > 5)    // 6
const allPositive = numbers.every(n => n > 0)  // true
const hasOdd = numbers.some(n => n % 2 !== 0)  // true

커스텀 고차 함수

// 함수를 반환하는 고차 함수 - 커링(Currying)
function multiply(a: number) {
  return function(b: number): number {
    return a * b
  }
}

const double = multiply(2)   // 함수 반환
const triple = multiply(3)

console.log(double(5))  // 10
console.log(triple(5))  // 15

// 화살표 함수로 커링
const add = (a: number) => (b: number) => a + b
const add5 = add(5)
console.log(add5(3))   // 8
console.log(add5(10))  // 15

// 실용적인 커링 예시
const filterByProperty = <T>(key: keyof T) => (value: T[keyof T]) => (array: T[]) =>
  array.filter(item => item[key] === value)

const users = [
  { name: '김철수', role: 'admin', age: 30 },
  { name: '이영희', role: 'user', age: 25 },
  { name: '박민준', role: 'admin', age: 28 },
]

const filterByRole = filterByProperty<typeof users[0]>('role')
const getAdmins = filterByRole('admin')
console.log(getAdmins(users)) // admin 사용자만

함수 합성 (Function Composition)

// 작은 함수들을 조합하여 복잡한 로직 구성
const pipe = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
  fns.reduce((acc, fn) => fn(acc), value)

const compose = <T>(...fns: Array<(arg: T) => T>) => (value: T): T =>
  fns.reduceRight((acc, fn) => fn(acc), value)

// 순수 함수들
const trim = (str: string) => str.trim()
const toLowerCase = (str: string) => str.toLowerCase()
const removeSpaces = (str: string) => str.replace(/\s+/g, '-')
const addPrefix = (prefix: string) => (str: string) => `${prefix}-${str}`

// pipe로 조합 (왼쪽에서 오른쪽으로 실행)
const createSlug = pipe(
  trim,
  toLowerCase,
  removeSpaces,
  addPrefix('blog')
)

console.log(createSlug('  Hello World  '))  // 'blog-hello-world'

// 실제 데이터 처리 파이프라인
interface Product {
  name: string
  price: number
  category: string
  inStock: boolean
}

const products: Product[] = [
  { name: '맥북', price: 2000000, category: 'laptop', inStock: true },
  { name: '아이폰', price: 1200000, category: 'phone', inStock: false },
  { name: '에어팟', price: 250000, category: 'audio', inStock: true },
  { name: 'iPadPro', price: 1500000, category: 'tablet', inStock: true },
]

// 함수형 파이프라인으로 데이터 처리
const getAvailableLaptopPrices = (products: Product[]): number[] =>
  products
    .filter(p => p.inStock)
    .filter(p => p.category === 'laptop')
    .map(p => p.price)
    .sort((a, b) => a - b)

console.log(getAvailableLaptopPrices(products)) // [2000000]

실무 적용 예시

React 함수 컴포넌트

// 함수형 컴포넌트 - 순수 함수 형태
// 같은 props → 같은 렌더링 결과
interface UserCardProps {
  user: { name: string; email: string; avatar: string }
  onFollow: (userId: string) => void
}

// 순수 함수 컴포넌트 (memo로 불필요한 리렌더링 방지)
const UserCard = React.memo(({ user, onFollow }: UserCardProps) => {
  // useCallback으로 함수 불변성 유지
  const handleFollow = useCallback(() => {
    onFollow(user.email)
  }, [user.email, onFollow])

  return (
    <div>
      <img src={user.avatar} alt={user.name} />
      <h3>{user.name}</h3>
      <button onClick={handleFollow}>팔로우</button>
    </div>
  )
})

// 커스텀 훅 - 고차 함수 패턴
function useFilter<T>(items: T[], filterFn: (item: T) => boolean) {
  return useMemo(() => items.filter(filterFn), [items, filterFn])
}

function ProductList({ products }: { products: Product[] }) {
  const [category, setCategory] = useState('all')

  const filteredProducts = useFilter(
    products,
    useCallback(
      p => category === 'all' || p.category === category,
      [category]
    )
  )

  return (/* ... */)
}

Redux Toolkit - 함수형 상태 관리

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

// createSlice 내부에서 Immer 사용 → 불변성 자동 처리
const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [], total: 0 } as CartState,
  reducers: {
    // 마치 직접 변경하는 것처럼 작성 (Immer가 불변 처리)
    addItem(state, action: PayloadAction<CartItem>) {
      state.items.push(action.payload)
      state.total += action.payload.price
    },
    removeItem(state, action: PayloadAction<string>) {
      const index = state.items.findIndex(item => item.id === action.payload)
      if (index !== -1) {
        state.total -= state.items[index].price
        state.items.splice(index, 1)
      }
    }
  }
})

RxJS - 반응형 함수형 프로그래밍

import { fromEvent, interval } from 'rxjs'
import { map, filter, debounceTime, switchMap } from 'rxjs/operators'

// 검색 입력 처리 - 함수형 파이프라인
const searchInput = document.getElementById('search')

fromEvent(searchInput, 'input')
  .pipe(
    map((e: Event) => (e.target as HTMLInputElement).value),  // 값 추출
    filter(query => query.length > 2),                         // 2글자 이상
    debounceTime(300),                                         // 300ms 디바운스
    switchMap(query => fetch(`/api/search?q=${query}`)),        // API 호출 (이전 요청 취소)
    switchMap(response => response.json())                      // JSON 파싱
  )
  .subscribe(results => {
    renderResults(results)
  })

함수형 프로그래밍의 이점 정리

// 1. 예측 가능성 - 같은 입력 → 항상 같은 결과
// 2. 테스트 용이성 - 순수 함수는 외부 의존 없이 테스트 가능
// 3. 병렬 처리 안전 - 상태 변경 없으므로 경쟁 조건 없음
// 4. 디버깅 쉬움 - 어느 단계에서 문제 발생했는지 추적 쉬움
// 5. 재사용성 - 작은 순수 함수들은 어디서나 재사용 가능

// 예시: map, filter, reduce는 어디서든 동일하게 동작
const processData = <T, R>(
  data: T[],
  transform: (item: T) => R,
  predicate: (item: R) => boolean
): R[] => data.map(transform).filter(predicate)

// 다양한 데이터 타입에 재사용 가능
processData(users, user => user.age, age => age >= 18)
processData(products, p => p.price, price => price < 50000)

정리 표

개념정의핵심 규칙실무 적용
순수 함수동일 입력 → 동일 출력, 사이드 이펙트 없음외부 상태 의존/변경 금지유틸 함수, 계산 로직, Reducer
불변성데이터를 직접 수정하지 않고 새 데이터 반환spread, map, filter 활용Redux State, React Props
고차 함수함수를 인자로 받거나 반환하는 함수map, filter, reduce, 커링데이터 처리 파이프라인
함수 합성작은 함수를 조합해 복잡한 로직 구성pipe, compose 패턴데이터 변환 파이프라인
커링다인자 함수를 단인자 함수 체인으로 변환부분 적용 활용재사용 가능한 필터/변환기