최근 우아한 스터디 - "쏙쏙 들어오는 함수형 코딩"으로 스터디원과 함께 함수형 프로그래밍에 대해 스터디 중입니다. 스터디 이전에도 살짝 봤었던 책이었는데 이번 스터디로 완독할 수 있을 것 같아 기대를 하고 있습니다.
책의 구성은 총 2부로 구성되어있습니다. 그 중 1부는 액션, 계산, 데이터 구분하기 그리고 데이터의 불변성을 다루고 있습니다.
책의 내용은 함수형 패러다임에 대한 것 뿐만 아니라 좋은 코드란 무엇인가? 라는 주제에 대해서도 고민하고 깨달음을 얻을 수 있었습니다. 아직 공부를 시작한지 얼마 안되었지만 이때까지 공부한 내용을 어느 정도 주관적인 내용을 덧붙여 책의 내용을 정리합니다.
책을 접하기 전, 제가 오해하고 있던 함수형 프로그래밍의 특징은 다음과 같습니다.
하지만 이러한 함수형 프로그래밍에 대한 접근은 실용적이지 못합니다. 기존의 함수형 프로그래밍에서 벗어나 실용적인 함수형 프로그래밍을 다시 정의해야 할 필요가 있어 보입니다.
함수형 프로그래밍의 정의는 다음과 같습니다.
과연 정의대로 부수효과 없이 프로그래밍을 하는것이 좋을까요?
이메일 보내기, 서버에 네트워크 요청하기, 회원가입으로 데이터베이스를 읽고 쓰기등등 우리가 서비스에 필요한 동작들은 부수효과입니다. 그런데 이러한 부수효과없이 프로그래밍을 하는 것은 의미없는 짓에 불과합니다.
그래서 실용적인 함수형 프로그래밍은 기존의 함수형 프로그래밍과 달리 부수효과와 함께 동작합니다. 따라서 우리는 부수효과를 잘 다룰줄 알야아합니다. 부수효과를 없어져야할 존재로 바라보는 것이 아니라 여러 도구와 방법으로 다룰 수 있는 존재로 보는 것 입니다.
부수효과를 잘 다루기 위해서는 우리는 코드를 바라보는 시각을 바꾸어야합니다.
실용적인 함수형 프로그래밍에서는 코드를 액션, 계산, 데이터 세가지로 구분합니다.
데이터는 이벤트에 대한 사실입니다. 어떠한 값 자체
라고도 볼 수 있습니다.
async function fetchUser(){
...
}
const user = await fetchUser()
const todo = {
id: 1,
text: "hello",
isDone: boolean
}
user는 데이터입니다. 웹 요청이라는 이벤트에 대한 사실이기 때문에 데이터라고 할 수 있습니다. 객체로 표현된 todo 역시 데이터입니다. 특정 todo에 대한 정보와 사실을 담고 있습니다.
액션은 부수효과를 일으킵니다. 실행횟수와 실행시점에 영향을 받고 외부환경에 의존적입니다.
function getCurrentTime(){
return new Date()
}
let count = 1
function increase(){
count += 1
}
async function fetchUser(){
//...
}
getCurrentTime 함수는 실행시점에 영향을 받습니다. 언제 호출했는지에 따라 매번 다른값을 반환합니다.
increase 함수는 전역변수 count를 참조하고 있으며 실행횟수에 따라 다른 결과를 만듭니다.
fetchUser 함수는 API 네트워크 통신으로 서버의 값을 받아옵니다. 외부환경인 서버에 따라 다른 값을 응답할 수도 있습니다.
액션은 제어하기 힘듭니다. 제어하기 힘든 함수는 테스트하기 힘들고 호출 결과를 예상하기 어렵습니다. 따라서 불필요한 액션을 줄이는 것이 중요합니다.
계산은 순수함수입니다. 수학함수라고도 하며 수학에서의 함수처럼 입력값이 동일하면 항상 출력값이 동일해야합니다. 함수 내부에서는 외부 변수를 읽거나 쓰지 않아야하고 암묵적 출력이 없어야합니다.
function sum(a,b){
return a+b
}
function addTodo(todoList, todo){
return [...todoList,todo]
}
계산은 실행시점과 실행횟수에 상관없이 늘 동일한 입력에 대해 동일한 출력을 반환합니다. 따라서 계산은 테스트하기 쉽습니다.
또한 계산은 동시에 안전한 실행이 가능합니다. 액션의 경우 동시에 실행된다면 여러 외부 변수에 따라 결과를 예측하기 매우 어렵습니다. 실행시점와 실행횟수에도 영향을 받지 않기 때문에 액션보다 신경써야할 것들이 적습니다.
부수효과를 잘 다루기 위해 첫 번째 우리가 해야할 일은 부수효과에서 계산을 빼내는 것입니다.
id가 짝수인 todo를 서버로 post 요청하는 함수가 있습니다.
async function sendTodo(){
for(const todo of todoList){
if(todo.id % 2 === 0){
await postTodo(todo)
}
}
}
서버로 post 요청하는 일은 부수효과이고 반드시 필요합니다. 하지만 불필요한 계산이 액션에 포함되어 있습니다.
id가 짝수인 todo를 찾는 로직을 계산으로 빼내보겠습니다.
function getEvenIdTodo(todoList){
return todoList.filter(todo=> todo.id % 2 === 0)
}
이제 액션인 sendTodo 함수에서 계산을 빼내왔습니다. 이렇게하면 짝수 id에 대한 책임은 계산은 getEvenTodo 함수에 있기 때문에 테스트하기 쉬워집니다.
다음으로 sendTodo 함수에서 암묵적인 입력을 제거하고 인자로 todoList를 입력 받도록 리팩토링해보겠습니다.
async function sendTodo(todoList){
for(const todo of todoList){
await postTodo(todo)
}
}
}
여전히 부수효과가 존재하는 액션이긴 하지만 암묵적 입력을 제거하여 결합도를 낮추고 재활용성이 높아졌습니다.
만약 id가 홀수인 todo를 post 요청한다고 하면 이 함수를 그대로 재활용하면 됩니다.
async function sendTodo(todoList){
for(const todo of todoList){
await postTodo(todo)
}
}
}
sendTodo(evenIdTodoList)
// 명시적 입력은 재활용이 쉽습니다.
sendTodo(oddIdTodoList)
이처럼 액션이라도 암묵적 입력, 암묵적 출력이 적은 것이 좋습니다.