프로그래밍/JavaScript

[정리] 러닝 리액트 3장 함수형 프로그래밍

seungdols 2018. 5. 10. 22:02

러닝 리액트

3장 자바스크립트를 활용한 함수형 프로그래밍

함수형 프로그래밍 개념이 시작 된 것은 1930년대이다. 그 당시 발견한 람다 계산법이 함수형 프로그래밍의 시작이라 할 수 있다. 함수를 함수로 넘기거나 함수가 함수를 결과로 내놓는 것도 가능하다. 다른 함수를 조작하고, 함수를 인자로 받거나 반환하는 것이 가능한 복잡한 함수를 고차함수 (high-order function)으로 부른다.

함수가 1급시민(First class citizen)이 되려면 변수에 함수를 대입할 수 있어야 하고, 함수를 다른 함수에 인자로 넘길 수 있으며, 함수에서 함수를 만들어서 반환할 수 있어야 한다.

3.1 함수형이란?

자바스크립트는 함수가 1급시민에 해당되기에 함수형 프로그래밍을 지원한다고 할 수 있다.

var log = function(message) {
console.log(message)
}

log('자바스크립트에서는 함수를 변수에 넣을 수 있습니다.')

const log = message => console.log(message);

const obj = {
message: '함수를 다른 값과 마찬가지로 객체에도 추가 할 수 있다.',
log(message) {
console.log(message)
}
}

obj.log(obj.message)


const messages = [
'함수를 배열에 넣기',
message => console.log(message)
]

messages[1](messages[0])

const insideFn = logger => logger('함수를 다른 함수에 인자로 넘길 수도 있습니다.')
insideFn(message => console.log(message))

var createScream = function(logger) {
return function(message) {
logger(message.toUpperCase() + '!!!')
}
}

const scream = createScream(message => console.log(message))

scream('함수가 함수를 반환할 수도 있습니다.');
scream('createStream은 함수를 반환합니다.');
scream('scream은 createStream이 반환한 함수를 가리킵니다.');

//ES6

const createScream = logger => message => logger(message.toUpperCase() + '!!!')

명령형 프로그래밍과 선언적 프로그래밍 비교

함수형 프로그래밍은 선언적 프로그래밍이라는 더 넓은 프로그래밍 패러다임의 한 가지다. 선언적 프로그래밍은 필요한 것을 달성하는 과정을 하나하나 기술하는 것보다 필요한 것이 어떤 것인지 기술하는데 방점을 두고 어플리케이션의 구조를 세워나가는 프로그래밍 스타일이다.

명령형 프로그래밍은 코드로 원하는 결과를 달성해 나가는 과정에만 관심을 두는 프로그래밍 스타일이다.

var string = 'This is the midday show with cheryl Waters'
var urlFriendly = '';

for (var i = 0; i < string.length; i++ ) {
if (string[i] === ' ') {
urlFriendly += '-'
} else {
urlFriendly += string[i]
}
}

console.log(urlFriendly)

위의 코드를 선언적 프로그래밍으로 변경해보자.

const string = 'This is the midday show with cheryl Waters'
const urlFriendly = string.replace(/ /g, '-')
console.log(urlFriendly)

var target = document.getElementById('target')
var wrapper = document.createElement('div')
var headline = document.createElement('h1')

wrapper.id = 'welcome'
headline.innerText = 'Hello World'

wrapper.appendChild(headline)
target.appendChild(wrapper)

위처럼 코드를 작성하려면, 너무나 많은 코드가 필요로 하게 된다.위 작업을 리액트 컴포넌트를 사용해 DOM을 선언적으로 구성하는 방법을 살펴보자.

const { render } = ReactDOM

const Welcome = () => (
<div id="welcome">
<h1>Hello World</h1>
</div>
)

render(
<Welcome />
document.getElementById('target')
)

리액트는 선언적이다. 코드를 보면 알겠지만, 렌더링할 DOM을 기술한다. 그리고 render 함수는 컴포넌트에 있는 지시에 따라 DOM을 만든다.

3.3 함수형 프로그래밍의 개념

함수형 프로그래밍의 주요 개념인 불변성, 순수성, 데이터 변환, 고차 함수, 재귀, 합성을 알아보자.

불변성 (Immutability)

함수형 프로그래밍에서는 데이터가 변할 수가 없다. 불변성 데이터는 결코 바뀌지 않는다.

let color_lawn = {
title: "잔디",
color: "#00FF00",
rating: 0
}

function rateColor(color, rating) {
color.rating = rating
return color
}

console.log(rateColor(color_lawn,5).rating)
console.log(color_lawn.rating)

//위와 같은 경우 원본 객체의 값이 변경된다.

var rateColor = function(color, rating) {
return Object.assign({}, color, {rating:rating})
}

console.log(rateColor(color_lawn,5).rating)
console.log(color_lawn.rating) //값이 변경 되지 않는다.

const rateColor = (color, rating) => (
{
...color,
rating
}
)

