IT Study./혼공JS

5장. 함수 - 2

impnem 2022. 6. 12. 20:21

 

 

 

혼공JS 5장 - 2

 

 

 


05-2 함수 고급

  • 다른 프로그래밍 언어는 함수를 지정된 위치에서 만들어야 하지만, 자바스크립트는 '함수도 하나의 자료'라는 개념을 가지고 있어서 중간에 만들 수 있다.
  • 이는 2010년 전후에 등장한 비동기 프로그래밍을 이끌었으며, 자바스크립트의 익명 함수는 문법적 가치를 크게 인정받아 다른 프로그래밍 언어로 전파되었다.
  • 람다 또는 익명 함수라는 이름으로 기본 문법에 포함되었다.

 

 

<콜백 함수>

  • 자바스크립트는 함수도 하나의 자료형이므로 매개변수로 전달할 수 있다. 이렇게 매개변수로 전달하는 함수를 콜백(callback) 함수라고 한다.
  • 예제를 총하여 콜백 함수가 무엇인지 알아보자.
  <script>
    // 함수를 선언합니다.
    function callThreeTimes (callback) {
      for (let i = 0; i < 3; i++) {
        callback(i) // callback이라는 매개변수는 함수이므로 호출할 수 있다.
      }
    }

    function print (i) {
      console.log(`${i}번째 함수 호출`)
    }

    // 함수를 호출합니다.
    callThreeTimes(print)
  </script>
  • 결과는 아래와 같다.

실행 결과

  • 위 코드는 callThreeTimes() 함수는 함수를 매개변수로 받아 해당 함수를 3번 호출한다. 매개변수로 전달했던 print() 함수가 print(0), print(1), print(2)로 차례차례 호출되어 위와 같은 실행 결과가 나타나게 된다.

 

  • 위 예제의 선언적 함수를 익명 함수로 변경한다면 아래와 같이 코드를 구성할 수 있다.
  <script>
    // 함수를 선언합니다.
    function callThreeTimes (callback) {
      for (let i = 0; i < 3; i++) {
        callback(i) // callback이라는 매개변수는 함수이므로 호출할 수 있다.
      }
    }

    // 함수를 호출합니다.
    callThreeTimes(function (i) { // 익명 함수 사용하기
      console.log(`${i}번째 함수 호출`)
    })
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

  • 자바스크립트가 기본적으로 제공하는 함수 중에도 콜백 함수를 활용하는 함수가 많다.
  • 어떠한 형태로 콜백 함수를 활용하는지 알아보자.

 

콜백 함수를 활용하는 함수: forEach()

  • 콜백 함수를 활용하는 가장 기본적인 함수는 forEach() 메소드이다.
  • forEach() 메소드는 배열이 갖고 있는 함수(메소드)로써 단순하게 배열 내부의 요소를 사용하여 콜백 함수를 호출해준다.
  • 배열이 갖고 있는 메소드 중에서 콜백 함수를 활용하는 메소드는 아래와 같은 형태의 콜백 함수를 사용한다.
function (value, index, array) { } // array는 주로 생략함.
  • 예제는 아래와 같다.
  <script>
    const numbers = [273, 52, 103, 32, 57]

    numbers.forEach(function (value, index, array) {
      console.log(`${index}번째 요소 : ${value}`)
    })
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

 

콜백 함수를 활용하는 함수: map()

  • map() 메소드도 배열이 갖고 있는 함수이다.
  • map() 메소드는 콜백 함수에서 리턴한 값들을 기반으로 새로운 배열을 만드는 함수이다.
  <script>
    // 배열을 선언합니다.
    let numbers = [273, 52, 103, 32, 57]

    // 배열의 모든 값을 제곱합니다.
    numbers = numbers.map(function (value, index, array) { // 매개변수로 value, index, array를 갖는 콜백 함수를 사용한다.
      return value * value
    })

    // 출력합니다.
    numbers.forEach(console.log) // 매개변수로 console.log 메소드 자체를 넘겼다.
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

 

  • 원하는 매개변수만 받기
    • 앞에서 forEach(), map() 함수의 콜백 함수에 매개변수를 value, index, array로 3개를 모두 입력했지만, 일반적으로 value만 또는 value와 index만 사용하는 경우가 많다.
    • 콜백 함수의 매개변수는 모두 입력할 필요가 없고, 사용하고자 하는 위치의 것만 순서에 맞춰 입력하면 된다.
  <script>
    // 배열을 선언합니다.
    let numbers = [273, 52, 103, 32, 57]

    // 배열의 모든 값을 제곱합니다.
    numbers = numbers.map(function (value) { // 함수 내부에서 value만 사용하므로 value만 매개변수로 넣는다.
      return value * value
    })

    // 출력합니다.
    numbers.forEach(console.log)
  </script>

 

