방구석에 놔둔 개발 노트

1년차 웹 / 앱 프론트엔드 엔지니어의 좌충우돌 얼렁뚱땅 앞뒤짱구 생존기

2022-12-11: 33가지의 JavaScript 개념 학습 (4)

목차

 1️⃣ 명시적 변환, 암시적 변환, Nominal, 구조화, 덕 타이핑

 2️⃣ 고차함수

 3️⃣ 컬렉션과 생성기

 4️⃣ 순수함수, 부수효과, 상태변이

 5️⃣ 닫기/폐쇄 (Closure)

1️⃣ 명시적 변환, 암시적 변환, Nominal, 구조화, 덕 타이핑

  • 명시적 형변환과 암시적 형변환
    • 명시적 형변환

      • 사용자가 의도를 가지고 직접 데이터 타입을 변환시키는 것을 말한다.
      • JavaScript의 명시적 형변환 방법으로 메소드를 활용하는 것이 있다.
        • 숫자로 형변환시켜주는 메소드: Number(), parseInt(), parseFloat()
        • 문자열로 형변환시켜주는 메소드: String(), toString(), toFixed()
        • 불린 타입으로 형변환시켜주는 메소드: Boolean()
    • 암시적 형변환

      • 컴파일러에 의해 자동으로 데이터 타입을 변환시키는 것을 말한다.
      • JavaScript의 암시적 형변환은 주로 산술 연산자나 느슨한 비교 연산자에서 이루어진다.
        • 산술 연산자
          • +: 문자열이 우선되므로, 숫자 + 문자는 문자열로 변환된다.
          • 그 외: 숫자형이 우선되므로, 숫자 - / * % 문자는 숫자로 변환된다.
        • 느슨한 비교 연산자
          • ==: 숫자와 문자열을 비교할 때 느슨한 비교를 통해 타입을 고려하지 않고 값이 같으면 true로 반환한다.
    • JavaSciript의 타이핑

      • 명목적 타이핑(Nominal Typing)

        • 특정 키워드를 이용해 타입을 지정해 사용하는 방식이다.
          int a = 3;
          a = 's' // Error
        

        int와 같이 선언하고자 하는 변수에 타입을 지정함으로서 에러를 방지할 수 있다.

        C나 Java, Python에서 사용되고 있다.

      • 덕 타이핑(Duck Typing)

        • 타입을 미리 정하는 게 아닌, 객체의 메소드 존재 여부 등으로 객체의 타입을 결정해 사용하는 방식이다.
        • JavaScript가 이 방식을 사용한다.
        • 덕 타이핑이라는 이름의 유래는 ‘덕 테스트(Duck Test)’에서 유래가 됐다.

          만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다.

          class Duck {
            quack() {
              console.log('꽥!')
            }
            feathers() {
              console.log('깃털은 검정색과 흰색')
            }
          }
        
          class Human {
            quack() {
              console.log('사람인데요? 꽥!')
            }
            feathers() {
              console.log('사람이라 깃털은 없어요. 하지만 털은 있습니다.')
            }
          }
        
          function inTheForest(duck) {
            duck.quack()
            duck.feathers()
          }
        
          inTheForest(new Duck())
          inTheForest(new Human())
        

        위의 함수 inTheForestduck이라는 매개변수, 파라미터를 가진다. 함수의 안에는 duck의 메소드인 quack()feathers()가 지정되어있다.

        함수 이전에 우리는 인간이라는 Human과 오리라는 Duckclass로 선언한 뒤, inTherForest()를 이용해 결과를 적용해봤다.

        그 결과 Human이든 Duck이든 class 안에 quack()features()메소드가 있으므로 둘 다 inTheForest()의 값을 반환한다.

        • duck이라는 파라미터가 들어올 때에는 값의 데이터 타입을 검사하지 않는다.
          • 함수의 파라미터로 어떠한 것이든 다 들어올 수 있다.
          • 그러나, 함수 내에서 실행할 때 조건을 충족하지 않으면 에러를 반환하게 된다.
        • 이러한 덕 타이핑은 빠른 개발을 유도하고, 타입에 구애받지 않으므로 생산성을 높일 수 있다.
        • 하지만, 타입 체크를 하지 않으므로 의도에 맞지 않은 코딩의 결과가 나올 수도 있다.
      • 구조적 타이핑(Structural Typing)

        • 실제 구조와 정의에 의해 결정되는 방식이다.
        • 덕 타이핑은 정의된 메소드를 포함하는가를 가리킨다면, 구조적 타이핑은 정의한 타입들을 가진 구조를 갖추고 있는가를 중요하게 여긴다.
        • JavaScript의 확장된 언어인 TypeScript이나 GO에서 이 방식을 사용하고 있다.
          interface Vector2D {
                  x: number;
              y: number;
          }
        
          function calculateLength(v: Vector2D) {
              return v.x + v.y;
          }
        
          interface NamedVector {
              name: string;
                  x: number;
              y: number;
          }
        
          const v: NamedVector = { x: 3, y: 4, name: 'Zee' };
          calculateLength(v) // 7
        

        위의 코드에서 함수 calculateLength는 약속을 정의내린 interface, Vector2D의 내용대로 매개변수의 타입을 체크한다.

        그런데 여기서 NamedVector라는 새로운 interface로 약속을 정의내린 걸 가지고 함수 calculateLength에 사용하려고 한다.

        NamedVectorVector2D와 내용이 다르므로 함수가 에러를 내뱉을 수 있다고 생각할 수 있으나, NamedVectorVertor2D의 구조에서 name key가 추가된 것뿐이다.

        따라서 기본적인 Vector2D의 구조를 갖추고, 거기에 key 하나가 추가된 것이므로 구조적으로 결함이 없다고 판단하여 함수 calculateLength가 문제 없이 실행된다.

        • 구조적 타이핑은 일종의 집합 관계를 확인하는 것과 같다.
          • 따라서, 중복된 범위가 존재한다면 호환이 가능하며 이로 인해 재사용할 수 있는 코드를 만들어낼 수 있고 이는 코드의 생산성에도 좋은 영향을 줄 수 있다.

        (공부하는 입장에선 아직 이 구조적 타이핑 내용이 와닿지 않는다. 추후 타입스크립트를 공부하고 재정리해보려고 한다.)

