시리즈: 개인공부 (0개 글)
함수형 프로그래밍?
어려운 것
시작하기 전에
자바스크립트에서 객체는 일급 객체고, 함수 또한 객체로 표현하기 때문에 일급 함수도 만족한다.
객체 지향에 대한 글을 읽고 오면 좋습니다.
- 함수를 변수에 할당할 수 있음
- 함수를 객체에 할당할 수 있음
- 다른 함수에 함수를 인수로 전달할 수 있음
- 함수가 함수를 반환할 수 있음 ( 앗.. 콜백지옥 앗…) 즉 참조 값을 쓸 수 있는 곳이라면 어디서든 함수를 사용할 수 있다.
일급 시민(First Class Citizen)
- 변수에 담을 수 있다.
- 인자로 전달할 수 있다.
- 반환값으로 전달할 수 있다.
일급 함수(First Class Function)
함수를 일급 시민으로 취급한다는 것.
함수형 프로그래밍
정의
함수를 일급 시민으로 간주하고, 프로그램의 상태 변경 및 부수 효과를 최소화하는 프로그래밍 패러다임
핵심 원리
수학적 함수에 기반하여, 동일한 입력에 대해 항상 동일한 출력을 보장하고 외부 상태를 변경하지 않는 순수 함수 작성을 지향. 데이터 불변성 유지 및 부수 효과 격리를 통해 프로그램의 예측 가능성 및 유지보수성 향상을 목표로 함.
주요 이점
- 높은 예측 가능성: 순수 함수의 특성으로 인해 코드의 동작을 쉽게 예측하고 이해할 수 있음.
- 용이한 디버깅: 부수 효과가 격리되어 있어 오류 발생 지점을 추적하고 수정하기 용이함.
- 병렬 처리 용이성: 외부 상태에 의존성이 낮아 여러 작업을 동시에 안전하게 처리할 수 있어 멀티코어 환경에서 성능 향상에 유리함.
- 코드 재사용성 증대: 순수하고 독립적인 함수는 다양한 상황에서 재사용하기 용이하여 개발 효율성을 높임.
- 테스트 용이성: 외부 의존성이 없는 순수 함수는 독립적인 단위 테스트를 수행하기에 적합함.
핵심 개념
1. 순수 함수(Pure Functions)
- 동일한 입력에 대해 항상 동일한 출력을 반환하는 함수
- 함수 외부의 어떤 상태에도 의존하지 않으며, 함수 외부의 상태를 변경하지 않음
- 이를 부수효과가 없다고 한다.
- 즉 수학적 함수의 정의와 일치.
예시.
// 입력 값에 대해서만 결과를 반환하며, 외부 상태를 변경하지 않는 순수 함수
function add(a, b) {
return a + b;
}
// 순수 함수가 아닌 예시 (외부 변수 'count'에 의존)
let count = 0; // <- 이 변수에 의존.
function impureAdd(a) {
count += a; // 외부 상태 'count'를 변경 (부수 효과)
return count;
}
- 수학적 계산, 데이터 변환 등 입력에 따라 결정적인 결과를 도출해야하는 로직에 사용
- 컴포넌트 렌더링 로직( 예 : React의 순수 컴포넌트 )과 같이 예측 가능한 UI 업데이트
사용 예시
- Lodash
- Ramda 와 같은 함수형 프로그래밍 라이브러리의 대부분의 함수는 순수 함수로 설계되어 데이터 처리의 안정성을 높임
주의
- 함수 내부에서 전역 변수나 외부 환경 설정과 같은 외부 상태를 읽거나 수정하는 행위는 순수 함수를 위반함.
- 콘솔 출력, 파일 시스템 접근, 네트워크 요청 등 부수 효과를 발생시키는 코드는 순수 함수 내부에 포함되어서는 안 됨. 이러한 부수 효과는 별도의 방식으로 관리해야 함.
2. 불변성 (Immutability)
- 생성된 데이터는 그 이후로 변경될 수 없다는 원칙.
- 데이터에 변화를 주려면 기존 데이터를 복사하여 새로운 데이터를 생성해야 함.
- 객체나 배열과 같은 참조 타입의 경우, 내부 속성이나 요소가 변경되지 않음을 의미함.
// 불변성을 유지하며 배열에 새로운 요소 추가
const array = [1, 2, 3];
const newArray = [...array, 4]; // 전개 연산자를 사용하여 기존 배열을 복사하고 새로운 요소 추가
console.log(array); // 출력: [1, 2, 3] (기존 배열은 변경되지 않음)
console.log(newArray); // 출력: [1, 2, 3, 4] (새로운 배열 생성)
// 불변성을 위반하는 예시 (기존 배열 직접 수정)
const mutableArray = [1, 2, 3];
mutableArray.push(4); // 기존 배열이 직접 변경됨
console.log(mutableArray); // 출력: [1, 2, 3, 4]
- 상태 관리 시스템 (예: Redux, Vuex)에서 상태 변화를 추적하고 관리하는 데 필수적인 개념. 불변성을 통해 이전 상태와 현재 상태를 쉽게 비교하고, 시간 여행과 같은 디버깅 기능을 구현할 수 있음.
-
동시성 환경에서 데이터 경쟁 조건을 방지하고 예측 가능한 프로그램 동작을 보장함.
- 객체나 배열의 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)의 차이를 정확히 이해하고 사용해야 함. 얕은 복사는 최상위 레벨의 불변성만 보장하며, 중첩된 객체나 배열은 여전히 변경될 수 있음.
- 불변성을 유지하기 위해 데이터를 복사하는 과정은 때때로 성능상의 오버헤드를 발생시킬 수 있으므로, 상황에 따라 적절한 전략을 선택해야 함.
3. 고차 함수(Higher-Order Functions)
- 다른 함수를 인자로 받거나, 함수를 반환하는 함수.
- 함수를 값처럼 다룰 수 있는 자바스크립트의 일급 함수 특성을 활용하는 핵심적인 개념.
- 코드의 추상화 수준을 높이고, 재사용 가능한 패턴을 만들 수 있도록 지원함.
// 배열의 각 요소에 주어진 함수를 적용하여 새로운 배열을 반환하는 고차 함수 'map'
const numbers = [1, 2, 3, 4];
const doubled = numbers.map((num) => num * 2); // 익명 함수를 인자로 전달
console.log(doubled); // 출력: [2, 4, 6, 8]
// 함수를 반환하는 고차 함수
function multiplier(factor) {
return function (number) {
return number * factor;
};
}
const double = multiplier(2);
console.log(double(5)); // 출력: 10
// 클로저..!?
- 배열 처리 (map, filter, reduce 등), 이벤트 처리, 콜백 함수, Promise 등 비동기 처리 패턴에서 광범위하게 사용됨.
-
특정 로직을 함수로 추상화하여 코드의 유연성과 확장성을 높임.
- React의
useEffect훅은 의존성 배열과 콜백 함수를 인자로 받는 고차 함수의 한 예시로, 컴포넌트의 생명주기 관리를 추상화함. - Vue의
watch옵션 역시 감시할 데이터와 콜백 함수를 인자로 받아 데이터 변화에 대한 반응형 로직을 구현하는 데 사용됨.
고차 함수 내부에서 콜백 함수가 실행될 때 this 바인딩이 예상과 다르게 작동할 수 있으므로 주의해야 함. 화살표 함수를 사용하거나 .bind() 메서드를 활용하여 this 컨텍스트를 명시적으로 설정해야 할 수 있음.
4. 함수 합성 (Function Composition)
- 여러 개의 작은 함수들을 결합하여 더 복잡한 기능을 수행하는 새로운 함수를 만드는 기법.
- 각 함수는 특정 작업을 수행하며, 이들을 순차적으로 연결하여 데이터 처리 파이프라인을 구축함.
- 코드의 가독성을 높이고, 각 함수의 역할을 명확하게 분리하여 유지보수를 용이하게 함.
// 두 개의 함수를 합성하는 함수 'compose'
const compose = (f, g) => (x) => f(g(x));
// 각자 독립적인 기능을 수행하는 두 개의 함수
const addOne = (x) => x + 1;
const double = (x) => x * 2;
// 'addOne' 함수를 먼저 실행하고, 그 결과를 'double' 함수의 입력으로 사용
const addOneThenDouble = compose(double, addOne);
console.log(addOneThenDouble(5)); // (5 + 1) * 2 = 12
// 여러 함수를 연속적으로 합성할 수도 있음
const triple = (x) => x * 3;
const addOneThenDoubleThenTriple = compose(triple, addOneThenDouble);
console.log(addOneThenDoubleThenTriple(5)); // ((5 + 1) * 2) * 3 = 36
- 데이터 전처리, 유효성 검사, 로깅 등 여러 단계를 거쳐 데이터를 처리해야 하는 상황에서 유용함.
- 함수형 리액티브 프로그래밍 (FRP) 패러다임에서 이벤트 스트림을 변환하고 결합하는 데 핵심적인 역할을 수행함.
- 함수 합성 시 함수의 실행 순서가 중요하며, 합성되는 함수들의 입력 타입과 출력 타입이 호환되는지 확인해야 함.
- 과도한 함수 합성은 코드의 흐름을 파악하기 어렵게 만들어 디버깅을 힘들게 할 수 있으므로 적절한 수준에서 사용해야 함.
5. 커링과 부분 적용 (Currying & Partial Application)
- 커링 (Currying): 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수들의 연속으로 변환하는 기법. 각 단계에서 인자를 하나씩 받으며, 최종적으로 모든 인자가 제공되면 원래 함수의 결과를 반환함.
-
부분 적용 (Partial Application): 여러 개의 인자를 받는 함수에 일부 인자만 미리 적용하여 새로운 함수를 생성하는 기법. 생성된 새로운 함수는 나머지 인자들을 받아서 원래 함수의 기능을 수행함.
- 커링은 함수의 재사용성을 높이고, 특정 인자가 미리 고정된 새로운 함수를 쉽게 생성할 수 있도록 함.
-
부분 적용은 함수의 인자 중 일부를 미리 설정해두고, 나머지 인자만 나중에 제공하여 함수의 사용을 간편하게 만들 수 있음. 특히 이벤트 핸들러나 콜백 함수를 생성할 때 유용함.
- 커링과 부분 적용은 함수를 추상화하는 정도가 높아 처음 접하는 개발자에게는 다소 어렵게 느껴질 수 있음. (저도 사실 이해 못함)
- 과도하게 사용하면 코드의 가독성을 저해할 수 있으므로, 명확하고 이해하기 쉬운 코드를 작성하는 데 주의를 기울여야 함.
// 정리.
// 1. 순수 함수 정의
function add(a, b) {
return a + b; // 순수 함수: 동일한 입력이면 항상 동일한 결과
}
// 2. 불변성을 지키는 데이터 처리
const numbers = [1, 2, 3, 4, 5];
const incrementedNumbers = numbers.map((num) => num + 1); // 기존 배열 numbers는 변경되지 않음
// 3. 고차 함수 사용: filter, map, reduce 등
const evenNumbers = numbers.filter((num) => num % 2 === 0);
const squaredNumbers = numbers.map((num) => num * num);
const sumOfNumbers = numbers.reduce((acc, num) => acc + num, 0);
// 4. 함수 합성: 여러 함수를 결합하여 새로운 함수를 생성
const compose = (f, g) => (x) => f(g(x));
const addTwo = (x) => x + 2;
const multiplyByThree = (x) => x * 3;
// 먼저 addTwo, 다음 multiplyByThree
const composedFunction = compose(multiplyByThree, addTwo);
const result = composedFunction(4); // (4 + 2) * 3 = 18
// 5. 커링을 활용한 함수 재사용
const curriedMultiply = (x) => (y) => x * y;
const triple = curriedMultiply(3);
const tripleResult = triple(5); // 15
// 결과 출력 (콘솔 환경에서는 잘 동작하나, 브라우저 콘솔에서도 동일하게 확인 가능)
console.log("Sum of Numbers:", sumOfNumbers);
console.log("Even Numbers:", evenNumbers);
console.log("Squared Numbers:", squaredNumbers);
console.log("Composed Function Result:", result);
console.log("Triple Result:", tripleResult);
그래서 왜 쓴다고요?
순수 함수의 특성 덕분에 동일한 입력에 대해서는 항상 동일한 출력을 보장하며, 외부 상태를 변경하는 부수 효과가 없어 코드의 동작을 명확하게 예측할 수 있음…
- 쉬운 테스트 : 순수 함수는 외부 의존성이 없기 때문
- 코드 재사용성 : 순수 함수는 특정 맥락에 강한 결합을 하지 않음. 즉 다양한 상황에서의 재사용 가능
- 병렬처리 + 동시성 안정성 : 부수효과 X & 상태변경 X => 여러 작업의 안정적 처리.