23년 1월 원티드 프리온보딩 백엔드 1일차 TIL (함수형 프로그래밍, 순수함수) (Wanted Pre Onboarding BackEnd)

thumbnail

백엔드 수업 1일차 학습

나는 이번에 프론트랑 같이 수강을 했는데 정말 어지러웠다.
난이도가 높은게 아니라 옆에는 프론트 띄우고, 옆에는 백엔드 띄우니 두 강의 내용을 쫒는거 자체가 불가능할 정도였다.

그래도 다행히 프론트 쪽은 아이스 브레이킹을 하느라 주요한 내용은 안나왔고, 백엔드 쪽을 약간 집중해서 들었다.
여기 TIL은 내가 들으면서 배우거나 학습한 내용을 정리하는 위주로 올릴 예정이다.

개인 학습 내용을 정리한 것이라 약간 부실할 수도 있다.

강의 자료는 강사님의 것이라 공유는 불가능하고, 혹시 저작권 침해나 외부 노출 불가 자료가 있다면 알려주시면 바로 수정 반영 처리하겠습니다.
오픈된 깃허브의 경우 웹에서 누구나 접근이 가능하기에 깃허브는 공개하였습니다.


2일차 강의는 이곳 포스팅 참조


금일 진행 내용

  • 함수형 프로그래밍 컨셉에 대하여
  • 객체지향, 함수형 프로그래밍 패러다임
  • 실용주의 프로그래밍
  • Live Coding1 : 고차함수(map, filter, reduce) 구현

함수형 프로그래밍 설명 및 간단 이론

  • 함수에 대해 설명하고, 함수형 프로그래밍에 대해 간략한 설명

함수에 대해

함수는 하나의 기능을 가지고 언제나 동일한 결과를 내야 한다.
함수 내에서 액션으로 인해 사이드 이팩트가 나는 것을 줄여야 한다. (종점은 Side Effect 제거)


순수 함수

부수효과 없이 결과값이 인자에만 의존하는 함수를 순수함수라 한다. 부수효과는 함수에서 결괏값을 주는 것 외에 하는 행동. (Side Effect)


함수형 프로그래밍 예 (맛보기)

//Bad case
const init: number = 0

const add = (number: number) => {
  return number + init
}

add(5)

위 코드는 외부의 init이라는 값에 의해 함수 밖에 있는 값에 의해 함수 값이 결정되기에 잘못된 예라 볼 수 있다.
그럼 위와 같은 코드를 순수함수 형태로 바꾸려면 아래와 같이 바꿀 수 있다.

//Good case
const init: number = 0

const add = (number: number, init: number = 0) => {
  return number + init
}

add(5)

외부에 있던 init이라는 변수를 전달인자로 바꾸고, 해당 값을 0으로 기본 값을 주는 방법으로 바꾸면 순수함수 형태로 바꿀 수 있다.

또 다른 예제를 보자

//Bad Case
const fruits: string[] = ['Apple', 'Orange', 'Blueberry']

const head = (arr: string[]) => {
  return arr.shift()
}

console.log(head(fruits))

여기서 shift 함수는 전달된 배열의 첫 요소를 반환하고, 나머지는 제거한다.
이렇게 하면 전달된 배열의 경우 조작이 일어나게 된다.

함수는 액션(무언가 데이터를 조작,삭제 등을 하는 행위)이 일어나는 것을 줄이고 없애야 한다.
그럼 이것을 개선 한다면?

//Good Case
const fruits: string[] = ['Apple', 'Orange', 'Blueberry']

const head = (arr: string[]) => {
  return arr.length < 1 ? undefined : arr[0]
}

console.log(head(fruits))

위와 같이 한다면 전달된 배열을 조작하지 않고 순수하게 값만 넘기는 형태로 구현이 된다.

또 예제가 있다.

//Bad Case
const fruits: string[] = ['Apple', 'Orange', 'Blueberry']
fruits[2] = 'Tomato'

이렇게 대입을 할 경우 원본 데이터에 대한 변경이 일어나게 된다.
함수형 페러다임에서는 원본을 복사해서 사용해야 한다.

그래서 아래와 같이 고차함수인 map을 이용해 새로운 배열을 만들어 낸다.

//Good Case
const fruits: string[] = ['Apple', 'Orange', 'Blueberry']