2️⃣ 고차함수

  • 이전에 학습했던 내용이 있어 해당 내용을 발췌했다.
  • 고차함수(Higher Order Function)

    • 함수를 인자로 받거나 함수를 반환값으로 받는 함수를 가리킨다.
    • JavaScript에서의 함수는 Object이다.
      • 이전의 프로토타입 체인 등을 공부했던 내용을 떠올려보면 JavaScript에서의 함수는 객체로 받고 있다는 것을 알 수 있을 것이다.
        • 객체이기 때문에, 함수 표현식을 이용해 함수를 변수에 할당할 수 있다.
        • 또한, 다른 함수의 인자에 함수를 전달할 수도 있다.
        • 아울러 함수의 반환값을 함수로서 전달할 수 있다.
          const message = () => {
              console.log('함수는 객체라서, 이렇게 변수에 할당할 수 있다.')
          }
        
          function example(message) {
              console.log('변수에 할당할 수 있다보니, 함수의 인자로도 전달할 수 있다.')
              return message;
              // 그리고 함수의 반환값으로 함수를 전달할 수도 있다.
          }
        

        위의 내용을 콘솔창에 입력하면 아래와 같이 결과를 볼 수 있다.

        • 이처럼 변수에 할당할 수 있고, 다른 함수의 인자로도 전달할 수 있으며 다른 함수의 결과로서 반환될 수 있는 객체를 ‘일급 객체(First-class Object)’라고 부른다.
          • JavaScript의 함수는 그런 일급 객체의 조건을 갖추고 있다.
  • 콜백 함수(Callback Function)

    • 고차 함수의 형태 중에선 ‘다른 함수의 인자’로 전달되는 함수가 있는데, 이를 콜백 함수라고 부른다.

        const button = document.querySelector('#btn');
      
        button.addEventHandler('click', sayHello());
      
        function sayHello() {
            console.log("안녕하세요!")
        }
      

      위의 코드에서 sayHello()라는 함수는 button이라는 문서 객체 내에 addEventHandler라는 메소드(함수)를 통해 인자로 보내진다.

      이 인자는 버튼을 클릭하기 전까지는 동작하지 않으나, 버튼을 클릭하는 이벤트가 작동될 때, 이벤트 호출을 받아와 인자에 있는 sayHello() 함수를 반환하여 내용을 작동시키게 한다.

      즉, 어떤 이벤트나 작업이 이루어질 때 호출되여 작동한다는 의미로서 답신의 의미인 ‘Callback’을 담아 ‘콜백 함수’라고 불리게 된다.

  • 커리 함수(Curry Function)

    • 고차 함수의 형태 중에선 ‘다른 함수를 반환하는 함수’가 있는데, 이를 커리 함수라고 부른다.
    • 커리(Curry)라는 이름은 해당 방식을 고안해 낸 논리학자인 하스켈 커리(Haskell Curry)의 이름을 따왔다고 전해진다.
      const curryExample = () => {
          return () => {
              console.log("커리 함수입니다!");
          }
      };
    
      curryExample;
      //  function curryExample() {
      //    return () => {
      //      console.log("커리 함수입니다!");
      //    }
      //  }
      // 변수의 내용인 함수 내용 자체를 가져온다.
    
      curryExample();
      //  () => {
      //    return console.log("커리 함수입니다!");
      //  }
      // 함수를 실행하면 curryExample 함수가 반환하는 함수의 내용을 가져온다.
    

    위와 같은 형태처럼 외부 함수를 실행하면 return값으로 내부의 함수를 반환하는 형태를 가진다. (이러한 형태를 클로저라고 부르는데 해당 내용은 추후에 다룰 것이다.)

    이런 경우, 반환 값으로 받고 있는 내부에 있는 함수를 실행하려면 어떻게 해야할까? 바로 아래와 같이 실행하면 된다.

      curryExample()();
      //  커리 함수입니다!
    

    앞서 변수 curryExample를 받아오면 함수의 내용을 반환한다. 함수 표현식으로 만들었으니, 이 변수를 함수로서 호출하면 그 함수의 실행 결과를 반환한다.

    내부 함수의 값을 반환하려면, 결국 함수 curryExample 함수의 실행 결과로서 반환된 함수를 실행해야 하는 건데 이 방식은 위의 내용처럼 이어서 호출하는 것으로 값을 받아올 수 있게 된다.

    위의 내용을 응용하면 이런 식으로도 표현할 수 있다.

      const curryExample = () => {
          return () => {
              console.log("커리 함수입니다!");
          }
      };
    
      const ex1 = curryExample();
    
      console.log(ex1);
      console.log(ex1());
    

  • JavaScript의 함수는 이런 커리 함수와 콜백 함수를 사용하지 않을 수도 있고, 단독적으로 쓰거나 함께 적용할 수도 있다.

    • 즉, 고차 함수 속에 콜백 함수와 커리 함수가 포함되어 있다.
  • 배열의 내장 고차 함수

    • JavaScript의 배열은 함수와 동일하게 객체로 취급한다.
    • 배열은 여러 개의 내장 메소드들을 prototype 객체에 저장해두고 있는데, 그 중에서는 콜백 함수와 커리 함수를 허용하는 메소드들이 있다.
      • 즉, 함수를 인자로 받거나 함수를 반환하는 배열의 내장 함수들이 존재한다.
      • 이를 배열의 내장 고차 함수라고 말한다.
    • 대표적인 배열의 내장 고차 함수, 즉 메소드는 아래와 같은 것들이 있다.

