ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Javascript] 함수형 프로그래밍 (+제너레이터/이터러블/이터레이터)
    언어, 프레임워크/Javascript & Typescript 2021. 10. 19. 14:37

     

    인프런 강의를 통해 배운 내용과 궁금한 점에 대해 정리한 글입니다.

    틀린 부분이 있다면 말씀 부탁드립니다!


    함수형 프로그래밍이라는 단어를 듣기만 한 상태에서 강의를 보기 시작했다. 강의를 반 정도 본 지금, 평소에 내가 쓰던 코드가 함수형 프로그래밍 코드와 비교했을 때 얼마나 더러운(^^) 코드인지 알게 되었다. 아직 완강을 못 한 상태이기 때문에 이번 글에서는 함수형 프로그래밍에 대한 기본적인 정의와 사용 시 이점, 그리고 강의에서 언급된 제너레이터, 이터러블, 이터레이터에 대해 정리해보려 한다.

     

    1. 함수형 프로그래밍은 선언적인 프로그래밍 패러다임이다.

    선언적 프로그래밍이란? (+명령적 프로그래밍이란?)

    더보기

    명령적 Imperative 프로그래밍  ↔ 선언적 Declarative 프로그래밍

    - 명령적 프로그래밍은 우리가 원하는 값을 얻기 위해 무엇을 어떻게(How) 해야 되는가를 서술한다.

    - 선언적 프로그래밍은 우리가 원하는 값이 무엇(What)인가를 서술한다.

     

    배열 안에 있는 요소를 차례대로 더한 값을 구하는 코드를 명령적/선언적 프로그래밍으로 작성해보면 아래와 같다.

    const list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    // 명령형 프로그래밍
    let result = 0;
    for(let i = 0; i < list.length; i++) {
        result += list[i];
    }
    
    // 선언형 프로그래밍
    let result = list.reduce((prev, curr) => prev + curr, 0);

    위 코드에서 보이듯이, 명령어 프로그래밍은 결과 값을 얻기 위해 배열의 각 요소를 순환하고, 요소를 선택하여 result에 더하는 것을 개발자가 구현해야 된다. 반면 선언형 프로그래밍은 reduce 함수를 이용하였기 때문에 단순히 우리가 원하는 결과가 무엇인지에 대해서만 집중할 수 있다.

    그렇다면 선언형 프로그래밍을 하게 되면 명령형 프로그래밍은 머릿속에서 지워도 될까? 답은 아니다. 위 예시에서 reduce는 이미 누군가가 만들어놓은 내장 함수이기 때문에, 사용자는 reduce가 어떻게 구현되어있는지 신경 쓰지 않고 함수의 기능에만 집중하면 된다(추상화). 하지만 선언형 프로그래밍을 하다 보면 직접 함수를 만들어야 하는 경우도 생길 것이다. 결국 선언형 프로그래밍의 기반은 추상화된 명령형 프로그래밍된 코드라는 의미다.

     

    - 좀 더 자세한 설명이 필요하다면 https://iborymagic.tistory.com/73  블로그 참고.

     

    2. 함수형 프로그래밍의 핵심 아이디어

    함수형 프로그래밍에서는 데이터는 불변성을 갖고(Immutable), 모든 함수는 순수 함수이다(Pure functions).

    함수형 프로그래밍을 검색하면 꼭 나오는 키워드들에 대해 간단히 정리해보겠다.

     

    ① 순수 함수(Pure functions)

    • 입력 값이 동일할 때 출력 값도 동일한게 보장
    • 순수 함수 실행시, 함수 외부에 있는 어떠한 코드에도 영향을 끼치지 않음 (Side Effect 없음)
    • 순수 함수는 입력 파라미터 외에 함수 외부의 어떠한 값에도 의존하지 않음 (참조 투명성)

     

    합성 함수(Function composition)

    • 새로운 함수를 만들거나, 연산을 위해 2개 이상의 함수를 조합하는 과정
    • 여러 개의 함수를 합성하여 확장된 함수 작성 가능

     

    공유 상태 피하기 (Avoid shared state)

    • 공유 상태(shared state)는 동일한 클로저 내에서 변수를 공유하는 것을 의미
      - 공유 상태의 문제점 1: 공유되는 변수에 대해 히스토리를 파악해야 한다는 점
      - 공유 상태의 문제점 2: 함수 호출 순서가 변경되면 공유되고 있는 예상한 결과 값과 달라질 수 있다는 점 
    • 순수 함수와 합성 합수를 통해 공유 상태로 인해 발생하는 문제점을 피할 수 있음
      → 실행 순서를 신경쓰지 않아도 된다는 의미가 아님!
      f1(f2()) !== f2(f1())

    상태 변경 피하기(Avoid mutating state)

    부작용 피하기(Avoid side effects)

     

    3. 함수형 프로그래밍을 사용하면 좋은 점

    • Immutable → Side effect 피할 수 있음
    • 명확하고 직설적  예측하기 쉽고, 간결하고, 테스트하기 수월

     


    이터러블 Iterable

    이터러블(Iterable)은 '순회 가능한' 이라는 의미로, 이터러블 객체들은 for ...of 문법으로 순회할 수 있다. 테스트해보면 알겠지만, 배열이나 문자열 타입은 for ...of 문법으로 순회 가능한 반면 객체타입은 에러가 발생한다(Uncaught TypeError: obj is not iterable). Iterable 객체는 Symbol.iterator 메소드 를 속성으로 갖고 있다.

    obj 일반 객체에 Symbol.iterator 속성 유무를 확인했을 때 undefined로 뜨는 것을 확인할 수 있다.

    Iterable 인 것들

    • Arrays
    • Strings
    • Maps
    • Sets
    • DOM data structures (work in progress)

     

    Iterable과 Iterable이 아닌 객체의 차이

    https://jsben.ch/

     

     

    cf) ES6에 왜 Iterable interface 개념이 추가되었을까?

    Iterable이 없었을 때는 배열에서의 순환과 객체에서의 순환이 다른 것처럼 다양한 데이터 타입에서의 순환을 다루기 번거로웠다. 또한 ES6에서 Set과 Map이라는 새로운 데이터 구조가 등장했고, 이러한 데이터 구조에서 순환 로직을 작성하는 건 더 복잡해졌을 것이다. 이것이 Iterable interface가 탄생한 이유이다. 

     

    이터레이터 Iterator

    Iterator가 무엇인지 알아보기 위해, Iterable 객체인 배열의 속성 Symbol.iterator 메소드를 실행해 반환되는 Iterator 객체를 새로운 변수에 할당해보고 확인해보자.

    배열의 속성으로 Symbol.iterator라는 함수가 존재하는걸 볼 수 있다.

    list 배열 속성인 Symbol.iterator 메소드를 실행하여 반환된 값을 iterator라는 변수에 할당해서 console.log로 확인해보자. 

    이터레이터 객체에 next라는 함수가 존재한다

    선언한 변수에 Iterator 객체가 할당되어있는 것과 next 메소드를 가지고 있는 것을 확인할 수 있다. next 함수를 실행하면 value와 done 값을 갖는 객체가 반환된다. 

    맨 처음 실행한 next 함수와 그 다음에 실행한 next함수의 결과값이 다른 것을 볼 수 있다. 
    배열의 마지막 값인 10이 value값으로 반환되고, 그 다음에 실행한 next함수에선 value가 undefined이다.

    이렇게 Iterator 객체는 next 메소드를 지니고 있으며, next 메소드 실행 값은 value와 done을 키로 갖는 객체라는 것을 확인했다. 

    • value: 현재 순서의 값
    • done: 반복 종료 확인 (true or false)

     

    지금까지 내용을 요약해보면 다음과 같다.

    - 이터러블(Iterable) 객체는 이터레이터 속성(Symbol.iterator)이 있는 객체를 의미한다.

    - 이터레이터 속성은  메소드로서 그 실행값은 Iterator 객체이며 next 메소드를  갖고있다.

    - next 메소드를 실행하면 value와 done key를 갖는 객체를 반환한다.

     

    제너레이터 Generator

    Generator 함수의 가장 큰 특징은 일반 함수처럼 처음부터 끝까지 한 번에 실행되는게 아니라, 특정 지점까지 실행한 뒤 내가 원하는 때에 다시 함수를 실행하여 값을 얻을 수 있다는 점이다. 즉 일반함수와 달리 지금 당장 필요없는 값을 얻기위해 연산하지 않고 값이 필요할 때 연산할 수 있으므로 퍼포먼스 측면에서 효율적이라 할 수 있다. 일반함수와 비교를 위해 코드로 작성해 정리해보겠다. 참고로 Generator 함수는 function* 키워드를 사용하여 선언한다. (Generator 함수를 실행하면 Generator 객체가 반환되며, 이는 Iterable 객체이자 Iterator이다.)

    // function* 을 통해 제너레이터 함수 선언
    function* infinityFunc() {
        let a = 0;
        while(true) {
            yield a++;
        };
    };
    
    let generator = infinityFunc();
    
    generator.next(); // {value: 0, done: false}
    generator.next(); // {value: 1, done: false}
    generator.next(); // {value: 2, done: false} ...

     

    일반 함수와 다른 점을 또 하나 발견할 수 있는데, 바로  yield 키워드이다. yield를 통해 Generator 함수를 중지할수도 있고 다시 시작시킬수도 있다. 덕분에 일반 함수였다면 브라우저가 멈출 수 있는 무한대 함수를 Generator 함수로 작성하여 next() 메소드로 하나씩 값을 뽑아낼 수 있다. Generator 함수에는 throw와 return도 있다고 하는데 관련해서는 다음 포스팅에 이어서 쓰도록 하겠다.

     

     

     

    이터러블, 이터레이터, 제너레이터에 대한 기본적인 개념을 정리하며, 이것이 함수형 프로그래밍을 할 때 도움이 되기를 바란다. 물론 레퍼런스들을 참고하며 글로 정리한 것과 내가 직접 활용해가며 느낀 것은 천지차이겠지만 혼란스러움(?)이 어느정도 가라앉은 것에 대해서는 만족한다.


    참고 자료

    댓글

jaejade's blog ٩( ᐛ )و