icon안동민 개발노트

순수 함수와 불변성


 순수 함수와 불변성은 함수형 프로그래밍의 핵심 원칙으로, 코드의 예측 가능성과 유지보수성을 높이는 데 중요한 역할을 합니다.

 타입스크립트에서 이러한 개념을 적용하면 타입 안정성과 함께 더욱 견고한 애플리케이션을 구축할 수 있습니다.

순수 함수의 개념과 구현

 순수 함수는 다음 특징을 가집니다.

  1. 동일한 입력에 대해 항상 동일한 출력을 반환합니다.
  2. 부수 효과가 없습니다.
  3. 외부 상태에 의존하지 않습니다.

 타입스크립트로 구현한 순수 함수 예시

function add(a: number, b: number): number {
    return a + b;
}
 
function capitalizeString(str: string): string {
    return str.charAt(0).toUpperCase() + str.slice(1);
}

불변성(Immutability) 구현

 타입스크립트에서 불변성을 구현하는 방법

  1. const assertions
const point = { x: 10, y: 20 } as const;
// point.x = 30; // Error: Cannot assign to 'x' because it is a read-only property.
  1. readonly 접근제어자
interface ReadonlyPoint {
    readonly x: number;
    readonly y: number;
}
 
const point: ReadonlyPoint = { x: 10, y: 20 };
// point.x = 30; // Error: Cannot assign to 'x' because it is a read-only property.
  1. Object.freeze()
const frozenObject = Object.freeze({ name: "John", age: 30 });
// frozenObject.name = "Jane"; // Error in strict mode, silent failure otherwise

순수 함수와 불변 데이터 구조

 타입스크립트의 타입 시스템을 활용하여 순수 함수와 불변 데이터 구조를 정의하고 강제할 수 있습니다.

type PureFunction<T, U> = (input: Readonly<T>) => U;
 
interface ImmutableState {
    readonly count: number;
    readonly items: ReadonlyArray<string>;
}
 
const incrementCount: PureFunction<ImmutableState, ImmutableState> = (state) => ({
    ...state,
    count: state.count + 1
});
 
const addItem: PureFunction<ImmutableState & { item: string }, ImmutableState> = (state) => ({
    ...state,
    items: [...state.items, state.item]
});

부수 효과(Side Effects) 관리

 부수 효과를 제어하고 관리하는 방법

type Effect<T> = () => T;
 
function runEffect<T>(effect: Effect<T>): T {
    return effect();
}
 
function logEffect(message: string): Effect<void> {
    return () => console.log(message);
}
 
function getRandomNumber(): Effect<number> {
    return () => Math.random();
}
 
// 사용 예
runEffect(logEffect("Hello, world!"));
const randomNum = runEffect(getRandomNumber());

참조 투명성(Referential Transparency)

 참조 투명성은 프로그램의 동작을 변경하지 않고 표현식을 그 값으로 대체할 수 있는 속성입니다.

function add5(x: number): number {
    return x + 5;
}
 
// 참조 투명성 검증
const result1 = add5(10);
const result2 = 15;
 
console.assert(result1 === result2, "참조 투명성이 유지되지 않았습니다.");

영속 데이터 구조(Persistent Data Structures)

 영속 데이터 구조는 불변성을 유지하면서 효율적인 데이터 조작을 가능하게 합니다.

 타입스크립트로 간단한 영속 리스트를 구현해보겠습니다.

class PersistentList<T> {
    constructor(private head: T, private tail: PersistentList<T> | null = null) {}
 
    prepend(value: T): PersistentList<T> {
        return new PersistentList(value, this);
    }
 
    get(index: number): T | undefined {
        if (index === 0) return this.head;
        return this.tail?.get(index - 1);
    }
}
 
const list1 = new PersistentList(1);
const list2 = list1.prepend(2);
const list3 = list2.prepend(3);
 
console.log(list1.get(0), list2.get(0), list3.get(0)); // 1, 2, 3

순수 함수와 불변성을 활용한 상태 관리

 순수 함수와 불변성을 활용한 간단한 상태 관리 패턴

interface State {
    readonly count: number;
    readonly name: string;
}
 
type Action = 
    | { type: "INCREMENT" }
    | { type: "SET_NAME", payload: string };
 
function reducer(state: State, action: Action): State {
    switch (action.type) {
        case "INCREMENT":
            return { ...state, count: state.count + 1 };
        case "SET_NAME":
            return { ...state, name: action.payload };
        default:
            return state;
    }
}
 
class Store {
    constructor(private state: State, private reducer: typeof reducer) {}
 
    dispatch(action: Action): void {
        this.state = this.reducer(this.state, action);
    }
 
    getState(): Readonly<State> {
        return this.state;
    }
}
 
// 사용 예
const store = new Store({ count: 0, name: "" }, reducer);
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "SET_NAME", payload: "John" });
console.log(store.getState()); // { count: 1, name: "John" }

 장점

  • 예측 가능한 상태 변화
  • 디버깅 용이성
  • 시간 여행 디버깅 가능

 단점

  • 복잡한 객체 구조에서의 성능 문제 가능성
  • 보일러플레이트 코드 증가

Best Practices와 주의사항

  1. 가능한 한 많은 함수를 순수 함수로 작성하세요.
  2. 불변 객체를 사용하여 의도치 않은 변경을 방지하세요.
  3. 깊은 불변성이 필요한 경우, 라이브러리(예 : Immer)의 사용을 고려하세요.
  4. 부수 효과를 명시적으로 관리하고 제한된 영역에서만 사용하세요.
  5. 복잡한 상태 로직은 리듀서 함수로 분리하세요.
  6. 타입 시스템을 활용하여 불변성과 순수성을 강제하세요.
  7. 참조 투명성을 유지하여 코드의 추론과 테스트를 용이하게 만드세요.
  8. 대규모 데이터 구조에는 영속 데이터 구조의 사용을 고려하세요.
  9. 성능 문제가 발생할 경우, 메모이제이션을 활용하세요.
  10. 팀 내에서 순수 함수와 불변성에 대한 가이드라인을 수립하고 공유하세요.

 순수 함수와 불변성은 타입스크립트 프로젝트의 품질을 크게 향상시킬 수 있는 강력한 개념입니다.

 이들을 적절히 활용하면 코드의 예측 가능성, 테스트 용이성, 그리고 유지보수성을 높일 수 있습니다. 타입스크립트의 정적 타입 시스템과 결합하면, 컴파일 시간에 많은 잠재적 오류를 잡아낼 수 있어 더욱 안정적인 애플리케이션을 구축할 수 있습니다.

 그러나 이러한 접근 방식이 항상 최선의 선택은 아닐 수 있습니다. 특히 성능이 중요한 부분에서는 가변 데이터 구조가 더 효율적일 수 있으며 외부 시스템과의 상호작용이 필요한 경우 순수 함수만으로는 충분하지 않을 수 있습니다.

 따라서 프로젝트의 요구사항과 제약 조건을 고려하여 적절한 균형을 찾는 것이 중요합니다.