728x90
러닝 리엑트을 요약한 내용입니다.
3.1 함수형이란 무엇인가?
- 자바스크립트에서는 함수가 1급 시민이기 때문에 함수형 프로그래밍을 지원한다고 말할 수 있다.
- 1급 시민이라는 말은 정수나 문자열 같은 다른 일반적인 값과 마찬가지로 함수를 취급할 수 있다는 뜻 이다.
- var 키워드를 사용해서 함수를 정의할 수 있다.
- var log function(message) { console.log(message); } log("자바스크립트에서는 함수를 변수에 넣을 수 있습니다.");
- 화살표 함수를 사용해 같은 함수를 정의할 수 있다.
- const 키워드를 사용하면 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), "일반적인 값과 마찬가지입니다.", message => console.log(message) ] messages[1](messages[0]) messages[3](messages[2])
- 함수를 다른 함수에 인자로 넘길 수 있다.
- insideFn() 호출시 message ⇒ console.log(message) 함수를 전달
- logger는 message ⇒ console.log(message)
- insideFn이 호출됨으로 logger 함수가 호출
const insideFn = logger => { logger("함수를 다른 함수에 인자로 넘길수도 있습니다."); }; insideFn(message => console.log(message));
- 함수가 함수를 반환할 수도 있다.
- const createScream = function(logger) { return function(message) { logger(message.toUpperCase() + "!!!"); } } const scream = createScream(message => console.log(message)); scream("함수가 함수를 반환할 수도 있습니다."); scream("createScream은 함수를 반환합니다."); scream("scream은 createScream이 반환한 함수를 가리킵니다.");
- 함수가 함수를 인자로 받는 경우, 함수가 함수를 반환하는 경우를 고차 함수라고 부른다.
- 2개 이상의 화살표가 있다면 고차 함수를 사용하고 있다는 뜻이다.
const createScream = logger => message => { logger(message.toUpperCase() + "!!!"); };
3.2 명령형 프로그래밍과 선언적 프로그래밍 비교
- 함수형 프로그래밍은 선언적 프로그래밍이라는 더 넓은 프로그래밍 패러다임의 한 가지이다.
선언적 프로그래밍
- 필요한 것을 달성해가는 과정을 하나하나 기술하는 것보다 필요한 것이 어떤 것인지를 기술하는 것에 더 방점을 두고 구조를 세워나가는 프로그래밍 스타일
- 문자열을 변경하는 선언적 프로그래밍 스타일 코드replace라는 함수를 사용해 추상적인 개념을 표현
- const string = " Restaurants in Hanalei"; const urlFriendly = string.replace(" ", "-"); console.log(urlFriendly);
- 모든 공백을 하이픈으로 바꾸는 자세한 방법은 replace 함수안에 정의
- 선언적 접근 방식이 더 읽기 쉽고, 그렇기 때문에 더 추론하기 쉽다.
- 각 함수가 어떻게 구현되었는지는 함수라는 추상화 아래에 감춰진다.
- 선언적 프로그래밍은 추론하기 쉬운 애플리케이션을 만들어내며, 애플리케이션에 대한 추론이 쉬우면 그 애플리케이션의 규모를 확장하는 것도 더 쉽기 마련이다.
- DOM을 만드는 과정을 선언적 프로그래밍 스타일로 구성
- const { render } = ReactDOM; const Welcome = () => ( <div id="welcome"> <h1>Hello Wordl</h1> </div> ); render(<Welcom />, document.getElementById("target"));
명령형 프로그래밍
- 코드로 원하는 결과를 달성해 나가는 과정에만 관심을 두는 프로그래밍 스타일
- 문자열을 변경하는 명령형 프로그래밍 스타일 코드
const string = " Restaurants in Hanalei"; var urlFriendly = ""; for (var i=0; i<string.length; i++) { if (string[i] === " ") { urlFriendly += "-"; } else { urlFriendly += string[i]; } } console.log(urlFriendly);
- 코드 안에서 어떤 일이 벌어지는지 코드를 읽는 사람이 더 잘 이해할 수 있게 주석을 많이 달 필요성이 있다.
- DOM을 만드는 과정을 명령형 프로그래밍 스타일로 구성
- 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);
- 문자열을 변경하는 명령형 프로그래밍 스타일 코드
3.3 함수형 프로그래밍 개념
- 함수형 프로그래밍의 핵심 개념임 불변성, 순수성, 데이터 변환, 고차 함수, 재귀 에 대해 소개할 것이다.
3.3.1 불변성
- 함수형 프로그래밍에서는 데이터가 변할 수 없다.
- 불변성 데이터는 결코 바뀌지 않는다.
- 원본 데이터 구조를 변경하는 대신 그 데이터 구조의 복사본을 만들되 그중 일부를 변경한다.
- 원본 대신 변경한 복사본을 사용해 필요한 작업을 진행한다.
- 객체를 넘겨서 값을 변경하는 예제
- let color_lawn = { title: "잔디", color: "#00FF00", rating: 0 } function rateColor(color, rating) { color.rating = rating; return color; } console.log(rateColor(color_lawn, 5).rating); // 5 console.log(color_lawn.rating); // 5
- 객체의 값을 복사하여 변경하는 예제
- Object.assign을 사용해서 값을 복사한 후에 rating 값을 변경
let color_lawn = { title: "잔디", color: "#00FF00", rating: 0 } var rateColor = function(color, rating) { return Object.assign({}, color, {rating: rating}); }; console.log(rateColor(color_lawn, 5).rating); // 5 console.log(color_lawn.rating); // 0
- Array.push 를 사용하여 색을 추가하는 예제
- Array.push는 불변성 함수가 아니다.
let list = [ { title: "과격한 빨강" }, { title: "잔디" }, { title: "파티 핑크" } ]; const addColor = function(title, colors) { colors.push( {title: title} ); return colors; }; console.log(addColor("화려한 녹색", list).length); // 4 console.log(list.length); // 4
- list 배열을 변화시키지 않고 유지하기 위해서는 Array.concat을 사용
- let list = [ { title: "과격한 빨강" }, { title: "잔디" }, { title: "파티 핑크" } ]; const addColor = (title, array) => array.concat({ title }); console.log(addColor("화려한 녹색", list).length); // 4 console.log(list.length); // 3
- 스프레드 연산자를 이용해서 배열을 복사할 수 있다.
- let list = [ { title: "과격한 빨강" }, { title: "잔디" }, { title: "파티 핑크" } ]; const addColor = (title, array) => [...list, { title }]; console.log(addColor("화려한 녹색", list).length); // 4 console.log(list.length); // 3
3.3.2 순수 함수
- 순수 함수는 파라미터에 의해서만 반환값이 결정되는 함수를 뜻함
- 순수 함수는 최소한 하나 이상의 인수를 받고, 인자가 같으면 항상 같은 값이나 함수를 반환한다.
- 순수 함수에는 부수 효과(Side Effect)가 없다
- 순수 함수는 인수를 변경 불가능한 데이터로 취급 한다.
- 순수하지 않은 함수
- 함수는 인자를 취하지 않으며, 값을 반환하거나 함수를 반환하지도 않는다.
- frederick 값이 바뀌기까지 한다.
const frederick = { name: "Frederick Douglass", canRead: false, canWrite: false }; function selfEducate() { frederick.canRead = true; frederick.canWrite = true; return frederick; }; selfEducate(); console.log(frederick);
- 다른 순수하지 않은 함수
- 파라미터를 받고 값을 반환하지만 값이 변경이 되었다.
const 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);
<aside> 💡 순수 함수는 자신의 환경 또는 '세계'를 변화시키지 않기 떄문에 복잡한 테스트 준비 과정이나 정리 과정이 필요하지 않기 때문에 테스트하기가 쉽다.
</aside>
- 순수 함수로 만들기
- const frederick = { name: "Frederick Douglass", canRead: false, canWrite: false }; const selfEducate = (person) => ({ ...person, canRead: true, canWrite: true }); console.log(selfEducate(frederick)); console.log(frederick);
- 순수 함수를 사용할 때 세가지 규칙을 따라서 만들자
- 순수 함수는 파라미터로 최소 하나 이상 받아야 한다.
- 순수 함수는 값이나 다른 함수를 반환해야 한다.
- 순수 함수는 인자나 함수 밖에 있는 다른 변수를 변경하거나, 입출력을 수행해서는 안된다.
3.3.3 데이터 변환
- 함수형 프로그래밍은 한 데이터를 다른 데이터로 변환하는 것이 전부다.
- 함수형 프로그래밍은 함수를 사용해 원본을 변경할 복사본을 만들어 낸다.
- 순수 함수를 사용해 데이터를 변경하면, 코드가 덜 멸여형이 되고 그에 따라 복잡도도 감소한다.
- Array.join 함수를 사용하여 콤마(,)로 각 학교를 구분한 문자열을 얻을 수 있다.
- join은 배열의 모든 원소를 인자로 받고 문자열을 반환한다.
- 원본 데이터는 그대로 남는다.
const schools = ["Yorktown", "Washington & Lee", "Wakefield"]; console.log(schools.join(", "));
- Array.filter 메서드를 사용해 W로 시작하는 학교만 있는 새로운 배열을 만든다.
- 술어(predicate)를 유일한 인자로 받는다.
- 술어는 Boolean 값, True, False를 반환하는 함수를 뜻한다.
- 술어가 반환하는 값이 True이면 해당 원소를 새 배열에 넣는다.
const schools = ["Yorktown", "Washington & Lee", "Wakefield"]; const wSchools = schools.filter(school => school[0] === 'W'); console.log(wSchools);
- 술어(predicate)를 유일한 인자로 받는다.
- 배열의 원소를 제거해야할 필요가 있다면 Array.pop이나 Array.splice보다는 Array.filter를 사용하자
- const schools = ["Yorktown", "Washington & Lee", "Wakefield"]; const cutSchool = (cut, list) => list.filter(school => school !== cut); console.log(cutSchool("Washington & Lee", schools).join(", ")); console.log(schools.join("\\n"));
- Array.map은 배려의 모든 원소에 적용해서 반환받은 값으로 이뤄진 새 배열을 반환한다.
- 원본 schools 배열은 아무 변화가 없다.
const schools = ["Yorktown", "Washington & Lee", "Wakefield"]; const highSchools = schools.map(school => `${school} Hight School`); console.log(highSchools.join("\\n")); console.log(schools.join("\\n"));
- map 함수는 객체, 값, 배열, 다른 함수 등 모든 자바스크립트 타입의 값으로 이뤄진 배열을 만들 수 있다.
- const schools = ["Yorktown", "Washington & Lee", "Wakefield"]; const highSchools = schools.map(school => ({ name: school })); console.log(highSchools);
- 배열의 원소중 하나만을 변경하는 순수 함수가 필요할 때도 map을 사용할 수 있다.
- 이 코드는 잘 이해가 되지 않음 ㅠㅠ
let schools = [ { name: "Yorktown" }, { name: "Stratford" }, { name: "Washington & Lee" }, { name: "Wakefield" } ]; const editName = (oldName, name, arr) => arr.map(item => { if (item.name === oldName) { return { ...item, name }; } else { return item; } }); let updatedSchools = editName("Stratford", "HB Woodlawn", schools); console.log(updatedSchools[1]); console.log(schools[1]);
- 객체를 배열로 변환하고 싶을때는 Array.map과 Object.keys를 함께 사용하면 된다.
- Object.keys는 어떠 객체의 키로 이뤄진 배열을 반환하는 메서드다.
- Object.keys는 학교 이름의 배열을 반환한다.
const schools = { "Yorktown": 10, "Washington & Lee": 2, "Wakefield": 5 }; const schoolArray = Object.keys(schools).map(key => ({ name: key, wins: schools[key] })); console.log(schoolArray);
- reduce와 reduceRigth 함수를 사용하면 객체를 수, 문자열, 불린 값, 객체, 심지어 함수와 같은 값으로 변환할 수 있다.
- reduce를 사용해 최대값을 찾는 예제
- 초기값은 0 이다.
- 처음 최대값 max는 0 이다.
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)
- if/else를 짧게 변경
const ages = [21, 18, 42, 40, 64, 63, 34]; const maxAge = ages.reduce((max, value) => (value > max ? value : max), 0); console.log('maxAge', maxAge)
<aside> 💡 Array.reduceRight는 Array.reduce와 같은 방식으로 동작하지만 배열의 첫번째 원소가 아닌라 맨 마지막 원소부터 시작한다는 점이 다르다.
</aside>
- 값이 들어 있는 배열을 해시로 변환한다.
- 초기 값은 빈 객체{} 이다.
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);
- reduce를 사용해 배열을 전혀 다른 배열로 만들 수도 있다.
- 이 코드 동작 안함
const colors = ["red", "red", "greend", "blue", "green"]; const uniqueColors = colors.reduce( (unique, color) => unique.indexOf(color) !== -1 ? unique : [...unique, color], [] ); console.log(uniqueColors)
<aside> 💡 map과 reduce는 함수평 프로그래머가 주로 사용하는 무기이며 자바스크립트도 예외가 아니다.
</aside>
3.3.4 고차 함수
- 함수형 프로그래밍에서는 고차 함수가 꼭 필요하다.
- 고차 함수는 다른 함수를 인자로 받을 수 있거나 함수를 반환할 수 있고, 때로는 그 두가지를 모두 수행한다.
- Array.map, Array.filter, Array.reduce는 모두 고차 함수다.
- 조건을 검사해서 조건이 참인 경우 fnTrue 함수를, 거짓인 경우 fnFalse를 호출 하는 예제
- const invokeIf = (condition, fnTrue, fnFalse) => (condition) ? fnTrue() : fnFalse(); const showWelcom = () => console.log("welcom!!!"); const showUnauthorized = () => console.log("Unauthorized!!!"); invokeIf(true, showWelcom, showUnauthorized); invokeIf(false, showWelcom, showUnauthorized);
- 다른 함수를 반환하는 고차 함수는 자바스크립트에서 비동기적인 실행 맥락을 처리할 때 유용한다.
- 고차 함수를 쓰면 필요할 때 재활용할 수 있는 함수를 만들 수 있다.
커링(Currying)
- 고차 함수 사용법과 과련한 함수형 프로그래밍 기법
- 커린은 어떤 연산을 수행할 때 필요한 값 중 일부를 저장하고 나중에 나머지 값을 전달받는 기법
- 다른 함수를 반환하는 함수를 사용하며, 이를 커링된 함수라고 부른다.
- 커링 예제 코드
- 2장에서 살펴본 getFakeMembers를 사용
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") )
3.3.5 재귀
- 재귀는 자기 자신을 호출하는 함수를 만드는 기법이다.
- 루프를 모두 재귀로 바꿀수 있다.
- 일부 루프는 재귀로 표현하는 쪽이 더 쉽다.
- 10부터 0까지 거꾸로 세는 코드
- 현재 값이 0보다 크면 countdown이 자기 자신을 호출하되 값을 감소시켜 호출 한다.
- 값이 0 이하가 되고 countdown이 그값을 돌려주면 호출 스택을 거슬러 올라가면서 값이 전달 된다.
const countdown = (value, fn) => { fn(value); return (value > 0) ? countdown(value - 1, fn) : value; } countdown(10, value => console.log(value))
- 지연 시간을 두고 10부터 0까지 거꾸로 세는 코드
- const countdown = (value, fn, delay = 1000) => { fn(value); return (value > 0) ? setTimeout(() => countdown(value - 1, fn), delay) : value; } const log = value => console.log(value); countdown(10, log);
- 데이터 구조를 검색할 때도 재귀가 유용하다.
- 재귀를 통해 객체에 내포된 값을 찾아내는 예제 코드
- 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))
<aside> 💡 가능하면 루프보다는 재귀를 사용하자.
</aside>
3.3.6 합성
- 함수형 프로그램은 로직을 구체적인 작업을 담당하는 여러 작은 순수 함수로 나눈다.
- 그 과정에서 언젠가는 모든 작은 함수를 한데 합칠 필요가 있다.
- 합성의 경우 여러 다른 구현과 패턴과 기법이 있다.
- 가장 낮익은 것은 함수를 연쇄 호출하는 체이닝일 것이다.
- 시간, 분, 초 오전오후 정보를 차례로 새로운 값으로 변환하는 예제 코드
- 템플릿 자체는 바뀌지 않았으므로 나중에 다시 비슷한 시각 표시가 필요할때 재사용 가능
const template = "hh:mm:ss tt"; const clockTime = template.replace("hh", "03") .replace("mm", 33) .replace("ss", "33") .replace("tt", "PM"); console.log(clockTime);
- 더 큰 함수로 조합해주는 compose 함수를 사용
- compose는 여러 함수를 인자로 받아서 한 한수로 결과를 내놓는다.
- 스프레드 연산자를 사용해 인자로 받은 함수들은 fns라는 배열로 만든다.
- 마지막 함수가 호출되면 최종 결과를 반환 한다.
const compose = (...fns) => (arg) => fns.reduce((composed, f) => f(composed), arg); const both = compose( civilianHours, appendAMPM ) both(new Date());
3.3.7 하나로 합치기
- 지금까지 함수형 프로그래밍의 핵심 개념을 소개했다.
- 이제 이런 개념을 한대 모아서 작은 자바스크립트 애플리케이션을 만들어보자.
- 시계를 명령형으로 구현한 코드를 살펴보자
- 함수가 길고 복잡하며 하는 일도 많다.
- 이런 함수는 주석이 없이 이해하기는 어렵고 유지보수 하기도 힘들다.
function getClockTime() { var date = new Date(); var time = ""; var time = { hours: date.getHours(), minutes: date.getMinutes(), seconds: date.getSeconds(), ampm: "AM" } if (time.hours == 12) { time.ampm = "PM"; } else if (time.hours > 12) { time.ampm = "PM"; time.hours -= 12; } if (time.hours < 10) { time.hours = "0" + time.hours; } if (time.minutes < 10) { time.minutes = "0" + time.minutes; } if (time.seconds < 10) { time.seconds = "0" + time.seconds; } return time.hours + ":" + time.minutes + ":" + time.seconds + " " + time.ampm; } function logClockTime() { var time = getClockTime(); console.clear(); console.log(time); } setInterval(logClockTime, 1000);
- 로직을 더 작은 부분인 함수로 나누자
- 각 함수는 한가지 작업에 초점을 맞추고 여러 함수를 합성해서 더 큰 함수를 만드는 방법으로 시계를 만들자
- 함수형 프로그래밍으로 변경한 예제
- const compose = (...fns) => (arg) => fns.reduce((composed, f) => f(composed), arg); const oneSecond = () => 1000; const getCurrentTime = () => new Date(); const clear = () => console.clear(); const log = message => console.log(message); const abstractClockTime = date => ({ hours: date.getHours(), minutes: date.getMinutes(), seconds: date.getSeconds() }); const civilianHours = clockTime => ({ ...clockTime, hours: clockTime.hours > 12 ? clockTime.hours - 12 : clockTime.hours }); const appendAMPM = clockTime => ({ ...clockTime, ampm: clockTime.hours >= 12 ? "PM" : "AM" }); const display = traget => time => traget(time); const formatClock = format => time => format.replace("hh", time.hours) .replace("mm", time.minutes) .replace("ss", time.seconds) .replace("tt", time.ampm); const prepenZero = key => clockTime => ({ ...clockTime, [key]: (clockTime[key] < 10) ? "0" + clockTime[key] : clockTime[key] }); const convertToCivilianTime = clockTime => compose(appendAMPM, civilianHours)(clockTime); const doubleDigits = civilianTime => compose( prepenZero("hours"), prepenZero("minutes"), prepenZero("seconds"), )(civilianTime); const startTicking = () => setInterval(compose( clear, getCurrentTime, abstractClockTime, convertToCivilianTime, doubleDigits, formatClock("hh:mm:ss tt"), display(log) ), oneSecond() ); startTicking();
마무리
이번 장에서는 함수형 프로그래밍의 원리르 소개했다. 이책 전체에서 리액트와 플럭스의 가장 좋은 사례에 대한 이야기를 하면서 두 라이브러리가 모두 함수형 기법을 기반으로 만들어졌음을 보여줄 것이다.
728x90
'자바스크립트 > 러닝 리엑트' 카테고리의 다른 글
데이터 포함시키기 (0) | 2023.11.13 |
---|---|
훅스 컴포넌트 개선하기 (1) | 2023.11.13 |
리액트 상태 관리 (0) | 2023.11.13 |
JSX를 사용하는 리액트 (1) | 2023.11.13 |
리액트의 작동 원리 (0) | 2023.11.13 |