콜백 함수를 활용하는 함수: filter()

  • filter() 메소드도 배열이 갖고 있는 함수이다.
  • filter() 메소드는 콜백 함수에서 리턴하는 값이 true인 것들만 모아서 새로운 배열을 만드는 함수이다.
  <script>
    const numbers = [0, 1, 2, 3, 4, 5]
    const evenNumbers = numbers.filter(function (value) {
      return value % 2 === 0
    })

    console.log(`원래 배열: ${numbers}`)
    console.log(`짝수만 추출: ${evenNumbers}`)
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

  • 위 코드에서 콜백 함수의 value만 활용하므로 value만 매개변수로 넣었다.

 


 

<화살표 함수>

  • map(), filter() 함수처럼 단순한 형태의 콜백 ㅎ마수를 쉽게 입력하고자 화살표(arrow) 함수라는 함수 생성 방법이 있다.
  • 화살표 함수는 function 키워드 대신 화살표(=>)를 사용하며, 아래와 같은 형태로 생성하는 간단한 함수이다.
(매개변수) => {

}
// or
(매개변수) => 리턴값
  • 내부에서 this 키워드가 지칭하는 대상이 다르다는 등의 미세한 차이가 있다.
  • 예를 들어 이전의 map() 함수의 콜백 함수로 화살표 함수를 넣는다면 아래와 같이 코드를 작성한다.

콘솔에서 실행한 결과

  • 다음 예제에서는 filter(), map(), forEach() 메소드를 연속적으로 사용하였다.
  <script>
    // 배열을 선언합니다.
    let numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

    // 배열의 메소드를 연속적으로 사용합니다.
    numbers
      .filter((value) => value % 2 === 0)
      .map((value) => value * value)
      .forEach((value) => { // 메소드 체이닝하였다.
        console.log(value)
      })
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

  • filter() 메소드는 배열을 리턴하므로 map() 메소드를 적용할 수 있고, map() 메소드도 배열을 리턴하므로 forEach() 메소드를 적용할 수 있다.
  • 이렇게 어떤 메소드가 리턴하는 값을 기반으로 해서 함수를 줄줄이 사용하는 것을 메소드 체이닝(method chaining)이라고 부른다.

 


 

<타이머 함수>

  • 자바스크립트에는 특정 시간마다 또는 특정 시간 이후에 콜백 함수를 호출할 수 있는 타이머(timer) 함수들이 있다.
  • 이 함수를 사용하면 시간과 관련된 처리를 할 수 있다.
함수 이름 설명
setTimeout(함수, 시간) 특정 시간 후에 함수를 한 번 호출합니다.
setInterval(함수, 시간) 특정 시간마다 함수를 호출합니다.
  • 어떻게 사용하는지 아래의 예제로 살펴보자.
  <script>
    setTimeout(() => {
      console.log('1초 후에 실행됩니다.')
    }, 1 * 1000)

    let count = 0
    setInterval(() => {
      console.log(`1초마다 실행됩니다(${count}번째)`)
      count++
    }, 1 * 1000)
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

  • setTimeout() 함수와 setInterval() 함수를 사용해서 특정 시간 후에 코드를 호출한다.
  • 코드를 실행하면 1초 후에 setTimeout() 함수의 콜백 함수가 실행되고, 1초마다 setInterval() 함수의 콜백 함수가 실행되는 것을 볼 수 있다.

 

  • 타이머를 종료하고 싶은 때는 clearTimeout() 함수와 clearInterval() 함수를 사용한다.
함수 이름 설명
clearTimeout(타이머_ID) setTimeout() 함수로 설정한 타이머를 제거합니다.
clearInterval(타이머_ID) setInterval() 함수로 설정한 타이머를 제거합니다.
  • 이 함수들의 매개변수에는 타이머 ID라는 것을 넣는데, 타이머 ID는 setTimeout() 함수와 setInterval() 함수를 호출할 때 리턴값으로 나오는 숫자이다.
  • 아래의 예제를 살펴보자.
  <script>
    let id
    let count = 0
    id = setInterval(() => {
      console.log(`1초마다 실행됩니다(${count}번째)`)
      count++
    }, 1 * 1000)

    setTimeout(() => {
      console.log('타이머를 종료합니다.')
      clearInterval(id)
    }, 5 * 1000)
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

 

 

 


 

 

 

