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>
- 실행 결과는 아래와 같다.
- 따라서 한 가지로 통일해서 사용하는 것이 오류의 위험을 줄일 수 있고, 통일한다면 익명 함수로 통일해서 사용하는 것이 안전을 위해서 더 편한 선택이다.
- 저자 유튜브 강의 보기
- 30강 - 콜백함수 : https://www.youtube.com/watch?v=Q5E5sENGPWI
- 31강 - 타이머 함수, 즉시 호출 함수(IIFE), 엄격 모드 : https://www.youtube.com/watch?v=tjGg1G_DyKQ
- 32강 - 선언적 함수와 익명 함수의 차이, 확인 문제 : https://www.youtube.com/watch?v=ANqKWpNjC70
해당 글은 [혼자 공부하는 자바스크립트] 책을 토대로 공부한 내용을 기록하기 위하여 작성됨.