Frontend Development
코드가 깔끔해지는 비밀: 프론트엔드에서 함수형 프로그래밍 활용하기
Kun Woo Kim
2025. 8. 18. 14:04
728x90
프론트엔드를 하다 보면 상태 변경과 데이터 변환이 정말 자주 등장한다. 이때 함수형 프로그래밍(FP)은 코드를 더 예측 가능하고 테스트하기 쉽게 만든다. 이번 글은 핵심을 짧게 정리하고, 코드 예제로 바로 확인한다. 실제로 언제 FP가 맞는지도 함께 본다.
1. 기본 개념 한 줄 요약
- 함수형 프로그래밍은 순수 함수와 불변성에 기반한 선언적 스타일이다.
- 동일 입력 → 동일 출력, 외부 상태 변경 없음. 원본은 건드리지 않고 새 값을 만든다.
2. 순수 함수 vs 부수효과, 코드로 비교
// 순수: 동일 입력이면 항상 동일 출력, 외부 상태 변경 없음
function add(a: number, b: number): number {
return a + b;
}
// 비순수: 외부 상태(total)를 변경 → 테스트와 예측이 어려워짐
let total = 0;
function accumulate(xs: number[]) {
for (const x of xs) total += x; // 외부 변수 수정(부수효과)
}
3. 불변성: 원본을 바꾸지 않고 새 값을 만든다
const numbers = [1, 2, 3];
const newNumbers = numbers.concat(4); // 또는 [...numbers, 4]
// numbers는 그대로, newNumbers는 새로운 배열
배열/객체 업데이트에서도 동일하다.
type Todo = { id: number; title: string; done: boolean };
const todos: Todo[] = [
{ id: 1, title: 'learn fp', done: false },
{ id: 2, title: 'write code', done: false },
];
// 항목 토글(원본 유지)
const toggle = (id: number) => (t: Todo) =>
t.id === id ? { ...t, done: !t.done } : t;
const updated = todos.map(toggle(1));
안전한 정렬도 복사 후 처리한다.
const xs = [3, 1, 2];
const sorted = [...xs].sort((a, b) => a - b); // 원본 보존
4. 고차 함수·커링·합성: 작은 함수를 연결해 큰 일을 한다
고차 함수는 함수를 인자로 받거나 반환한다.
function withLogging<T>(fn: (x: T) => T) {
return (x: T) => {
console.log('input:', x);
const result = fn(x);
console.log('output:', result);
return result;
};
}
커링/부분 적용은 인자를 부분적으로 고정해 재사용성을 높인다.
const multiply = (a: number) => (b: number) => a * b;
const double = multiply(2);
double(10); // 20
합성은 여러 함수를 파이프라인으로 잇는다.
const pipe = <T>(...fns: Array<(arg: T) => T>) => (initial: T) =>
fns.reduce((acc, fn) => fn(acc), initial);
const trim = (s: string) => s.trim();
const toLower = (s: string) => s.toLowerCase();
const sanitize = pipe<string>(trim, toLower);
sanitize(' HeLLo '); // 'hello'
선언적 데이터 변환 예시도 비슷하다.
const unique = (xs: number[]) => Array.from(new Set(xs));
const removeNeg = (xs: number[]) => xs.filter((x) => x >= 0);
const sortAsc = (xs: number[]) => [...xs].sort((a, b) => a - b);
const normalizeNumbers = pipe<number[]>(removeNeg, unique, sortAsc);
normalizeNumbers([3, -1, 2, 3, 2]); // [2, 3]
5. 실제로 언제 FP가 더 자연스러운가
- 입력이 같으면 결과가 같아야 하는 계산 로직
- 외부 의존성을 분리해 단위 테스트를 간단히 하고 싶은 경우
- 공유 상태 변경으로 인한 버그를 줄이고 싶은 경우
성능은 보통 큰 차이가 없다. 다만, 불변 업데이트가 과도하면 복사 비용이 생긴다. 필요한 곳만 최적화하면 된다.
6. 프론트엔드 적용 시나리오
React 상태 업데이트: 불변 업데이트를 기본값으로 둔다.
// 체크된 ID 집합 토글러(불변 집합) const toggleChecked = (id: number) => (set: Set<number>) => { const next = new Set(set); next.has(id) ? next.delete(id) : next.add(id); return next; };
이벤트 핸들러 구성: 커링으로 인자 고정 후 재사용한다.
const handleChangeFor = (field: 'email' | 'password') => (value: string) => ({ field, value, });
API 응답 정규화: map/filter/reduce로 선언적으로 변환한다.
type User = { id: number; name: string; active: boolean }; const activeUserNames = (users: User[]) => users.filter((u) => u.active).map((u) => u.name).sort();
7. 실무 팁과 흔한 실수
- 팁
- 파이프라인 유틸(
pipe
)을 모듈화해 중복을 없앤다. - 데이터 변경이 잦다면 구조를 단순화해 얕은 복사 비용을 줄인다.
- 타입을 명확히 해 합성 과정에서 타입 안정성을 확보한다.
- 파이프라인 유틸(
- 흔한 실수
- 원본을 바꾸는 메서드(
sort
,splice
등)를 그대로 사용 - 과한 커링/합성으로 가독성 저하
- 함수 내부에서 날짜/랜덤/네트워크 호출로 순수성을 깨뜨림
- 원본을 바꾸는 메서드(
핵심은 작고 순수한 함수를 안전하게 조합하는 것이다. 프론트엔드 데이터 처리에 이 원칙을 적용하면, 유지보수성과 품질이 안정적으로 올라간다.
728x90