3️⃣ 컬렉션과 생성기

4️⃣ 순수함수, 부가효과, 상태변이

  • 순수 함수 (Pure Function)

    • 순수 함수는 어떤 함수에 동일한 인자를 주었을 때, 항상 같은 값을 반환하는 함수를 말한다.
    • 순수 함수가 되기 위해서는 아래의 조건들을 지켜줘야 한다.
      • 파라미터 혹은 매개변수를 최소 하나 이상 받아야한다.
      • 받은 파라미터에 의해서만 반환 결과가 결정되며, 그 결과는 값이거나 함수이다.
      • 외부의 요인 등으로 인해 발생하는 부수 효과가 없어야 한다.
    • 순수 함수를 코드로 간단하게 나타내면 아래와 같다.

        const value1 = 1;
        const value2 = 2;
      
        const plusFunc = (int1, int2) => {
        // 파라미터 혹은 매개 변수를 최소 하나 이상 받아야하며
            return int1 + int2
            // 받은 파라미터에 의해서만 반환 결과가 결정되며, 그 결과는 값이거나 함수이다.
            // 아울러, 동일한 인자를 주었어도 항상 같은 값을 반환해야 한다.
        }
      
        plusFunc(value1, value2);
        // 외부의 요인 등으로 인해 발생하는 부수 효과가 없어야 한다.
        // 외부 값을 가져와도 항상 같은 값을 발생하므로 순수 함수가 맞다.
      
    • 아래는 순수 함수가 아닌 사례이다. 왜 그런지 살펴보도록 하자.

        const idol = {
            "name" : "마츠리",
            "company" : "765프로덕션"
        };
      
        const idolFunc = () => {
            // 1. 반드시 파라미터를 받아야 하는데 파라미터가 없다.
            idol.name = "시호"
            // 2. 외부의 값을 가져와 함수 내에서 데이터를 바꾸므로 부수 효과가 발생한다.
            return idol;
        }
      
    • 순수 함수가 아닌 함수를 불순 함수(Impure Function)라고 부른다.

    • 순수 함수는 코드의 내용을 예측하기가 쉽다.
    • 또한 외부 요인에 따른 변화가 없으므로, 모듈화하기도 좋다는 큰 장점이 있다.
  • 부수 효과 (Side Effect)

    • 함수에서 외부의 요인에 변화를 주거나, 함수에 들어온 인자의 값이 변경되는 등 함수의 목적과 다른 효과나 부작용을 초래하는 것을 의미한다.
    • 위의 불순 함수 코드의 사례를 보면 idolFunc 함수 내에 객체 idolname keyvalue시호로 바꿈으로서 idolFunc 함수가 값을 반환 시, 본래의 객체 idol과는 다른 내용이 담긴 값을 출력하게 된다.
      • 이것은 함수 내부에서 객체 idol의 데이터를 변경했기 때문인데, 이것을 부수 효과가 일어났다, Side Effect가 발생했다고 말한다.
  • 상태 변이 (State Mutataion)

    • Mutation이라는 영단어는 ‘돌연변이’라는 의미를 가지고 있다.
    • 새로운 변수를 만들거나 기존 변수의 재할당없이 JavaScript의 객체나 배열을 변경하게 되면 기존의 객체나 배열의 형태를 잃고 상태가 변화하게 되는데, 이를 상태 변이라고 한다.

      아래의 코드에서 변수 testArray는 어떤 배열을 가지고 있는데, 배열의 내장 메소드인 sort()를 호출함으로서 testArray의 내용이 바뀌게 된다.

      const testArray = [4, 3, 1, 2];
    
      testArray.sort(); // (4) [1, 2, 3, 4]
    
    • 이러한 상태 변이를 유발하는 요소는 다음과 같은 단점을 가진다.
      • 상태 변이를 유발하는 요소는 기존 데이터를 변형시키므로 원본을 알기 어렵게 만든다.
      • 이로 인해 문제가 발생할 경우, 문제가 발생한 지점을 찾아내는 데에 시간이 걸린다.
    • 이런 배열과 객체의 원본을 해치지 않고 보존할 수 있는, 객체의 불변성을 유지하는 방법으로 ‘얕은 복사’와 ‘깊은 복사’가 존재한다.
      • 해당 내용은 추후 별도의 페이지에서 다루도록 하겠다.