const newFruits = fruits.map((fruit: string) =>
  fruit === 'Blueberry' ? 'Tomato' : fruit
)

console.log('newFruites = ', newFruits)

객체지향과 함수형 프로그래밍

둘의 개념 중 상반되는 개념은 없다.

객체지향(OOP)

  • 일반적인 방법으로 모듈화
  • 다형성
  • 현실 세계를 모델링

함수형 (FP)

  • 일반적인 방법으로 모듈화
  • 인풋과 아웃풋에만 의존
  • 수학의 관점으로 사고하기

함수형 프로그래밍 패러다임 (키워드)

  • No Side Effects

    • Pure Function
    • No Mutation
  • Higher Order Function

    • Function is value


잠시 휴식 후 Live Coding


Live Coding (고차함수 구현)

수업 예제 Github

먼저 간단한 예제부터 시작한다.


일반적인 예제

const arr: number[] = [1, 2, 3, 4, 5]

// 1. 홀수만 걸러주세요
// 2. 걸러진 원소에 곱하기 2를 해주세요
// 3. 모두 다 더해주세요

let sum = 0
for (const el of arr) {
  if (el % 2 === 1) {
    const newElement = el * 2
    sum += newElement
  }
}

위 코드는 일반적인 방법으로 구현한 것이고, 아래는 고차함수를 이용해 구현한 것이다.

const sum2 = arr
  .filter(el => el % 2 === 1)
  .map(el => el * 2)
  .reduce((prev, curr) => prev + curr)

고차함수 Map 구현

이제 고차함수 중 map을 직접 구현한 코드

/**
 * map: 배열을 순회하면서 func 을 적용해서 새로운 결과 값을 담은 배열을 리턴한다.
 * func: (el) => value
 */
const map = (func, iter) => {
  const result = []

  for (const el of iter) {
    result.push(func(el))
  }

  return result
}
console.log(map(el => el * 2, arr))

고차함수 Filter 구현

/**
 *  filter: 배열을 순회하면서 func 의 truthy 값(조건에 맞는 값)만 배열에 담아 리턴한다.
 *  func: (el) => truthy | falsy
 */
const filter = (func, iter) => {
  const result = []

  for (const el of iter) {
    if (func(el)) {
      result.push(el)
    }
  }

  return result
}

console.log(filter(el => el % 2 === 1, arr))

고차함수 Reduce 구현

/**
 * reduce: 배열을 순회면서 func 을 반복 적용해서 새로운 결과 값을 얻어낸다. (쪼개는 함수)
 * func: (acc, el) => acc
 * func: (prev, curr) => acc
 */

const reduce = (func, acc, iter) => {
  if (iter === undefined) {
    iter = acc[Symbol.iterator]()
    acc = iter.next().value
  }

  for (const el of iter) {
    acc = func(acc, el)
  }

  return acc
}

console.log(reduce((prev, curr) => prev + curr, 0, arr))
console.log(reduce((prev, curr) => prev + curr, arr))

Live Coding (함수의 연결)

/**
 * 함수의 합성, pipe
 * 순회 가능한 객체를 받아서 함수의 파이프라인을 타고 최종 결과값을 리턴한다.
 */
const pipe = (iter, ...functions) =>
  reduce((prev, func) => func(prev), iter, functions)

const arr = [1, 2, 3, 4, 5]

const sum2 = arr
  .filter(el => el % 2 === 1)
  .map(el => el * 2)
  .reduce((prev, curr) => prev + curr)

console.log(sum2)

pipe(
  arr,
  arr => filter(el => el % 2 === 1, arr),
  arr => map(el => el * 2, arr),
  arr => reduce((prev, curr) => prev + curr, arr),
  result => console.log(result)
)

우리가 구현된 고차함수를 쓸 때 .으로 체이닝 하듯이 쓰는 것을 구현


Live Coding (커링과 지연함수)

난 커링을 잘 몰라서 이 부분은 Javascript.Info 에서 많이 참고했다.
커링은 전달된 함수를 가지고 있다가, 다른 함수가 오면 같이 함수를 처리 반환하는 함수를 의미한다.(틀릴 수 있음 => 향후 포스팅 예정)

const curry = func => (a, ...args) =>
  args.length > 0 ? func(a, ...args) : (...args) => func(a, ...args)

커링의 구현체는 위와 같다.

이를 통해 각 고차함수를 커링으로 묶으면 아래와 같다.
단순하게 함수를 커링 전달인자로 넘긴다.