①즉시 호출 함수

  • 함수를 만들고 즉시 호출하는 함수를 즉시 호출 함수(IIFE, Immediately Invoked Function Expression)라고 한다.
  • 여러 웹 사이트의 자바스크립트 코드를 보면 아래와 같이 익명 함수를 생성하고 곧바로 즉시 호출하는 패턴을 많이 볼 수 있다. 이러한 코드는 왜 사용하는 것일까?
(function () { })()
  • 예를 들어 카카오 페이지 소스 코드를 보면 script 태그를 여러 번 사용한 것을 볼 수 있다.
  • 이렇게 코드가 여러 곳에서 사용되면 변수 이름이 충돌할 가능성이 높다.
  • 아래의 예제를 살펴보자.
  <!-- 다른 곳에서 가져온 자바스크립트 코드 -->
  <script>
    let pi = 3.14
    console.log(`파이 값은 ${pi}입니다.`)
  </script>

  <!-- 내가 만든 자바스크립트 코드 -->
  <script>
    let pi = 3.141592
    console.log(`파이 값은 ${pi}입니다.`)
  </script>
  • 실행 결과는 아래와 같다.

  • 식별자가 이미 사용되고 있다는 오류를 발생하면서 <!-- 내가 만든 자바스크립트 코드-->라는 부분이 실행되지 않는다.
  • 변수가 존재하는 범위를 스코프(scope)라고 부르는데, 이 스코프는 같은 단계에 있을 경우 무조건 충돌이 일어난다.
  • 자바스크립트에서 이러한 스코프 단계를 변경하는 방법은 중괄호를 사용해서 블록을 만들거나, 함수를 생성해서 블록을 만드는 방법이다.
  <!-- 다른 곳에서 가져온 자바스크립트 코드 -->
  <script>
    let pi = 3.14
    console.log(`파이 값은 ${pi}입니다.`)

    // 블록을 사용한 스코프 생성
    {
      let pi = 3.141592 // 다른 블록에 속하므로 변수 이름 충돌이 발생하지 않는다.
      console.log(`파이 값은 ${pi}입니다.`)
    }
    console.log(`파이 값은 ${pi}입니다.`)

    // 함수 블록을 사용한 스코프 생성
    function sample () {
      let pi = 3.141592 // 다른 블록에 속하므로 변수 이름 충돌이 발생하지 않는다.
      console.log(`파이 값은 ${pi}입니다.`)
    }
    sample()
    console.log(`파이 값은 ${pi}입니다.`)
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

  • 코드를 실행하면 블록 내부에서는 변수 pi를 출력했을 때 3.141592가 나오고, 블록 외부에서는 3.14가 출력되는 것을 확인할 수 있다. 이름 충돌이 발생하지 않는다.
  • 이렇게 블록 내부에서 같은 이름으로 변수를 선언하면 변수가 외부 변수와 충돌하지 않고 외부 변수를 가린다.
  • 내부 블록에서는 내부 블록에서 선언한 변수만 볼 수 있다.
  • 이렇게 블록이 다른 경우 내부 변수가 외부 변수를 가리는 현상을 섀도잉(shadowing)이라고 부른다.

 


 

②즉시 호출 함수 문제 해결하기

  • 블록을 사용하는 방법과 함수 블록을 사용해 변수 충돌을 막는 방법 모두 최신 자바스크립트를 지원하는 웹 브라우저에서는 사용할 수 있다.
  • 하지만 구 버전의 자바스크립트에서 변수를 선언할 때 사용하던 var 키워드는 함수 블록을 사용하는 경우에만 변수 충돌을 막을 수 있다.
  • 지금도 구 버전의 자바스크립트를 지원하는 웹 브라우저(인터넷 익스플로러)에 대응해야 하는 경우가 많고, Babel 등 최신 버전의 자바스크립트를 구 버전의 자바스크립트로 변경해주는 트랜스파일러도 단순한 블록으로 함수 충돌을 막는 코드는 제대로 변환해주지 못한다.
  • 그래서 많은 개발자들이 함수 블록을 사용해 이런 문제를 해결한다.
  • 충돌 문제를 해결하기 위해 사용하는 것이므로 함수를 만들자마자 즉시 호출할 수 있도록 다음과 같이 작성한다.
  <!-- 다른 곳에서 가져온 자바스크립트 코드 -->
  <script>
    let pi = 3.14
    console.log(`파이 값은 ${pi}입니다.`)
  </script>
  <!-- 내가 만든 자바스크립트 코드 -->
  <script>
    (function () {
      let pi = 3.141592
      console.log(`파이 값은 ${pi}입니다.`)
    })()
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

 


 

