어떻게 코드를 잘 작성할 수 있을까?

Bside Korea
9 min readJun 16, 2023

--

함수형 사고로 개발해보기: 『쏙쏙 들어오는 함수형 코딩』을 읽고

안녕하세요 비사이드코리아에서 개발하는 안준범입니다

에릭 노먼드가 쓴 쏙쏙 들어오는 함수형 코딩에 대해 읽고 배우고 느낀점을 작성해보자 합니다

프롤로그

React를 사용하면서 불변성 , 순수함수, immer.js 와 같은 키워드들을 접하면서 함수형 용어에 대해 많이 찾아 보게 되었으며, Js의 map, filter, reduce, React의 Hook 등 많은 실전 활용법에 대해 확인 할 수 있었습니다.

그러나 어느정도 이론과 사용법은 알지만, 깊이 있게 제대로 사용하고 있다는 느낌은 모호했습니다. 그러던 중 팀원 한분이 진행하는 “쏙쏙 들어오는 함수형 프로그래밍” 세션을 듣고 저도 바로 읽어보았습니다.

팀원분은 “함수형으로 래픽토링 하는 법”에 집중하였지만 저는 “액션/계산/데이터”와 “카피온라이트,방어적복사” 이 두가지에 대해서 설명해보겠습니다.

1장. 함수형 프로그램 핵심 3대장 ( 액션 / 계산 / 데이터 )

책에서는 함수형 프로그래밍을 바라보는 관점을 3가지로 분류합니다.

액션이란?

  • 이메일 보내기, db에서 읽어오기 등
  • 부수효과가 있는 함수
  • 실행시점과 횟수에 의존한다.
  • 암묵적 입력 또는 출력을 가지고 있다.

ex) 메일을 하나 보내는 것과 여러개 보내는것은 차이가 있다

계산이란?

  • 부수효과가 없는 함수 (순수함수)
  • 입력값으로 출력값을 만든다 ( 결과를 예측할 수있음 )
  • 계산은 외부에 영향을 주지 않기 때문에 쉽게 테스트가 가능하다.
  • 계산을 조합하여 더 큰 계산을 만들 수 있다. ( 재사용성 )

데이터란?

  • 이벤트에 대한 결과 / 사실, 말 그대로 데이터 ( 객체 , 리스트 .. )

함수형 프로그래밍은 부수효과나 순수함수가 아닌 액션 / 계산 / 데이터로 함수(코드)를 분리하고 있고,

함수형 프로그래밍의 핵심은 액션과 계산을 확실히 분리해서 액션을 최소화하고 계산함수를 많이 만들어서 관리를 하는 것을 목표로 한다고 설명하고 있습니다.

어떻게 액션과 계산을 구분 할수 있을까?

// NOTE: 기존 코드 
const Origin = () => {
const [voteResults, setVoteResults] = useState([])

// 명시적, 암묵적 입출력이 섞여있는 함수
const onClickVoteExample = () => {
const voteExampleReflectionList = voteResults.map((value) => ({ ...value, vote: value.agenda.defaultVote }))
setVoteResults(voteExampleReflectionList)
}

return (
<ModeratorsVoteExampleBox onClickVoteExample={onClickVoteExample} />
)
}

함수는 암묵적 / 명시적 입출력이 존재하며 이를 섞어서도 사용할 수 있습니다.

위의 코드를 보면 onClickVoteExample() 함수에 명시적, 암묵적 입출력이 섞여있는 함수가 섞여있습니다

이는 액션에 해당하며 실행횟수와 시점에따라 결과가 다르기 때문에 테스트 및 재사용이 어렵습니다.

액션코드에서 계산코드 찾아서 빼기

// NOTE: 함수형 사고 관점으로 리팩토링
const New = () => {
// 데이터
const [voteResults, setVoteResults] = useState([])

// 계산
// setState와 같은 암시적 출력을 사용하지 않음
const exampleVotingList = (voteResults) => {
const voteList = voteResults.map((value) => ({ ...value, vote: value.agenda.defaultVote }))
return voteList
}

// 액션
const onClickVoteExample = () => {
setVoteResults(exampleVotingList(voteResults))
}

return (
<ModeratorsVoteExampleBox onClickVoteExample={onClickVoteExample} />
)
}

onClickVoteExample() 액션함수에서 exampleVotingList()함수를 빼내고 명시적 입출력을 사용했습니다.

코드는 길어졌지만 액션 / 계산 역할이 명확해 졌으며

이렇게 만들어진 exampleVotingList() 함수는 독립적이며 테스트 및 재사용하기에 용이해졌습니다.

즉 , 부수효과가 없는 계산함수가 되었습니다!

정리

  • 함수형 프로그래밍은 함수를 액션만으로 정의하기 보다는 액션 < 계산 < 데이터 순으로 가장 많이 가능한 많이 쪼개려고 한다.
  • 계산/ 데이터로 갈수록 외부에 영향을 받지도 주지도 않으며 테스트 및 재사용이 쉬워진다

2장. 카피온라이트 , 방어적복사

