자바스크립트 완벽 가이드 8장 정리 ( Function )
포스트
취소

자바스크립트 완벽 가이드 8장 정리 ( Function )

해당 포스트는 자바스크립트 완벽 가이드라는 교재로 스터디를 하면서 8장을 정리한 포스트입니다.
주관적으로 해석한 내용이 들어가 있어서 잘못된 내용이 포함될 수 있습니다.
또한 교재의 모든 내용을 정리하지 않고 주관적인 판단에 의해 필요한 내용만 작성했습니다.

📦 함수 선언

함수를 선언하면 기본적으로 함수 이름의 식별자를 갖는 변수에 함수가 할당됩니다.

만약에 익명함수라면 변수 이름에 맞게 함수 이름이 할당됩니다.
익명 함수에 변수에 할당되지 않았다면 빈 문자열의 이름을 갖습니다.

1
2
3
4
const func = () => {};
console.log(func.name === "func"); // true

console.dir((() => {}).name === ""); // true

0️⃣ 함수 선언식

1
function func() {}

1️⃣ 함수 표현식

1
const func = function () {};

2️⃣ 화살표 함수

화살표 함수는 함수 표현식만 가능합니다.
( TDZ가 발생하기 때문에 이점이라고 생각합니다. ( var 제외 ) ( 즉, 선언하기 전에 사용 불가능 ) )

1
const func = () => {};

🛎️ 함수 호출

0️⃣ 조건부 호출 ( 옵셔널 체이닝 (?.) )

null/undefined인 경우 함수로 호출하지 않는 방법입니다.

1
2
3
4
5
let func = () => {};
func = null;

func(); // Error: func is not a function
func?.(); // undefined

1️⃣ Tagged Template Literal

1
2
3
4
5
6
const func =  (fruits, ...rest) => {
  console.log(fruits); // ["apple", "banana", "melon"]
  console.log(rest); // ["Hello, ", " | ", " , ", " ..."]
};

func`Hello, ${"apple"} | ${"banana"} | ${"melon"} ...`;
1
2
3
4
5
6
const styled =  (css, ...props) => {
  console.log(props);
  props[0](); // "!!!"
};

styled`Hello, ${() => {console.log("!!!")}}`;

🧬 함수의 프로퍼티

0️⃣ length

함수의 매개 변수의 개수에 해당하는 값을 가집니다.
( 단, 나머지 매개 변수는 제외합니다. )

1️⃣ name

함수의 이름이 정해졌다면 해당 이름 / 이름이 정해지지 않았다면 할당한 변수의 이름을 사용합니다.
( 디버깅이나 에러 메시지에 유용하게 사용된다고 합니다. )

2️⃣ prototype

화살표 함수나 객체에 ES6방식으로 메서드를 추가하는 경우를 제외한 함수들이 갖는 객체입니다.
생성자 함수로 사용하는 경우 prototype이 됩니다.

3️⃣ 함수의 이름

함수의 이름으로는 함수 바디에서만 호출이 가능합니다.

1
2
3
4
5
// (1)
let func = () => console.dir(func);

// (2)
func(); // 무엇이 출력될까요?

(2)에서 출력되는 값은 (1)의 익명의 화살표 함수입니다.
말은 익명이라고 했지만 사실 (1)에서 변수 이름을 기반으로 func라는 이름을 갖는 함수가 되었습니다.
그리고 함수 이름의 func()를 호출하는 방법은 함수 바디에서만 호출이 가능합니다.

🤿 함수 Dive

0️⃣ 매개변수 vs 인수 ( parameter vs argument )

매개변수는 함수에서 받는 변수를 의미하고, 인수는 함수를 호출할 때 전달해주는 값을 의미합니다.
( 매개변수는 함수 바디 안에서 로컬 변수로 동작합니다. )

1
2
3
4
5
// x, y는 매개변수
const func = (x, y) => x + y;

// 1, 2는 인수
func(1, 2);

1️⃣ default parameter

매개 변수는 함수가 호출될 때 평가됩니다.

1
2
3
4
5
// (1)
const func = (x = 10, y = 20) => x + y;