③엄격 모드

  • 여러 자바스크립트 코드를 보면 블록의 가장 위쪽에 'use strict'라는 문자열이 등장하는 것을 볼 수 있다.
  • 이는 엄격 모드(strict mode)라고 부르는 기능으로 자바스크립트는 이러한 문자열을 읽어 들인 순간부터 코드를 엄격하게 검사한다.
  <script>
    'use strict'
    문장
    문장
  </script>
  • 아래의 예제를 살펴보자.
  <script>
    data = 10
    console.log(data)
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

  • data라는 변수를 let 키워드 등으로 선언하지 않고 곧바로 사용한다. 일반적인 자바스크립트 코드에서는 문제없이 실행된다.
  • 하지만 엄격 모드에서는 이러한 코드를 사용할 수 없다.

 

  • 다음으로 엄격 모드를 사용한 예제를 살펴보자.
  <script>
    'use strict'
    data = 10
    console.log(data)
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

  • 변수를 let 키워드 등으로 선언하지 않았는 데 사용했다고 곧바로 오류가 발생한다.
  • 자바스크립트는 오류를 어느 정도 무시하고 넘어가는 것들이 있다. 그래서 편하게 코딩할 수 있지만 실수로 이어지기도 한다.
  • 일반적으로 엄격 모드를 사용하는 것이 좋다.
  • 엄격 모드에서 발생하는 오류에 대해서는 모질라 엄격 모드 문서()를 참고하라.
 

Strict mode - JavaScript | MDN

엄격 모드는 평범한 JavaScript 시멘틱스에 몇가지 변경이 일어나게 합니다.

developer.mozilla.org

  • 즉시 호출 함수를 만들고, 이 블록의 가장 위쪽에서 엄격 모드를 적용하는 경우가 많다.
  • 이렇게 하면 해당 블록 내부에만 엄격 모드가 적용된다.
  • 엄격 모드의 일반적인 사용 패턴은 아래와 같다.
  <script>
    (function () {
      'use strict'
      문장
      문장
    })()
  </script>
  • 엄격 모드를 모르면 'use strict'라는 코드를 이해할 수 없다.
  • 따라서 여러 자바스크립트 코드를 읽을 수 있도록 꼭 기억하자.

 


 

④익명 함수와 선언적 함수의 차이

  • while 반복문과 for 반복문은 2가지 모두 많이 사용되지만, 사용하는 상황이 조금씩 다르다.
  • while 반복문은 조건을 중심으로 반복할 때, for 반복문은 횟수를 중심으로 또는 배열 등을 중심으로 반복할 때 사용한다.

 

  • 그런데 익명 함수 선언적 함수는 사용하는 상황이 비슷하다.
  • 기본적으로 혼자 개발할 때는 자신이 편하다고 생각하는 것을 사용하고, 다른 사람들과 함께 개발할 때는 모두가 편하다고 생각하는 것을 사용하면 된다.
  • 다만 최근에는 많은 개발자가 안전 등의 이유로 익명 함수를 선호하는 편이다.
  • 왜 익명 함수가 더 안전하다고 하는지, 어떤 차이가 있기에 그런 것인지 간단하게 알아보자.

 

익명 함수의 사용

  • 익명 함수순차적인 코드 실행에서 코드가 해당 줄을 읽을 때 생성된다.
  <script>
    // 변수를 선언합니다.
    let 익명함수

    // 익명 함수를 2번 생성합니다.
    익명함수 = function () {
      console.log('1번째 익명 함수입니다.')
    }
    익명함수 = function () {
      console.log('2번째 익명 함수입니다.')
    }

    // 익명 함수를 호출합니다.
    익명함수()
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

  • 위 코드는 위에서 아래로 차례대로 코드가 실행되면서 익명 함수라는 변수에 '2번째 익명 함수입니다.'를 호출하는 함수가 할당된다.

 