JavaScript는 기본적으로 pass by reference 방식을 사용하기에 언제든 원본값이 수정되거나 함수내부에서 값 수정시 외부에 영향을 미칠 수 있습니다.

이를 위해 pass by value 의 형태로 변경하는 방법을 설명하고자 합니다.

카피 온 라이트( Copy on Write )

const cart = []

const addCartItem = (item) => {
cart.push(item)
}


// copy-on-write 적용하여 계산으로 바꾸기
const cart = []

const addCartItem = (cart,item) => {
const newCart = cart.slice() // 1. 복사본 생성
newCart.push(item) // 2. 복사본 변경
return newCart // 3. 복사본 리턴
}

위의 addCartItem()함수는 pass by reference 방식을 통해 원본을 직접전달 , 직접 수정을 함으로써 순수함수( 계산함수 )의 함수의 동작이 외부에 영향을 끼치지 말아야 한다에 어긋납니다.

아래는 addCartItem() 함수는 slice함수로 new_cart 라는 복사본을 만들고 그 복사본을 내부에서 수정후 리턴하고 있습니다.

이와 같이 작업시 외부에 영향을 끼치지 않고 똑같은 결과를 만들 수 있는데 이를 카피 온 라이트 (Copy on Write) 혹은 얕은 복사라고 합니다.

객체일 경우 Object.assign() 을 사용하며, 배열이든 객체든 ES6의 Spread Operator(스프레드 연산자)를 사용해서 얕은복사를 한다면 더 간결한 코드를 짤 수 있습니다.

  • copy-on-write : 데이터를 변경할 때 복사본( 얕은복사 )을 만들어 수정하고 반환하는 것. ( 원본 수정 x )

방어적 복사

// 통제 바깥의 코드. 무슨 일이 일어나는지 알 수 없음. cart를 변경할 수도 있음
const blackFridayPromotion(cart) = () => {
}

const addCartItem = (cart, value) => {
const newCart = structuredClone(cart); // 깊은 복사
blackFridayPromotion(newCart, value) // cart값을 변경해도 원본은 변하지 않는다.
return newCart
}

카피 온 라이트를 사용해서 액션을 계산으로 변경 할 수 있었습니다.

그런데 해당 액션이 우리가 수정 / 제어 할 수 없는 라이브러리라면 어떻게 해야 할까요?

그런 경우 중첩된 모든 구조를 복사하는 깊은 복사 방식을 사용합니다.

책에서 JavaScript는 DeepCopy 구현이 어렵다고 Lodash의 cloneDeep() 함수사용을 권장하지만 ,

structuredCloneAPI가 추가 되었기 때문에 이 API를 사용하면 됩니다!

https://developer.mozilla.org/en-US/docs/Web/API/structuredClone

정리

  1. copy-on-write : 데이터를 변경할 때 복사본을 만들어 수정하고 반환하는 것. ( 원본 수정 x )
  • JavaScript의 경우 Closure나 Haskell 같은 언어와달리 .slice() 함수나 immer.js 같은 라이브러리를 통해서 복사본을 만들어야 합니다. or Spread 연산자

2. 방어적 복사 : 안전하지 않은 코드(서드파티 or 레거시 코드)를 사용하기 전 / 후에 데이터를 복사해서 사용하는 것.

3. 방어적 복사는 깊은 복사이기 때문에 비용이 많이든다. 가급적 얕은복사를 사용하자

마치며, 책 후기

책 부제가 심플안 코드로 복잡한 소프트웨어 길들이기라고 되어있는데 참 맞는 말이라고 생각이듭니다.

책에서는 처음보았을때 난해한 함수형 용어들(불변성 , 순수함수, 1급함수 등..)을 액션, 계산, 데이터 등으로 좀더 쉽게 재정의하서 설명하고 있으며 또한 대중적인 언어인 JavaScript를 통해 코드예시가 적혀있어서 비교적 쉽게 읽을 수 있었습니다.

본 글에는 책의 단편적인 내용만 가져왔지만 이외에도 계층형구조 , 타임라인 , 반응형 아키텍쳐 등 더 많은 내용들이 있습니다.

나중에 책을 볼수 있는 기회가 된다면 보시는걸 추천 드립니다!

당장 이 책의 내용들을 모두 적용 하긴 어렵겠지만, 느낀점은 다음과 같습니다.

  1. 다른 시각으로 코드를 바라 볼 수 있게 되었다.
  2. 함수형 프로그래밍에 대해 두루뭉실하게 알고있거나 그냥 쓰고있는 것들이 정리가 되었다.
  3. 설계 할때나 코드를 작성 할때 막연히 변수, 함수 로만 생각했던 것들을 액션 / 계산 / 데이터 라는 기준이 생겼으며 이와 함께 암묵적인 입출력들을 명시적 바꾸면서 조금씩 리펙토링을 한다면 장기적으로 더 좋은 코드를 짤수 있지 않을까라는 생각이 든다 !

--

--

Bside Korea

비사이드는 주주자본주의에 기반하여 국내 자본시장을 개선하기 위해 태어난 최초의 주주행동주의 플랫폼입니다