// (2)
func(100); // 120

위 예시의 경우 함수가 호출되는((2)) 시점에 매개 변수인 x, y가 평가된다는 의미입니다.
따라서 아래와 같이 신기한 구문도 가능합니다.

1
2
3
4
const func = (x = 10, y = x * 2) => x + y;

// (2)
func(100); // 300

2️⃣ 즉시 실행 함수 ( IIFE )

1
2
3
4
5
// (1)
(() => {}())

// (2)
(() => {})()

3️⃣ 함수 vs 메서드

함수와 메서드는 둘 다 함수라고 부를 수 있습니다.
하지만 메서드는 함수와는 조금 다른 동작을 하기 때문에 구분해서 부르는 것 같습니다.

일반적으로 메서드는 객체의 property로 사용되는 함수를 메서드라고 합니다.

1
2
3
4
5
6
7
8
9
10
// (1)
const champion = {
  name: "Aatrox",
  // (2)
  say() {
    console.log(this.name);
  }
};
const { say } = champion;
say();

객체의 property로 할당된 함수인 (2)를 메서드라고 부를 수 있을까요?
반은 맞고 반은 틀립니다.( 코어 자바스크립트 70p )

왜냐하면 메서드라고 불리는 시점은 함수가 호출되는 시점에 확신할 수 있기 때문입니다.

1
2
3
4
5
6
7
const { say } = champion;

// (3) 함수로서 호출
say(); // undefined

// (4) 메서드로서 호출
champion.say();

4️⃣ this

this에 대해서는 따로 정리한 포스트가 있으니 여기를 참고해주세요!

일반적으로 함수의 this는 변수의 스코프 규칙을 따르지 않습니다.
this값을 런타임에 결정됩니다.

따로 바인딩을 해주지 않은 경우 this는 전역 객체를 가리킵니다.
( 단, 화살표 함수와 바인딩을 바꾸는 경우는 제외입니다. )

5️⃣ 메서드 체이닝

메서드가 객체를 반환하고 그 객체에 이어서 메서드를 호출하는 것을 의미합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const champion = {
  name: "Aatrox",

  say() {
    console.log("Hello");
    return this;
  },

  move() {
    console.log("Moving");
    return this;
  },

  attack() {
    console.log("Attack");
    return this;
  },
};

champion.say().move().attack();
/**
 * 실행 결과
 * Hello
 * Moving
 * Attack
 */

대표적으로 Promise.prototype.then()을 이용한 체이닝이 있습니다.

1
2
3
4
5
Promise.resolve(1)
  .then(v => v + 1)
  .then(v => v + 1)
  .then(v => v + 1)
  .then(console.log); // 4

🃏 클로저

클로저에 대한 자세한 개념은 여기를 참고해주세요!

0️⃣ 어휘적 스코프 ( Lexical Scope )

함수는 내부적으로 [[Environment]]라는 숨김 프로퍼티를 갖고 그 값은 선언한 시점의 Lexical Environment를 참조합니다.
따라서 함수의 호출 시점이 아닌 함수의 선언 시점의 스코프(렉시컬 환경)을 기반으로 실행됩니다.

그리고 함수가 선언 시점의 스코프를 기억하고 있기 때문에 클로저라는 개념이 가능하게 됩니다.

1️⃣ 클로저와 getter/setter와 IIFE

getter/setter와 클로저를 이용한 예시입니다.

매개 변수인 _n에 직접적으로 접근할 수 없고 getter/setter로만 접근이 가능합니다.
또한 값에 접근하거나 수정할 때 특정한 조건을 부여할 수 있다는 이점이 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const counter = ((_n) => {
  return {
    get count() {
      return _n++;
    },
    set count(n) {
      if (_n > n) {
        throw new Error("기존 값보다 더 작은 값은 입력할 수 없습니다.");
      }
      _n = n;
    },
  };
})(1);

console.log(counter.count);
console.log(counter.count);

counter.count = 10;

console.log(counter.count);
console.log(counter.count);

2️⃣ var를 사용하면 안되는 이유