선언적 함수의 사용

  • 선언적 함수순차적인 코드 실행이 일어나기 전에 생성된다.
  • 따라서 선언적 함수는 같은 블록이라면 어디에서 함수를 호출해도 상관없다.
  <script>
    // 선언적 함수를 호출합니다.
    선언적함수() // 선언적 함수를 생성하는 코드 앞에 입력합니다.

    // 선언적 함수를 2번 생성합니다.
    function 선언적함수 () {
      console.log('1번째 선언적 함수입니다.')
    }
    function 선언적함수 () {
      console.log('2번째 선언적 함수입니다.')
    }
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

  • 위 코드와 같이 선언적 함수를 생성하기 전에 함수를 호출해도 함수가 이미 생성된 상태이므로 아무 문제없이 실행된다.
  • 또한 선언적 함수도 입력한 순서대로 생성되고 같은 이름이라면 덮어쓰므로 코드를 실행했을 때 '2번째 선언적 함수입니다.'를 출력하는 모습을 볼 수 있다.

 

선언적 함수와 익명 함수의 조합

  • 2가지 상황이 조합된 경우에는 선언적 함수가 먼저 생성되고, 이후에 순차적인 코드 진행을 시작하면서 익명 함수를 생성한다.
  <script>
    // 함수를 호출합니다.
    함수()

    // 익명 함수를 생성합니다.
    함수 = function () {
      console.log('익명 함수입니다.')
    }

    // 선언적 함수를 생성하고 할당합니다.
    function 함수 () {
      console.log('선언적 함수입니다.')
    }

    // 함수를 호출합니다.
    함수()
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

  • 위의 코드는 코드의 순서와 관계없이 나중에 호출한 함수()가 '익명 함수입니다.'라는 글자를 출력한다.
  • 익명 함수가 선언되기 전에 호출한 함수()는 '선언적 함수입니다.'라는 글자를 출력하는 것을 볼 수 있다.
  • 익명 함수는 우리가 코드를 읽을 때와 같은 순서로 함수가 선언되지만, 선언적 함수는 우리가 코드를 읽는 순서와 다른 순서로 함수가 선언된다.
  • 함수를 같은 이름으로 덮어쓰는 것은 굉장히 위험한 일이므로 안전하게 사용할 수 있는 익명 함수를 더 선호하는 것이다.

 

블록이 다른 경우에 선언적 함수의 사용

  • 선언적 함수는 어떤 코드 블록(script 태그 또는 함수 등으로 구분되는 공간)을 읽어 들일 때 먼저 생성된다.
  <script>
    선언적함수()

    function 선언적함수 () {
      console.log('1번째 선언적 함수입니다.')
    }
  </script>
  <script>
    function 선언적함수 () {
      console.log('2번째 선언적 함수입니다.')
    }
  </script>
  <script>
    선언적함수()
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

  • 이처럼 블록이 나누어진 경우에는 선언적 함수의 실행 흐름을 예측하는 것이 훨씬 힘들다.
  • 다른 프로그래밍 언어들은 일반적으로 선언적 함수 형태로 함수를 많이 사용하지만, 자바스크립트는 이처럼 블록이 예상하지 못하게 나뉘는 문제 등이 발생할 수 있어 안전을 위해 익명 함수를 더 많이 사용하는 편이다.

 

  • 과거 자바스크립트는 var이라는 키워드를 사용해서 변수를 선언했다.
  • var 키워드는 이전 코드처럼 덮어쓰는 문제가 발생한다.
  • 하지만 현대의 자바스크립트는 let 키워드와 const 키워드를 사용해서 변수와 상수를 선언한다.
  • 그리고 이러한 키워드들은 위험을 원천적으로 차단하기 위해서 오류를 발생시킨다.
  <script>
    // 익명 함수를 생성합니다.
    let 함수 = function () {
      console.log('익명 함수입니다.')
    }

    // 선언적 함수를 생성하고 할당합니다.
    function 함수 () {
      console.log('선언적 함수입니다.')
    }

    // 함수를 호출합니다.
    함수()
  </script>
  • 실행 결과는 아래와 같다.

실행 결과

  • 따라서 한 가지로 통일해서 사용하는 것이 오류의 위험을 줄일 수 있고, 통일한다면 익명 함수로 통일해서 사용하는 것이 안전을 위해서 더 편한 선택이다.

 

 

 

 

 

 


해당 글은 [혼자 공부하는 자바스크립트] 책을 토대로 공부한 내용을 기록하기 위하여 작성됨.