
최근 면접에서 함수형 프로그래밍을 사용해야 하는 이유를 설득해 보라는 요청을 받았습니다. 제가 꾸준히 함수형 패러다임에 대해 관심을 가지고 공부를 하고 있지만, 상대방을 조리 있게 말로 설득하는 능력은 많이 부족하여 어버버거렸습니다. 그래서 이번 기회에 함수형 패러다임의 세 가지 주요 개념에 대해, 저의 생각들을 글로 정리해 보았습니다.
1. 일급 함수(First-class function)
일급 함수는 함수를 값으로서 다룬다는 것을 의미합니다. 즉, 변수에 함수를 할당할 수 있다는 뜻입니다. 변수를 값에 할당할 수 있다면 어떤 일이 일어날까요?
프로그램에서 변수를 이리저리 전달하는 것과 같이, 함수를 이곳저곳에 주고받을 수 있습니다. 주고받은 함수를 활용할 수 있으니, 자연스럽게 재사용성이 증가합니다.
리액트로 개발을 하다 보면 자식 컴포넌트에 함수를 전달하고 자식은 프로퍼티로 받은 함수를 호출하는 패턴을 종종 사용하실 것입니다.
function Parent() { function doSomething() { ... } return ( <Child onClick={doSomething} /> ); }
이러한 패턴은 자식 컴포넌트가 프로퍼티로 함수를 받을 수 있기 때문에 가능합니다.
또한 함수를 인자로 받거나 함수를 반환하는 고차 함수를 만들 수 있습니다. 함수라는 것은 값을 인자로 받고 값을 반환하는 녀석입니다. 이 “값”에 함수를 넣을 수 있으니 자연스럽게 “함수”를 받거나 “함수”를 반환하는 함수도 존재할 수 있는 것이죠.
고차 함수를 만들 수 있으면 어떤 점이 좋을까요?
1.1 조합
첫 번째로 함수를 조합할 수 있습니다.
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x); const trim = (s) => s.trim(); const toLower = (s) => s.toLowerCase(); const spacesToDash = (s) => s.replace(/\s+/g, "-"); const slugify = pipe(trim, spacesToDash, toLower); slugify(" Hello World "); // "hello-world"
위 코드는 트리밍을 하고, 소문자로 치환하고, 공백을 대쉬로 치환하는 일련의 작업을 조합을 통해 하나의 변수로 표현한 코드입니다.
원하는 동작을 자유롭게 조합할 수 있고 이를 통해 유연하고 확장가능한 프로그램을 작성할 수 있습니다.
1.2. 콜백 패턴
두 번째로 콜백 패턴을 구현할 수 있습니다.
setTimeout(() => { alert("hello, world"); }, 1000); fetch("...").then(...).catch(...);
어떠한 동작을 특정 시점에 호출하기 위해서는 함수를 인자로 받을 수 있어야 합니다.
이 외에도
React.memo
와 같은 HOC, 리액트의 훅, 다양한 Array 메소드들에도 이러한 일급 함수의 개념이 적용되어 있습니다.2. 순수 함수
순수 함수라는 것은 동일한 입력에 동일한 출력을 하는 함수를 의미합니다. 수학적 용어로 설명하자면 결정론적 함수입니다.
추가적으로, 부수효과도 일으키지 않아야 합니다.
동일한 입력에 동일한 출력을 하는 것이 왜 중요할까요? 바로 “안전”하기 때문입니다. 예를 들어, 키보드의 “a” 버튼을 눌렀을 때, “a” 가 출력되는 게 아니라 “z”가 출력된다면 어떨까요? 더 나아가서 누를 때마다 무작위의 알파벳이 출력되면 어떨까요? “a” 출력을 기대하지만 실제 동작은 그렇지 않기에 불안정합니다. 불안정은 상황을 제어하는 능력을 저하시킵니다.
사람은 누구나 안정감을 찾으려는 본능이 있습니다. 불안정이 커지면 예측할 수 없는 돌발 상황이 발생할 확률이 높아지고, 이는 신변에 위협을 가할 수 있기 때문이죠. 그래서 사람은 늘 주변 상황을 안정적으로 제어하고 싶어합니다. 안정적으로 주변을 제어하기 위해서는 특정 함수가 내가 기대하는 대로 동작하는 것을 보장해야 합니다.
순수 함수는 이러한 예측 가능성과 위기 관리 측면에서 아주 좋은 수단입니다. 일관된 동작을 보장하니까 결과를 예측하기 쉬워지고, 예측하기 쉬워지면 위기를 관리하기 쉬워집니다.
TDD를 들어보신 분이라면 “테스트하기 용이하다”는 말도 들어보셨을 것입니다. 일관된 동작을 하는 함수는 내가 원하는 동작을 수행하는지 확인하기 편리합니다. 이는 디버깅하기 쉽다는 것을 의미하고, 테스트 코드를 작성하기 위해서는 함수를 순수하게 작성하는 것이 중요합니다.
순수 함수의 개념은 리액트의 함수 컴포넌트에도 적용됩니다.
React는 작성되는 모든 컴포넌트가 순수 함수일 거라 가정합니다. 이러한 가정은 작성되는 React 컴포넌트에 같은 입력이 주어진다면 반드시 같은 JSX를 반환한다는 것을 의미합니다. - React 공식 한국어 문서
리액트에서는 순수하지 않은 컴포넌트를 검증하기 위한 엄격 모드(
<React.StrictMode>
)를 제공합니다. 이 모드를 적용하면 한번의 렌더링에 컴포넌트가 두번씩 호출됩니다.2.1. 부수효과
순수 함수는 동일한 입력에 동일한 결과를 반환합니다. 반대로 말하자면 입력이 바뀌지 않는 한 영원히 동일한 렌더링을 보여준다는 뜻입니다.
웹의 기능이 다양해지고 사용자들의 요구가 늘어남에 따라, 생동감 있는 웹을 구현하기 위해서는 상태를 변경하거나 애니메이션을 시작하는 것과 같은 변화가 필요합니다. 이 변화를 부수 효과(side effect)라고 부르며, 이러한 부수 효과는 말 그대로 렌더링 중이 아닌 사이드에서 발생합니다.
리액트에서는 이러한 부수 효과(다양한 기능)를 훅을 통해 제공하고 있습니다. 예를 들어
useState
는 컴포넌트가 시간에 따라 변할 수 있는 상태를 가지도록 만들어주고, useEffect
는 컴포넌트가 외부 시스템과 상호작용할 수 있게 해줍니다.3. 불변성
불변성이란 한 번 만든 값은 변경하지 않고, 변경해야 한다면 새롭게 만드는 것을 말합니다.
const gildong = { name: "gildong", age: 23, }; // 가변성. 암시적 변경. gildong.age += 1; // 불변성. 명시적 변경. const oldGildong = { name: "gildong", age: gildong.age + 1, };
현대의 웹 환경에서는 인터렉티브 웹을 구현하기 위해 상태라는 개념을 도입하고, 이 상태에 따라 화면이 알아서 그려지는 방식을 많이 사용합니다. 당장 React 만 봐도 그렇죠.
웹 프레임워크는 상태에 따라 화면을 그리기 위해 상태 변경을 감지하는 것이 중요합니다. 상태의 변경을 판단하기 위해서는 비교를 해야 하는데, 이때 얕은 비교를 사용합니다. 만약 객체 같은 참조형 값이라면 참조를 비교하게 됩니다.
State를 읽기 전용인 것처럼 다루세요 - 리액트 공식 한국어 문서
길동이의 나이를 암시적으로 변경한다면 리액트는 상태의 변경을 감지할 수 없습니다. 길동이는 나이를 먹어야 함에도 불구하고 화면으로는 나이를 먹지 않은 것으로 나타납니다. 이것은 우리가 원하는 동작이 아닙니다.
이러한 반응성 문제 외에도, 의도치 않게 다른 상태를 변경시킬 위험이 있습니다.
const a = [1, 2, 3]; const b = a; b.push(4); console.log(a); // [1, 2, 3, 4]
위 코드에서는
b
를 변경했지만, a
도 덩달아 변경되어버렸습니다.이렇게 가변성(암시적 변경)은 순수성을 깨트리게 되고, 예측 가능성을 저해하고, 불안정을 키우고, 위험을 관리하기 힘들어집니다.
리액트에서는 이러한 불변성의 개념을 상태와 훅의 의존성 배열에 적용하고 있습니다.
const gildongRef = useRef({ name: "gildong", age: 23, }); useEffect(() => { ... }, [gildongRef.current]); return ( <button onClick={() => (gildongRef.current.age += 1)} >add age</button> );
위 코드에서
useEffect
의 의존성 배열에는 객체인 gildongRef.current
가 들어있습니다. 이 값은 객체이며, 버튼의 동작은 가변적으로 값을 변경하고 있고, 렌더링을 트리거하지 않기 때문에 버튼을 아무리 눌러도 useEffect
훅이 호출되지 않습니다.마무리
이렇게 함수형 패러다임의 주요 개념인 일급 함수, 순수 함수, 불변성의 장점을 정리해보았습니다.
물론 모든 상황에 적합한 패러다임은 아니며, 러닝 커브가 높다는 단점도 있습니다.
일급 함수의 경우에는 함수를 주고받을 수 있다는 점과 동작의 조합의 관점에서 바라보았고 순수 함수와 불변성은 예측 가능성과 위험 관리의 측면에서 바라보았습니다.
원론적인 얘기만 하기보다는 체감할 수 있는 예제와 리액트에 적용된 부분들을 같이 곁들였는데, 독자 여러분들이 잘 느끼셨을지 모르겠네요. 독자분들은 설득이 되셨나요? 함수형 패러다임의 매력을 느끼셨나요?
개인적으로 동작을 작은 함수들을 조합해 나가는 바텀-업 방식은 함수형 프로그래밍을 공부했을 때 신선한 충격이었습니다. ‘이 동작을 이렇게 조합해서 구현한다고?’ 라는 생각을 했었습니다. 저는 아직 그런 경지에 오르지는 못했지만, 제가 상상할 수 있는 한계 밖을 본 것만으로도 충분한 가치가 있다고 생각합니다.
여러분들은 함수형 패러다임에 대해 어떻게 생각하시나요? 생각을 자유롭게 댓글로 달아주세요!