var의 스코프와 클로저가 결합되면 특이한 결과가 발생합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for (var i = 0; i < 4; i++) {
  // 클로저는 아님
  setTimeout(() => console.log(i), 1000);
}
console.log("i >> ", i);
/**
 * "i >> 4"
 * 4
 * 4
 * 4
 * 4
 */

for (let i = 0; i < 4; i++) {
  // 쿨로저
  setTimeout(() => console.log(i), 1000);
}
/**
 * 1
 * 2
 * 3
 * 4
 */

⚔️ 재귀 함수

재귀 함수란 자기 자신을 다시 호출하는 함수를 의미합니다.
잘 사용하면 매우 유용하지만, 조금만 잘못 사용하면 stack overflow를 만나기도 하고 더 비효율적으로 동작할 수 있기 때문에 유의해서 사용해야합니다.

유명하게 사용하는 예시가 팩토리얼과 피보나치수열에 사용됩니다.

0️⃣ 함수 프로퍼티 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const factorial = (n) => {
  const number = +n;

  if (typeof number !== "number") {
    console.error("숫자를 입력해주세요!");
    return;
  }

  // 캐싱된 값이 아니라면 캐싱
  if (!(number in factorial)) {
    factorial[number] = number * factorial(number - 1);
  }

  return factorial[number];
};

factorial[1] = 1;

console.log(factorial(5)); // 120
console.dir(factorial);

/**
 * 1: 1
 * 2: 2
 * 3: 6
 * 4: 24
 * 5: 120
 * name: "factorial"
 * ... 나머지 생략
 */

1️⃣ 메모이제이션과 재귀 함수 활용

자세한 예시는 메모이제이션를 참고해주세요!

메모이제이션이란 이미 계산했던 값은 캐싱해서 다시 사용하는 기법을 의미합니다.
함수의 실행이 복잡한 연산이 필요하거나 반복적으로 많은 수행이 필요하다면 유용하게 사용할 수 있습니다.

🪵 고차 함수

고차 함수란 함수를 인자로 받아서 함수를 반환하는 함수를 의미합니다.
단순히 함수를 받아서 함수를 리턴하는 것에 의미가 있는 것은 아니고, 유용하게 사용할만한 몇 가지 예시가 있습니다.

고차함수여러 헬퍼 함수들에 대한 게시글을 참고하시면 이해에 도움이 됩니다.

모두 함수를 인자로 받아서 특별한 역할을 하는 조금 다른 함수를 반환합니다.

자주 사용하는 것중에 ReactuseCallback()도 고차 함수라고 볼 수 있는 것 같습니다.
함수를 받아서 메모이제이션하는 함수를 리턴해주기 때문이죠.

📁 function 보다 화살표 함수를 사용해야 하는 이유

화살표 함수를 사용하면 좋은 이유는 목적이 명확하기 때문입니다.

아래에서는 몇 가지 구체적인 근거를 작성해보겠습니다.

0️⃣ TDZ 발생 ( var 제외 )

구체적인 설명은 호이스팅을 참고해주세요!

저는 개인적으로 모든 것은 선언하기 전에 사용하면 안된다고 생각합니다.
사람이 보통 코드를 읽는 방식은 위에서 아래로 좌측에서 우측으로 읽습니다.
그러면 함수를 호출할 때도 선언을 하고 사용하는 것이 일반적으로 이해하기 좋은 흐름입니다.

하지만 함수 선언식을 이용하면 코드 평가 단계에서 함수를 먼저 할당합니다.
따라서 함수가 선언되기 전에 함수를 사용할 수 있게 됩니다.

물론 선언하기전에 사용한다고 특별히 문제가 있지는 않지만 코드를 읽는 흐름에 방해가 될 수 있다고 생각하는 편이라서 함수 선언식은 특수한 상황이 아니면 사용하지 않는 편입니다.

화살표 함수를 사용하면 함수 선언식으로 생성하는 것이 불가능하기 때문에 화살표 함수를 선호합니다.

1️⃣ arguments