5️⃣ 닫기/폐쇄 (Closure)

let l0 = 'l0';

function fn2() {
    let l2 = 'l2';
    console.log(l0, l1, l2);
}

function fn1() {
    let l1 = 'l1';
    console.log(l0, l1);
    fn2();
}

fn1();

위의 코드의 마지막줄처럼 함수 fn1을 실행시키면, 함수가 작동을 하다가 함수 fn2가 실행되는 도중에 에러가 출력되는 것을 볼 수 있다.

스코프를 확인해보면 위의 코드는 아래처럼 나타난다.

  • 함수 fn1의 스코프에서는 fn2가 포함되어 있지 않다.

    • 따라서, 함수 fn2 안의 변수 l2을 찾을 수 없다.

  • 함수 fn2는 함수 fn1 안에서 실행되도록 설정되어 있으나 함수 fn1과는 독립적인 스코프를 가진다.

    • 따라서, 함수 fn1 안의 변수 l1을 함수 fn2에서는 찾을 수 없다.

  • 즉, 함수 fn2가 함수 fn1 안에서 호출됐다 하더라도 fn1fn2 안에서 정의된 변수에 접근할 수 없다.

만약, 위의 코드를 바라는 대로 함수 fn2에 선언된 변수 l2를 함수 fn1 안에 접근하려면 어떻게 해야할까?

