‘액션-계산-데이터’ 관점으로 보는 함수형 프로그래밍 패러다임

Ocean·2023년 11월 5일
0

함수형 프로그래밍(FP)에서 중요한 요소 3가지

  1. 순수함수 : 코드를 액션과 계산, 데이터로 분리하자. 특히 액션에서 계산을 분리하는 코드를 작성하자.
  2. 불변성 : 카피 온 라이트와 방어적 복사를 이용하여 불변성을 유지하자.
  3. 선언적 패턴 : 계층형 설계와 추상화 벽을 이용해, ‘무엇’과 ‘어떻게’를 구분하여 좋은 설계를 유지하자.

1부. 액션, 계산, 데이터

Untitled

  • 액션은 호출 시점과 실행 횟수에 의존한다.
  • 계산은 입력과 출력으로 이루어져 있다.
  • 데이터는 이벤트에 대한 사실이다.

[예시] 카운터 프로그램

<button id="button">0</button>

<script>
	document.getElementById("button").onClick = function() {
		button.innerText++;
	}
</script>

어디가 액션이고, 계산이고, 데이터일까?

  • 데이터는 이벤트에 대한 사실이다. → 이 프로그램에서 숫자가 바로 데이터
  • 액션은 호출 시점과 실행 횟수에 의존한다. → 사용자가 버튼을 클릭했을 때 숫자 1이 커지는 것은 언제 하느냐에 따라서 or 여러 번 클릭할수록 다른 결과가 만들어지기 때문에 액션
  • 계산은 입력과 출력으로 이루어져 있다. → 클릭하면 기존에 있는 숫자에 1을 더해 새로운 숫자를 만들어내는 계산을 하고 있음

위 프로그램에서 각자의 관계는 액션이 발생하면 미리 정의된 계산에 의해 데이터가 바뀌게 된다.

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)); // 외부에서 필요한 모든 입력을 넣어준다.
}

이렇게 만들어진 계산은 이제 언제든지 재사용이 가능하며 테스트하기에도 용이하다!

  • 명시적인 입력과 출력만을 가지며 어떠한 부수 효과도 만들어내지 않는다.
  • 같은 입력에 대해서는 언제나 같은 결과만을 만들어내야 한다.

“계산이 바로 순수함수였다!!”

2부. 불변성 - 카피 온 라이트, 방어적 복사

❗️계산은 여러 번 실행해도 외부 영향에 값이 변경되지 않아야 한다❗️

하지만 함수가 숫자나 문자열이 아닌 객체나 배열을 사용한다면, JavaScript는 기본적으로 pass by reference 방식을 사용한다.

→ 언제든 외부에서 값이 수정되거나 함수 내부에서 외부에 영향을 미칠 수 있다.

이를 방지하기 위해서는 객체나 배열을 pass by value 형태로 변경하는 방식을 알아야 한다.

pass by [ ]

pass by [ ]

카피 온 라이트: Copy on Write

[버튼을 누를 때마다 배열 값을 하나씩 늘리는 카운터]

// 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 기능을 지원하니 이를 사용하면 된다.

Untitled

3부. 선언적 패턴과 계층형 구조

“함수형 프로그램은 함수를 통해서 관심사를 분리할 수 있다”

설게는 엉킨 코드를 푸는 것이다

Untitled

코드를 작게 분리하면 좋은 점

  • 재사용하기 쉽다
  • 유지보수하기 쉽다
  • 테스트하기 쉽다

이렇게 분리한 코드를 조합하는 과정에서 자연스레 함수 간의 계층이 생겨난다.

계층적 구조

Untitled

  • 액션으로 갈수록 코드의 형태는 ‘무엇을 하는 것인지’ 행동을 기반한 기획서에 가까운 코드가 만들어지며, 데이터 구조를 몰라도 되는 형태의 코드를 작성하게 된다.
  • 반면, 계산과 데이터에 가까워질수록 데이터 중심적인 코드를 작성하게 되고 상대적으로 재사용성이 높고 테스트하기 좋은 코드 형태를 갖추게 된다.

계층이 견고해지는 구조로 작성하게 되면 유연하면서도 변화에 국지적인 형태의 좋은 설계를 가져가게 된다.

  • 상위 : 무엇을 해야 할지 같은 기획서에 가까운 선언적 패턴으로 코드를 가져갈 수 있다.
  • 하위 : 테스트가 쉬운 코드 조각들로 구성이 되는 좋은 설계 방향의 코드가 만들어진다.

선언적 패턴: 계층형 설계와 추상화 벽을 이용하여 ‘무엇’과 ‘어떻게’를 구분하여 좋은 설계를 유지하자.


액션-계산-데이터 정리

프로그램은 곧 데이터의 변화이다.

그리고 데이터는 액션에 의해 변한다.

데이터가 변하는 방법을 독립적으로 계산했을 때, 액션과 계산과 데이터를 함수로 연결하여 작성하는 개념이 바로 ‘함수형 프로그래밍 패러다임’이다.

특히 실행 시점과 횟수에 의존적인 액션에서 독립적인 계산을 분리하여, 복잡한 코드를 간단하게 만들고 테스트를 쉽게 만드는 것이 중요하다.

불변성 정리

카피 온 라이트

  • 원본의 값을 복사해서 수정하자

방어적 복사

  • 깊은 복사를 이용해 중첩된 모든 구조를 복사하자

선언적 패턴과 계층형 구조 정리

선언적 패턴: 계층형 설계와 추상화 벽을 이용하여 ‘무엇’과 ‘어떻게’를 구분하여 좋은 설계를 유지하자.

  • 상위 : 기획서에 가까운 선언적 패턴
  • 하위 : 테스트가 쉬운 코드 조각
profile
chick! chick!

0개의 댓글