MDN - arguments를 보면 나머지 매개변수 방법을 권장합니다.
arguments는 유사 배열 객체라서 사용하기도 불편하고 사용하지 않아도 함수에서 자체적으로 생성이 됩니다.

따라서 arguments 자체가 생성되지 않는 화살표 함수를 사용하는 것이 좋다고 생각합니다.

2️⃣ 생성자 함수

function을 이용해서 만든 함수는 생성자 함수로 사용할 수 있는 권한이 있습니다.
따라서 prototype 프로퍼티가 내부적으로 생성됩니다.

하지만 굳이 함수를 이용해서 객체를 만들어내야할까요?
의도가 명확한 class가 있는데 굳이 생성자 함수로 사용할 필요는 없다고 생각합니다.
그리고 class는 객체로 만들어내기 위한 특별한 처리를 해줍니다.( private, 메서드 enumable: false 등 )

따라서 class대신 생성자 함수를 사용할만한 이유가 없다고 생각합니다.

그리고 개발자 누구나 실수를 할 수 있기 때문에 의도는 생성자 함수로 만들었다고 하더라도 함수의 호출로 사용할 수 있습니다.
일반적인 함수로 사용한다고 하더라도 오류를 발생하지는 않습니다.
함수에서도 this를 사용할 수 있고, return으로 undefined를 줄테니까요.
그리고 나중에 객체를 사용하는 부분에서 오류가 발생하게 될 것 입니다.

아니 누가 생성자 함수도 구별을 못하고 일반 함수로 사용하겠냐고 생각할 수 있지만, 코드가 복잡해지다보면 그러고 누가 된 본인을 발견할 수 있습니다.

3️⃣ this

다른 언어를 공부하다가 JavaScript로 오면 정말 이상한(물론 다른 것도 많지만) 것이 this입니다.

객체도 아닌데 this를 사용하는 것에 이상함을 느낍니다.
이제 this를 사용할 수 있다는 것을 인지하고 나서는 일반적으로 예측한 this와 다른 값이 자꾸 나오게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const champion = {
  name: "Aatrox",
  say() {
    console.log("Hello, " + this.name);

    const innerFunc = function () {
      // (1)
      console.log("innerFunc >> Hello, " + this.name);
    };

    innerFunc();
  },
};

champion.say();

/**
 * Hello, Aatrox
 * innerFunc >> Hello, undefined
 */

일반적으로 예측하면 (1)에서 당연히 "innerFunc >> Hello, Aatrox"라는 결과가 나올 거라고 생각합니다.
하지만 실행해보면 thisglobalThis를 가리켜서 undefined가 나오게 됩니다.

이런 경우 화살표 함수를 사용하면 문제가 바로 해결됩니다.

그리고 메서드로서 호출되는 경우가 아니라면 this를 사용할 이유가 없기 때문에 그냥 화살표 함수를 사용하고 메서드 내부에서 호출되는 경우도 화살표 함수를 사용하면 상위 스코프의 this를 가지니까 더 합리적인 동작이라고 생각합니다.
( 생성자 함수 앞에서 사용하지 않는 것이 좋다고 했으니 제외 )

4️⃣ 바인딩 되지 않음

화살표 함수는 call(), apply(), bind()를 이용한 바인딩이 되지 않습니다.
따라서 이 함수의 this에 대한 모호함이 사라집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const func = function () {
  console.log(this.name);
};
// (1)
func(); // undefined
// (2)
func.call({ name: "Aatrox" }); // "Aatrox"

// =============================================== //

const arrowFunc = () => {
  console.log(this.name);
};
// (3)
arrowFunc(); // undefined
// (4)
arrowFunc.call({ name: "Aatrox" }); // undefined

function을 사용한 함수는 호출되는 위치와 this 바인딩 메서드까지 고려해서 판단해야합니다.
하지만 화살표 함수는 호출되는 위치만 고려하면 됩니다.( this바인딩 불가능 )
그리고 메서드에서 호출한다면 일반적으로 생각하는 this가 나오기 때문에 따로 수정해줄 필요도 없습니다.

