자바스크립트/러닝 리엑트

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

막이86 2023. 11. 13. 11:29
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])
  • 함수를 다른 함수에 인자로 넘길 수 있다.
    1. insideFn() 호출시 message ⇒ console.log(message) 함수를 전달
    2. logger는 message ⇒ console.log(message)
    3. 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);
  • 순수 함수를 사용할 때 세가지 규칙을 따라서 만들자
    1. 순수 함수는 파라미터로 최소 하나 이상 받아야 한다.
    2. 순수 함수는 값이나 다른 함수를 반환해야 한다.
    3. 순수 함수는 인자나 함수 밖에 있는 다른 변수를 변경하거나, 입출력을 수행해서는 안된다.

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);
    
  • 배열의 원소를 제거해야할 필요가 있다면 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