위와 같이 애로우 함수와 스프레드 연산자로 같은 함수를 작성할 수도 있다.

var addColor = function(title, colors) {
colors.push({ title: title})
return colors;
}

console.log(addColor("매력적인 초록", colorArray).length)
console.log(colorArray.length)


const addColor = (title, array) => array.concat({title})
console.log(addColor("매력적인 초록", colorArray).length)
console.log(colorArray.length)

//const addColor = (title, array) => [...array, {title}]

순수 함수 (Pure Function)

순수함수는 파라미터에 의해서만 반환값이 결정되는 함수를 뜻한다.

var frederick = {
name: "Frederick Douglass",
canRead: false,
canWrite: false
}

//해당 함수는 인자가 없으며, 값을 반환하거나 함수를 반환하지 않는다.
function selfEducate() {
frederick.canRead = true;
frederick.canWrite = true;
return frederick
}

selfEducate()
console.log(frederick)

//위 함수를 순수 함수로 만들어보자.

var frederick = {
name: "Frederick Douglass",
canRead: false,
canWrite: false
}

//하지만, 이 함수도 순수함수는 아니다.
const selfEducate = (person) => {
person.canRead = true;
person.canWrite = true;
return person
}


console.log(selfEducate(frederick))
console.log(frederick)

var frederick = {
name: "Frederick Douglass",
canRead: false,
canWrite: false
}


const selfEducate = (person) =>
({
...person,
canRead: true,
canWrite: true
})


console.log(selfEducate(frederick))
console.log(frederick)

function Header(text) {
let h1 = document.createElement('h1');
h1.innerText = text;
document.body.appendChild(h1);
}

Header("Header() caused side effects")


//위 코드 차이
const Header = (props) => <h1>{props.title}</h1>

함수를 만드는 세 가지 규칙을 따르면, 순수함수를 만들 수 있다.

  1. 순수함수는 파라미터를 최소 하나 이상 받아야 한다.

  2. 순수함수는 값이나 다른 함수를 반환해야 한다.

  3. 순수함수는 인자나 함수 밖에 있는 다른 변수를 변경하거나 입출력을 수행해서는 안 된다.

데이터 변환 (transform data)

함수형 프로그래밍은 한 데이터를 다른 데이터로 변환하는 것이 전부다. 함수형 프로그래밍은 함수를 사용해 원본을 변경한 복사본을 만들어낸다.

함수형 자바스크립트를 유창하게 사용하기 위해 통달해야 하는 핵심 함수 2개가 있다. Array.mapArray.reduce가 바로 그것이다.

const schools = [
"seungdols",
"naver",
"google",
"netflix"
]

console.log(schools.join(", "))


const g = schools.filter(school => school[0] === 'g')
console.log(g)

filter 메소드는 원본 배열로부터 새로운 배열을 만들어내는 내장 함수이다. 인자로 predicate를 유일한 인자로 받는데, 이는 true 또는 false를 반환하는 함수를 뜻한다.

const companies = [
"seungdols company",
"naver",
"google",
"netflix"
]

const famousCompany = companies.map(company => `${company} is famous!!`)

console.log(famousCompany)

map함수는 변환 함수를 인자로 받는다. 그 함수를 배열의 모든 원소에 적용해서 반환 받은 값으로 새 배열을 반환한다.

const editName = (oldName, name, arr) =>
arr.map(item => {
if (item.name === oldName) {
return {
...item,
name
}
} else {
return item
}
})



const editName = (oldName, name, arr) =>
arr.map( item => (item.name === oldName) ? ({...item, name}) : item)

//원소와 인덱스를 넘겨주는 예시
const editNth = (n, name, arr) =>
arr.map( (item, i) => (i === n) ? ({...item, name}) : item )

let updateCompany = editNth(2, "gitlab", companies)

console.log(updateCompany[2])
console.log(companies[2])

    const schools = {
"Yorktown": 10,
"Washington & Lee": 2,
"Wakefield": 5
}

const schoolArray = Object.keys(schools).map(key =>
({
name: key,
wins: schools[key]
})
)

console.log(schoolArray)

배열의 값을 이용해 객체로 만드는데, 이는 중요하다. 함수형 프로그래밍에 필요한 도구는 배열을 기본 타입의 값이나 다른 객체로 변환하는 기능이다.

reducereduceRight 함수를 사용하면 객체를 수, 문자열, 불린 값, 객체, 함수로 변환할 수 있다.

    const ages = [21,18,42,40,64,63,34]

const maxAge = ages.reduce((max, age) => {
console.log(`${age} > ${max} = ${age > max}`)
if (age > max) {
return age
} else {
return max
}
}, 0)

console.log('maxAge', maxAge)

// 축약형
const max = ages.reduce(
(max, value) => (value > max) ? value : max,
0
)