결론: TDZ발생 , 더 합리적인 this, 더 가벼움, this바인딩 안됨, function처럼 길게 적을 필요 없음 등의 이유로 화살표 함수를 사용하는 게 웬만하면 좋다고 생각합니다.

❓ 함수형 프로그래밍

TODO: 이해하기 어렵고 지금까지 이해한 내용이 맞는지 혼란스러워서 아직 정리를 못했습니다…
( 추후에 쏙쏙 들어오는 함수형 코딩이라는 책을 읽어보고 정리할 예정 )

  • 키워드
    1. “어떻게” 보다는 “무엇을”
    2. 순수 함수
    3. 사이드 이펙트
    4. 멱등성
    5. 불변성
    6. 반복문 보다는 배열 메서드
    7. 각각의 함수가 서로의 존재를 몰라야 하고 각자의 역할만 수행

💡 소소한 내용

0️⃣ 매개 변수와 구조 분해 할당

매개 변수에는 모든 구조 분해 할당이 가능합니다.

1
2
3
4
5
6
7
8
9
const arr = [[1, 2], [3, 4]];

// (1) 즉시 실행 함수 ( 배열 구조 분해 할당 )
(([[x, y], [a, b]]) => console.log(x, y, a, b))(arr);

const champion = { stat: { speed: 345 } };

// (2) 즉시 실행 함수 ( 객체 구조 분해 할당 )
(({ stat: { speed } }) => console.log(speed))(champion);

개인적으로 배열은 1deps, 객체는 2dep까지만 구조 분해 할당을 하는 편입니다.
( 너무 깊게 구조 분해 할당을 하면 가독성이 더 안 좋게 느껴집니다. )

1️⃣ 매개 변수 타입 생각하기

JavaScript에서는 매개 변수의 타입에 대해 체크해주지 않기 때문에 항상 매개 변수의 타입에 대한 예외 처리를 해주는 것이 좋습니다.
타입이 다르다고 바로 에러를 발생하기 보다는 이후에 에러가 나는 경우가 많아서 디버깅하기 힘들어집니다.

1
2
3
4
const sum = (x, y) => x + y;

// (3)
const number = sum(1 + "2");

(3)에서 3를 기대하겠지만, "12"가 나옵니다.
더 문제는 (3)에서가 아닌 다른 곳에서 에러가 발생한다는 점입니다.

따라서 매개 변수를 사용하는 함수에서 예외 처리를 하는 것이 좋습니다.

1
2
3
4
5
6
7
const sum = (x, y) => {
  if(typeof x !== "number" || typeof y !== "number") {
    throw new Error("숫자만 입력해주세요!")
  }

  return x + y;
}

위처럼 작성하면 문제의 원인을 바로 파악할 수 있습니다.
하지만 위처럼 모든 함수에 타입을 체크하는 구문을 넣는 것은 많이 고통스러운 작업입니다.

이런 문제때문에 간단하게는 JSDoc을 사용하거나 더 미래를 생각한다면 TypeScript를 사용하는 것이 좋습니다.

2️⃣ 재귀 함수의 Stack Overflow

재귀 함수 호출 시 적은 수의 호출은 문제가 없습니다.
하지만 수만번 이상(정확한 수치는 아님)을 호출한다면 콜 스택에 가득 차기 때문에 Maximun call-stack size exceeded 오류 발생하게 됩니다.

📮 레퍼런스

  1. « 자바스크립트 완벽 가이드 8장 » ( 데이비드 플래너건 지음, 한성용 옮김, 인사이트, 2022 )
  2. 1-blue - 코어 자바스크립트 3장 this
  3. 1-blue - [[Environment]]
  4. 1-blue - 클로저
  5. 1-blue - 메모이제이션
  6. 1-blue - 호이스팅
  7. 1-blue - 고차함수
  8. 1-blue - 여러 헬퍼 함수들
  9. YouTube - 정재남 - “function”은 아예 쓰지 마세요
  10. 1-blue - 함수의 this를 바꾸는 방법
  11. 1-blue - JSDoc
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

정렬 알고리즘

React의 동작에 대한 고찰