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