설계Hard#050
함수형 프로그래밍의 핵심 개념(순수 함수, 불변성, 고차 함수)을 설명하고 실무 적용 예를 들어주세요.
#설계#함수형프로그래밍#FP
답변 포인트
사이드 이펙트 없음, 데이터 불변, 함수를 값으로 다루는 특성을 생각해보세요.
정답 및 해설
빠른 요약
순수 함수: 같은 입력 → 항상 같은 출력, 사이드 이펙트 없음. 테스트/예측 쉬움.
함수형 프로그래밍(Functional Programming, FP)은 프로그램을 순수 함수들의 조합으로 구성하는 프로그래밍 패러다임입니다. 상태 변화와 사이드 이펙트를 최소화하고, 데이터를 불변으로 다루는 것이 핵심입니다. JavaScript/TypeScript에서는 완전한 함수형 언어는 아니지만 FP 개념을 부분적으로 적용하여 코드의 예측 가능성과 테스트 용이성을 크게 높일 수 있습니다.
순수 함수 (Pure Function)
개념
순수 함수는 두 가지 조건을 만족하는 함수입니다.
- 동일한 입력 → 항상 동일한 출력 (참조 투명성)
- 사이드 이펙트 없음 (외부 상태를 변경하지 않음)
순수 함수 vs 비순수 함수
TypeScript
// ❌ 비순수 함수 - 외부 상태에 의존
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) // ['사과', '바나나', '딸기']순수 함수의 이점
TypeScript
// 테스트가 매우 쉬움 - 입출력만 검증하면 됨
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)
개념
불변성은 데이터를 직접 수정하지 않고, 수정이 필요한 경우 새로운 데이터를 반환하는 것입니다.
객체/배열 불변 처리
TypeScript
// ❌ 가변(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 = '부산' // 마치 직접 수정처럼 작성
// 내부적으로 불변 처리됨
})배열 불변 처리 패턴
TypeScript
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 - 불변성의 실무 예시
TypeScript
// 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)
개념
고차 함수는 함수를 인자로 받거나, 함수를 반환하는 함수입니다.
내장 고차 함수
TypeScript
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커스텀 고차 함수
TypeScript
// 함수를 반환하는 고차 함수 - 커링(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)
TypeScript
// 작은 함수들을 조합하여 복잡한 로직 구성
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 함수 컴포넌트
TSX
// 함수형 컴포넌트 - 순수 함수 형태
// 같은 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 - 함수형 상태 관리
TypeScript
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 - 반응형 함수형 프로그래밍
TypeScript
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)
})함수형 프로그래밍의 이점 정리
TypeScript
// 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 패턴 | 데이터 변환 파이프라인 |
| 커링 | 다인자 함수를 단인자 함수 체인으로 변환 | 부분 적용 활용 | 재사용 가능한 필터/변환기 |