console.log('max', max)

    const colors = [
{
id: '-xekare',
title: "과격한 빨강",
rating: 3
},
{
id: '-jbwsof',
title: "큰 파랑",
rating: 2
},
{
id: '-prigbj',
title: "큰곰 회색",
rating: 5
},
{
id: '-ryhbhsl',
title: "바나나",
rating: 1
}
]

const hashColors = colors.reduce(
(hash, {id, title, rating}) => {
hash[id] = {title, rating}
return hash
},
{}
)

console.log(hashColors)

const colors = ["red", "red", "green", "blue", "green"];

const distinctColors = colors.reduce(
(distinct, color) =>
(distinct.indexOf(color) !== -1) ?
distinct :
[...distinct, color],
[]
)

console.log(distinctColors)

고차함수 (High-Order Function)

함수형 프로그래밍에서는 고차함수가 꼭 필요하다. 고차함수는 다른 함수를 조작할 수 있는 함수다. 고차 함수는 다른 함수를 인자로 받거나 함수를 반환할 수 있고, 때로는 그 두가지를 모두 수행한다.

    const invokeIf = (condition, fnTrue, fnFalse) =>
(condition) ? fnTrue() : fnFalse()

const showWelcome = () =>
console.log("Welcome!!!")

const showUnauthorized = () =>
console.log("Unauthorized!!!")

invokeIf(true, showWelcome, showUnauthorized)
invokeIf(false, showWelcome, showUnauthorized)

커링은 고차 함수 사용법과 관련한 함수형 프로그래밍 기법이다. 커링은 어떤 연산을 수행 할 때 필요한 값 중 일부를 저장하고, 나중에 나머지 값을 전달 받는 기법이다. (이 표현은 커링을 설명하는데 오류가 조금 존재한다.)

    const getFakeMembers = count => new Promise((resolves, rejects) => {
const api = `https://api.randomuser.me/?nat=US&results=${count}`
const request = new XMLHttpRequest()
request.open('GET', api)
request.onload = () =>
(request.status === 200) ?
resolves(JSON.parse(request.response).results) :
reject(Error(request.statusText))
request.onerror = (err) => rejects(err)
request.send()
})

const userLogs = userName => message =>
console.log(`${userName} -> ${message}`)

const log = userLogs("grandpa23")

log("attempted to load 20 fake members")
getFakeMembers(20).then(
members => log(`successfully loaded ${members.length} members`),
error => log("encountered an error loading members")
)

재귀 (Recursion)

재귀는 자기 자신을 호출하는 함수를 만드는 기법이다.

    const countdown = (value, fn) => {
fn(value)
return (value > 0) ? countdown(value-1, fn) : value
}

countdown(10, value => console.log(value))

    const dan = {
type: "person",
data: {
gender: "male",
info: {
id: 22,
fullname: {
first: "Dan",
last: "Deacon"
}
}
}
}

const deepPick = (fields, object={}) => {
const [first, ...remaining] = fields.split(".")
return (remaining.length) ?
deepPick(remaining.join("."), object[first]) :
object[first]
}

console.log( deepPick("type", dan) )
console.log( deepPick("data.info.fullname.first", dan) )

다만, 브라우저 호출 스택 제한이 존재한다. 가능하면, 루프보다 재귀를 사용하는게 좋지만, 재귀의 깊이가 깊어질 수록 브라우저에서 최적화를 해주지 못하는데, 이때에 tail recursion기법을 활용하면 된다.

그리고 trampolinestream등의 기법을 사용해 재귀를 수동으로 최적화할 수 있다.

합성 (Composition)

합성의 경우 여러 가지 다른 구현과 패턴, 기법이 있다. 가장 익숙한 것은 chaining이다. composition은 함수와 함수를 인자로 받아 처리를 할 때, 하나의 함수에서 다른 함수들을 하나로 합친다라는 의미를 갖는다고 생각하면 편리하다.

    const template = "hh:mm:ss tt"
const clockTime = template.replace("hh", "03")
.replace("mm", "33")
.replace("ss", "33")
.replace("tt", "PM")

console.log(clockTime)

    const createClockTime = date => ({date})

const appendAMPM = ({date}) =>
({
date,
ampm: (date.getHours() >= 12) ? "PM" : "AM"
})

const civilianHours = clockTime => {
const hours = clockTime.date.getHours()
return {
...clockTime,
hours: (hours > 12) ?
hours - 12 :
hours
}
}

const removeDate = clockTime => {
let newTime = {...clockTime}
delete newTime.date
return newTime
}

// 앞의 모든 함수를 합성하지만 그렇게 좋은 방법은 아니다

const oneFunction = date =>
removeDate(
civilianHours(
appendAMPM(
createClockTime(date)
)
)
)

console.log(oneFunction(new Date()))

// 더 우아하게 함수를 합성하는 방법
//meta programming
const compose = (...fns) =>
(arg) =>
fns.reduce(
(composed, f) => f(composed),
arg
)



const oneFunction = compose(
createClockTime,
appendAMPM,
civilianHours,
removeDate
)

console.log(oneFunction(new Date()))

반응형