const map = curry((func, iter) => {
  const result = []
  for (const el of iter) {
    result.push(func(el))
  }

  return result
})

const filter = curry((func, iter) => {
  const result = []
  for (const el of iter) {
    if (func(el)) {
      result.push(el)
    }
  }

  return result
})

const reduce = curry((func, acc, iter) => {
  if (iter === undefined) {
    iter = acc[Symbol.iterator]()
    acc = iter.next().value
  }

  for (const el of iter) {
    acc = func(acc, el)
  }

  return acc
})

커링에 대해 잠깐 집고 넘어가면… 아래는 커리를 쓰지 않은 버전

const arr = [1, 2, 3, 4, 5]
const noCurAdd = (a, b) => a + b

console.log(noCurAdd(1, 3))
console.log(noCurAdd(1)) //포인트 1
console.log(noCurAdd(1)(3)) //에러 포인트

const curAdd = curry((a, b) => a + b)
console.log(curAdd(1, 3))
console.log(curAdd(1)) //포인트 2
console.log(curAdd(1)(3))

위 결과를 실행하면 에러와 함께 아래의 것을 맞이할 것이다.

4
NaN
/Users/mhlab/Develop/study/Wanted/PreOnboard_Back/lecture/wanted-pre-onboarding-challenge-BE-task-JAN.2023/lecture-1/3.js:53
console.log(noCurAdd(1)(3)); //에러 포인트
                       ^

TypeError: noCurAdd(...) is not a function
    at Object.<anonymous> (/Users/mhlab/Develop/study/Wanted/PreOnboard_Back/lecture/wanted-pre-onboarding-challenge-BE-task-JAN.2023/lecture-1/3.js:53:24)
    at Module._compile (node:internal/modules/cjs/loader:1155:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1209:10)
    at Module.load (node:internal/modules/cjs/loader:1033:32)
    at Function.Module._load (node:internal/modules/cjs/loader:868:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:22:47

보면 알겠지만 커리 미사용의 두 번째 포인트1 영역의 경우 NaN이 뜰 것이다.
당연한 것이 함수가 다음 전달인자가 없이 넘어 왔기에 숫자가 아님을 표시하는 NaN이 뜨는 것이고, 에러가 난다.

아마 저 주석의 에러 포인트에서 에러가 날 것이다. 이건 당연한게 전달인자를 받는 부분에서 noCurAdd함수의 전달인자를 벗어나 또 다른 값이 왔다.

그러면 왜 커링을 적용한 것은 되는가?
커링의 구현체를 보면 알 수 있다.

커링의 구현체에서 보면 아래와 같이 되어 있다.

func => (a, …args) =>

즉 전달인자가 스프레드 연산자(Spread Operator)를 통해 여려 인자를 받기에 뒤에 있는 값도 같은 전달인자라 판단한 것이다.

하지만 포인트2의 경우 인자가 하나기에 아래와 같이 함수로 표현되어 나타나진다.

4
[Function (anonymous)]
4

그 외 첫번째는 바로 알 수 있고, 마지막은 그냥 전달인자라 보면 된다.

console.log(curAdd(1)(3)(5))

위 코드는 당연히 에러가 난다.
왜냐하면 커리 안에 있는 함수의 인자를 벗어났기 때문이다.

그 외에는 이것을 응용한 것을 소개하는 시간이었다.
이번 포스팅에서는 해당 내용을 따로 추가하진 않겠지만…
시간이 되면 업데이트 포스팅을 할 예정이다.


1일차 후기

음…개인적으로 현업을 내려놓은지 약 2년? 3년 정도 된거 같은데 다시 한번 워밍업? 하는 시간이었다.
내가 최근에 만든, 그리고 만들고 있는 서비스 코드도 진짜 엉망인데…

이번에 함수형을 배워서 좀 개선해 나가보고 싶다.
코드스테이츠 블록체인 + 온보딩 백엔드 + 온보딩 프론트엔드 = 고통의 삼위일체

좀 빡센데…해보자.
힘들어야 성장하더라(?)


2일차 강의는 이곳 포스팅 참조


참고 자료


Written by@MHLab
로또는 흑우집합소 🎲
와인관리, 시음노트, 셀러관리는 마와셀 🥂

🫥 My Service|  📜 Contact|  💻 GitHub