함수형 프로그래밍(FP)에서 중요한 요소 3가지
<button id="button">0</button>
<script>
document.getElementById("button").onClick = function() {
button.innerText++;
}
</script>
어디가 액션이고, 계산이고, 데이터일까?
언제 하느냐
에 따라서 or 여러 번 클릭할수록
다른 결과가 만들어지기 때문에 액션위 프로그램에서 각자의 관계는 액션이 발생하면 미리 정의된 계산에 의해 데이터가 바뀌게 된다.
function App() {
// data
const [count, setCount] = useState(0);
// calculations
const increase = (value) => value + 1;
// actions
const onClick = () => setCount((prev) => increase(prev))
// 선언적 패턴
return <button onClick={onClick}>{count}</button>
}
사실 함수형 프로그래밍은 현대 UI 프로그래밍에 잘 맞으며 알게 모르게 우리가 쓰는 중이다!
여기서 주목할 코드는 바로 increase
를 별도의 함수로 만들었다는 점이다.
const increase = () => {
// ...
setState(count + 5);
}
const onClick = () => {
// ...
increase();
}
“단순한 함수 쪼개기는 액션 - 계산 - 데이터의 분리가 아니다”
계산은 반드시 입출력으로 이루어져야 하며, 같은 입력에 대해서는 항상 같은 출력값을 내놓아야 한다.
→ 여러 번 실행되어도 외부 세계에 영향을 주지 않아야 함
함수형 프로그래밍의 핵심은 액션과 계산을 확실히 분리해서 액션을 최소화하고 계산함수를 많이 만들어서 관리하는 것을 목표로 한다.
const increase = () => {
...
// count는 함수 외부에서 왔으므로 암묵적 입력
const result = count + 1;
setState(result);
...
// result는 명시적 출력
return result
}
const action = () => {
...
increase();
}
하나의 함수에 암묵적 입출력과 명시적 입출력이 섞여 있다면 계산이 아니라 액션이다!
const increase = (count, offset) => {
const result = count + offset; // count와 offset을 명시적 입력으로 변경
// setState와 같은 명시적 출력은 사용하지 않는다.
return result;
}
// 암묵적인 입출력은 별도의 action에 모아준다.
const action = () => {
setState((prev) => increase(prev, 1)); // 외부에서 필요한 모든 입력을 넣어준다.
}
이렇게 만들어진 계산은 이제 언제든지 재사용이 가능하며 테스트하기에도 용이하다!
“계산이 바로 순수함수였다!!”
❗️계산은 여러 번 실행해도 외부 영향에 값이 변경되지 않아야 한다❗️
하지만 함수가 숫자나 문자열이 아닌 객체나 배열을 사용한다면, JavaScript는 기본적으로 pass by reference
방식을 사용한다.
→ 언제든 외부에서 값이 수정되거나 함수 내부에서 외부에 영향을 미칠 수 있다.
이를 방지하기 위해서는 객체나 배열을 pass by value
형태로 변경하는 방식을 알아야 한다.
pass by [ ]
[버튼을 누를 때마다 배열 값을 하나씩 늘리는 카운터]
// 1씩 증가하는 계산 함수
const increase = (value) => value + 1;
// 1씩 증가하는 값을 배열에 넣는 함수
const increaseArray = (arr) => {
const value = arr[arr.length - 1];
arr.push(value + 1);
return arr;
}
이렇게 만들어진 코드는 계산이 아니라 액션이 된다.
JavaScript에서 Object나 Array와 같은 덩치가 큰 값을 다룰 때 pass by reference
라는 방식을 통해 원본을 그대로 전달하고 원본을 직접 수정할 수 있는 방식을 통해 효율적으로 값을 조작한다.
하지만 FP 세계에서 계산은 함수의 동작이 외부 세계에 영향을 끼치지 않아야 하고, 실행회수와 시점과는 무관해야 한다.
→ 함수에서 Array나 Object의 원본 값을 직접 수정하면 메모리상으로는 효율적이겠지만, 외부 세계에 영향을 끼치지 말아야 한다는 제약 조건을 깨게 된다.
const increase1 = (arr) => {
arr = arr.slice(); // array를 조작하기 전에 복사해서 사용한다.
const value = arr[arr.length - 1];
arr.push(value + 1);
return arr;
}
// spread 표기법을 사용해 더 간결하게 작성
const increase2 = (arr) => [...arr, arr[arr.length - 1]];
이렇게 원본의 값을 복사해서 수정하면 외부 세계에 영향을 끼치지 않는 계산이 된다.
이러한 방식이 ‘카피 온 라이트(Copy on Write)’ 혹은 얕은 복사이다.
→ 카피 온 라이트 방식을 통해서 액션을 계산으로 만들 수 있다.
const setObjectName = (obj, value) => {
return {
...obj,
name: value
}
}
const setObjectName2 = (obj, name) => ({...obj, name});
카피 온 라이트 방식으로 액션을 계산으로 변경할 수 있다.
[하지만 만약 해당 액션이 우리가 수정할 수 없는 라이브러리라면?]
// 액션을 써야하지만 라이브러리 함수라서 내가 수정할 수가 없다.
import someActionLibrary from "lib";
const someCalcuation = (obj, value) => {
someActionLibrary(obj, value); // obj의 값을 변경해서
return obj; // 출력한다면 이 함수는 계산일까?
}
const someCalcuation = (obj, value) => {
const clone = structuredClone(obj); // 완전한 clone을 만들어 낸다.
someActionLibrary(clone, value); // clone 값을 변경해도 원본은 변하지 않는다.
return clone;
}
이렇게 중첩된 모든 구조를 복사하는 방식을 깊은 복사라고 부른다.
JS에서는 원래 이러한 기능이 없었지만, 최근 structuredClone() API가 Native 기능을 지원하니 이를 사용하면 된다.
“함수형 프로그램은 함수를 통해서 관심사를 분리할 수 있다”
코드를 작게 분리하면 좋은 점
이렇게 분리한 코드를 조합하는 과정에서 자연스레 함수 간의 계층이 생겨난다.
계층이 견고해지는 구조로 작성하게 되면 유연하면서도 변화에 국지적인 형태의 좋은 설계를 가져가게 된다.
무엇을 해야 할지
같은 기획서에 가까운 선언적 패턴으로 코드를 가져갈 수 있다.선언적 패턴: 계층형 설계와 추상화 벽을 이용하여 ‘무엇’과 ‘어떻게’를 구분하여 좋은 설계를 유지하자.
프로그램은 곧 데이터의 변화이다.
그리고 데이터는 액션에 의해 변한다.
데이터가 변하는 방법을 독립적으로 계산했을 때, 액션과 계산과 데이터를 함수로 연결하여 작성하는 개념이 바로 ‘함수형 프로그래밍 패러다임’이다.
특히 실행 시점과 횟수에 의존적인 액션에서 독립적인 계산을 분리하여, 복잡한 코드를 간단하게 만들고 테스트를 쉽게 만드는 것이 중요하다.
카피 온 라이트
방어적 복사
선언적 패턴: 계층형 설계와 추상화 벽을 이용하여 ‘무엇’과 ‘어떻게’를 구분하여 좋은 설계를 유지하자.