아래의 코드를 보자.

let l0 = 'l0';

function fn1() {
    function fn2() {
        let l2 = 'l2';
        console.log(l0, l1, l2);
    }

    let l1 = 'l1';
    console.log(l0, l1);
    fn2();
}

fn1();

아래의 코드를 통해 함수 fn1을 실행하면 처음 썼던 것과 다르게 console.log가 문제 없이 작동한 것을 볼 수 있을 것이다.

위의 이미지대로 코드를 실행하는 순서를 보면 함수 fn1 안에 fn2을 선언한 뒤, 첫 console.log를 부르고 난 뒤에 함수 fn2를 실행시켜지면서 실행 컨텍스트 순서에 따라 내부의 함수 fn2에 접근하는 걳을 볼 수 있다.

이때, fn1의 범위는 닫기/폐쇄/Closure로 지정되고 현재의 로컬 스코프가 fn2로 지정된 것이 보일 것이다.

이렇게 하면 fn2에서는 부모가 되는 외부 함수 fn1에 있는 변수에도 접근할 수 있으므로 우리가 원하는 대로 두 번째의 console.log도 작동하는 것을 볼 수 있다.

이처럼, 함수의 유효 범위는 함수가 어디서 호출됐냐가 아닌 함수가 어디서 정의됐냐에 따라 달라지게 되는데 이것이 이전에 배웠던 내용인 렉시컬 스코프(Lexical Scope)를 말한다.

그리고, 이런 식으로 외부 함수 안에 내부 함수를 정의하면 내부 함수는 외부 함수의 요소에 접근할 수 있는데 이것을 우리는 클로저(Closure), 폐쇄, 닫기라고 말한다.

좀 더 정확하게 말하자면, 내부 함수가 외부 요소와 함께 환경에 가둬지게 되는 것이다.

그러다보니, 내부 함수가 외부 함수의 변수를 가지고 있으면, 외부 함수의 실행이 끝나 사라지더라도 내부 함수가 외부 함수의 변수에 접근할 수 있게 된다.

이렇게 클로저의 개념을 안다면 아래와 같이 이용할 수 있다.

function plusFactory(init) {
    function plus(int){
        return init + int;
    }
    return plus;
}

let doPlus1 = plusFactory(1); // init = 1
console.log(doPlus1(1)); // 2
console.log(doPlus1(2)); // 3

let doPlus2 = plusFactory(2); // init = 2
console.log(doPlus2(1)); // 3
console.log(doPlus2(2)); // 4

이처럼 변수에 외부 함수를 선언해줌으로서 이를 통해 내부 함수 값에 접근해 내부 함수의 최종 값을 반환해줄 수 있다.

그리고 doPlus1doPlus2와 같이 함수가 만들어지는 시점에서 유효 범위가 정해지므로, 내부 함수에 접근하더라도 외부 함수였던 plusFactoryinit에도 접근할 수 있다.

📁 참고 자료

자바스크립트의 형변환은 두가지다

[프로그래밍 언어론] 형변환이란? 묵시적 형변환 과 명시적 형변환에 대하여

덕 타이핑과 구조적 타이핑

기본적인 블로그 : 네이버 블로그

구조적 타이핑 - 이펙티브 타입스크립트

JavaScript Collections

[JavaScript] 37. Set 과 Map

Set.prototype.forEach() - JavaScript | MDN

[ES6+] Map(), Set() 객체의 특징과 사용법

자바스크립트 맵 객체 (Javascript Map Object)

[Javascript] Map 사용법

WeakMap이 알고 싶다

맵과 셋

위크맵과 위크셋

가비지 컬렉션

프론트엔드 클린코드 - 객체 변이(mutation) 지양

[JavaScript]함수형 프로그래밍, 순수 함수

순수함수란 무엇인가요? | 2ssue's dev note