프론트 공부/JavaScript와 모던 JS

JavaScript에서 함수 작성하기 (feat. 모던 자바스크립트)

홍구리당당 2023. 10. 19. 14:58

0. 오늘의 배울 것.

자바스크립트에서 함수를 정의내리고 사용하는 방법은 어마무시하게 다양하다!!

js 자체에서 함수를 활용하는 방법도 다양하지만, 모던 자바스크립트로 새로운 함수 정의법이 추가되면서 정말 다양해졌다. 이를 잘 알고 적재적소에 사용하는 것이 중요하다!!

가끔 알고리즘 문제를 풀면서 다른 사람들의 풀이를 볼 때, 거기서 함수 정의한 방식 자체가 생소해서 코드를 이해 못했던 적이 있다. 이번 기회에 함수 정의법을 잘 공부해두고, 남의 코드를 해석하는 능력도 길러보자.



1. 함수 정의하는 방법.

(1) 함수 선언식: function 키워드를 통해 일반 함수 정의하기.

가장 기본적으로 함수를 만드는 방식은, 함수 선언식을 활용하는 것이다. 아래 코드처럼 function 키워드 다음 함수 코드를 작성하는 방식이다.

function addNum(a, b) {
  return a + b;
}
console.log(addNum(3,4));

(2) 함수 표현식: 함수를 값처럼 활용하기.

우리는 어떤 값을 활용할 때 그 값을 함수의 인자로 주기도 하고, 변수에 할당해주기도 한다. const variation = "value"; 이렇게 value 라는 값을 variation 변수에 넣는 식으로 활용하는데, 함수 표현식은 이렇게 값처럼 활용되는 방식이다. 함수 표현식을 function expression이라 한다!!

const addNum = function(a, b){
      return a+b;
};
console.log(addNum(3,4));

변수에 함수를 할당하는 것뿐만 아니라, 인자로 값을 넘겨주듯 함수를 인자로 넘겨주는 것도 일종의 함수 표현식이다. 예를 들어 addEventListener 메소드에서 두 번째 인자로 함수를 정의하는 것이 바로 함수 표현식!

myBtn.addEventListener("click", function () {
  console.log("이것도 함수 표현식이다.");
});

함수 선언식 vs 함수 표현식

함수 표현식으로 함수를 선언하면 const 키워드 특성 상 hoisting 문제가 일어나지 않는다. (물론 var 키워드를 쓰면 var 특성의 hoisting이 발생하긴 한다.) function으로 함수 선언하면 hoisting 발생!

또한 const 특성 상 함수 표현식을 코드 블록 내부에 쓰면 지역 함수처럼 쓸 수 있고 바깥에서는 못 쓴다.

함수 선언 방식은 자유롭게 선언 가능한 게 장점이지만 함수 표현식 방식은 코드에 일관성을 가질 수 있단 게 장점이다.



함수 표현식을 쓸 때 이름을 붙이는 케이스: 기명 함수 표현식, Named Function Expression

함수 표현식으로 함수를 만들 때 변수 이름 말고 함수에 이름을 따로 줄 수도 있는데, 이것을 이를 기명 함수 표현식이라 한다.

함수에는 각자 name 이라는 프로퍼티가 있는데 이름을 따로 정해주지 않으면 보통 변수 이름을 함수의 name 프로퍼티 값으로 가진다.

// 이 함수의 name 속성에 변수 이름 sayHi 가 할당됨.
const sayHi = function () {
  console.log("Hi");
};

console.log(sayHi.name); // sayHi 출력

그런데 함수에 이름을 붙여주면 name 속성은 함수 이름을 문자열로 갖게 된다. 물론 이름을 붙여준다 해서 불록 외부에서 호출할 수 있는 것은 아니다!! 이 이름은 여전히 코드블록 내에서만 쓸 수 있는 지역적인 이름이다.

const sayHi = function printHiInConsole() {
  console.log("Hi");
};

console.log(sayHi.name); // printHiInConsole 호출됨.

왜 굳이 이름을 따로 지어주는 걸까? 이는 함수 자신을 함수 내부에서 호출하기 위함이다. (ex. 재귀함수) 만약 이름이 없는 채로 변수명으로 함수 내부에서 자기 자신을 호출했다고 하자. 그런데 외부에서 해당 변수를 실수로 수정했다고 치자! 아래 코드와 같이 말이다...

// 에러 발생 코드.
let countdown = function(n) {
  console.log(n);
  if (n === 0) {
    console.log('End!');
  } else {
    countdown(n - 1); // 여기서 문제가 생김..
  }
};
let myFunction = countdown;
countdown = null;
myFunction(5); // TypeError

js에서 function은 배열이나 객체 같은 참조형 타입이다. 그래서 countdown 변수에는 function{...} 의 주소값이 담기고 myFunction에도 function{...}의 주소값이 담긴다. 때문에 위의 코드에서 myFunction(5) 로 함수를 호출하면 function 이 실행되기는 한다!!!

그런데 함수를 실행하다가 6번째 줄, countdown(n-1) 에서 에러가 발생해버린다. countdown 은 null을 가리키게끔 수정되었기 때문이다.

만약 기명함수로 표현한다면 countdown 변수에 null 값이 담기더라도 함수에 영향을 주지 않으므로 정상적으로 작동한다.

let countdown = function printCountdown(n) {
  console.log(n);
  if (n === 0) {
    console.log('End!');
  } else {
    printCountdown(n - 1);
  }
};

let myFunction = countdown;
countdown = null;
myFunction(5); // 정상적으로 동작

사진으로 보면 이렇다.

(무기명함수에서는 함수 내부에서 함수 호출식이 null 값으로 바뀌어 에러)

(기명함수에선 함수 내부 함수 호출식이 countdown 값이 바뀌든 말든 에러나지 않음.)


2. 즉시 실행 함수

함수가 선언된 그 즉시 바로 실행되는 함수를 immediately invoked function expression, IIFE라고 부른다.

(function () {
  console.log("hi");
})();

(function (x, y) {
  console.log(x + y);
})(3, 5);

IIFE는 함수에 이름을 지어주더라도 외부에선 사용 불가능하며 재사용도 안된다. 그래서 보통 IIFE는 이름 없는 익명함수로 만들지만, 재귀함수 같은 걸 구현할 때엔 이름이 필요할 수도 있다.

// 이름을 붙여도 재사용 불가.
(function printHi() {
  console.log("hi");
})();
printHi(); // 에러 발생!!!


// 재귀함수를 짤 때엔 이름을 붙이자.
(function countdown(n) {
  console.log(n);
  if (n === 0) {
    console.log("End!");
  } else {
    countdown(n - 1);
  }
})(5);

즉시실행함수는 선언 동시에 실행되는 함수기 때문에 보통 프로그램 초기화 기능으로 실행된다.

(function init() {
  // 프로그램이 실행 될 때 기본적으로 동작할 코드들..
})();

또한 즉시실행함수는 재사용 하지 않을 일회성 함수로도 활용된다. 함수 내부 변수는 외부 전역변수로부터 조금은 자유로워지니 이걸 활용해 이름 지을 때의 고뇌로부터 좀 더 자유로워진 코드를 짤 수도 있다.

const firstName = "Young";
const lastName = "Kang";

const greetingMessage = (function () {
  const fullName = `${firstName} ${lastName} `;

  return `Hi! My name is ${fullName}`;
})();



3. 함수를 값처럼 활용하기.

함수 표현식에서 봤듯이 함수 그 자체를 변수처럼 활용할 수 있다. 객체 속성으로 넣거나, 값으로 넣거나, 다른 함수의 인자로 주는 등을 할 수 있다!!

(이렇게 다른 함수의 인자로 넘겨주는 함수를 callback 함수라고도 한다.)

고차함수

고차함수는 함수를 이중으로 쓸 수 있게 하는 방식이다.

function getPrintHi() {
  return function () {
    console.log("hi");
  };
}

// hi 출력됨.
const hi = getPrintHi();
console.log(getPrintHi());
console.log(hi);

// 얘도 hi 출력됨.고차함수로 리턴되는 함수를 바로 호출하려면 소괄호를 두 번 쓰자.
getPrintHi()();

이렇게 함수를 다양하게 활용할 수 있는 것을 일급 함수라 하고, 그래서 js는 일급 함수 언어라고도 불린다!!



4. 인자와 파라미터. argument, parameter

파라미터란 함수에 값을 전달하기 위해 사용하는 변수. 외부로 부터 값을 전달받기 위해 함수를 선언할 때 작성하는 것이 파라미터, 함수를 호출 할 때 파라미터로 전달하는 값이 인자이다!!

보통 인자와 파라미터를 혼용해서 쓰지만, 엄연히 말하자면 둘은 다른 용어이다. 함수를 정의할 때 함수에 한 값을 줄 것입니다 하고 작성하는 것이 파라미터이다.

그리고 함수를 호출할 때 파라미터 위치에 실제로 내가 주는 값을 인자라고 부른다.

// name이 파라미터
function print(name) {
  console.log(name);
}

// hongjw이 아규먼트이다!
print("hongjw");

js에서는 함수 파라미터에 옵셔널한 값을 줄 수도 있다.

function greeting(name = "hong") {
  console.log(name);
}
greeting(); // hong 출력.
greeting("kim"); // kim 출력.

// 옵셔널 파라미터는 여러 개 쓸 수도 있음.
function introduce(name="hong", age=25){
  console.log(`my name is ${name} and im ${age}.`);
}
introduce();
introduce("kim");

introduce(28); // 어!! 이상하게 출력됨...!! my name is 28 and im 25 로 출력된다.

옵셔널 값을 여러 개 줄 수는 있지만, 순서를 잘 맞춰 써야 한다. 인자로 무엇을 주든 간에, 첫번째 인자는 무작정 첫 번째 파라미터로 넘어가기 때문이다. 때문에 옵셔널 값을 여러 개 쓰고 싶다면 undefined를 의도적으로 주고 옵셔널 값을 발동시켜야 한다.

function greeting(name = "hong", age=20, interest){
    console.log(name);
    console.log(age);
    console.log(interest);
}

// 25 js undefined가 출력됨.
greeting(25 "js");

// hong 25 js 출력됨.
greeetings(undefined, 25, "js");

여러 개의 인자를 다룰 때엔 arguments를 썼"었"다.

함수를 만들 때 인자 개수를 정하지 않고 1개나 여러 개나 맘대로 주고 싶다면 어떻게 해야 할까? 이 땐 함수 내부에서 arguments라는 특별한 객체를 썼었다! (요즘엔 새로 나온 문법인 rest parameter을 쓰는데 밑에 기술하겠다.)

function printArg(a, b) {
  console.log(arguments);
  console.log(arguments.length);
  console.log(arguments[0]);

  for (let arg of arguments) {
    console.log(arg);
  }
}

이렇게 하면 인자를 몇 개를 받든 활용 가능하다. arguments라는 예약어를 사용해서 넘친 인자들을 받아오는 것이다. 단, arguments는 유사배열이라 배열 메소드를 쓸 수 없다. splice 같이 인덱스 기준으로 자르기가 불가능한 것.

그래서 최근 등장한 문법이 바로 rest parameter이다.


인자를 여러 개 다루를 때엔 rest parameter!!

2015년 이후 rest parameter이라는 문법이 등장했다.

function printArg(...args) {
  for (const arg of args) {
    console.log(arg);
  }
}

arguments와 마찬가지로 인자 여러 개를 받아오는데, 유사배열이 아니라 배열 형태로 가져온다. 그래서 rest parameter은 splice로 인덱싱 접근이 가능하고 배열 메소드를 사용할 수 있다!!

주의할 것은!! rest parameter은 인자 여러 개를 받으면 필수 인자 이외의 것들을 배열로 묶는 역할이므로 꼭 파라미터 맨 마지막에 써야 한다는 점.

function printArg(first, second, ...others) {
  console.log(first);
  console.log(second);
  for (const arg of others) {
    console.log(arg);
  }
}

이젠 rest parameter만 기억하고 쓰면 되지만, 2015년 문법 이전에는 arguments도 자주 썼으니 arguments도 알아두자.


5. 새로운 함수 정의 방식, 화살표 함수 arrow function

2015 이후에 새롭게 등장한 문법 중 하나로 화살표 함수가 있다. arrow function은 기존 함수 선언을 좀 더 간결하게 만들어준다. arrow function으로 만든 함수는 이름이 없는 익명 함수기 때문에 보통 다른 함수의 아규먼트를 선언할 때나 함수 표현식을 쓸 때 많이 사용한다.

// 일반 함수표현식
const double = function (num) {
  return num * 2;
};
myBtn.addEventListener("click", function () {
  console.log(e.target);
});

// arrow function
const double = (num) => {
  return num * 2;
};
myBtn.addEventListener("click", () => {
  console.log(e.target);
});

특정 상황에서는 arrow function을 더 간결하게 쓸 수도 있다.

// 1. 파라미터가 1개 뿐일 때엔 소괄호 생략 가능. 근데 가독성 문제로 그냥 소괄호 다 쓰는 게 좋다.
//const double = num => {
//  return num * 2;
//};

// 2. 함수가 return 문만 있다면 중괄호와 리턴 생략 가능.
const double = (num) => num * 2;

리턴 값이 객체인 경우 소괄호를 쓰면 가능하다.

//위 아래는 같은 함수.
const getPrint = function () {
  return { name: "print" };
};
const getPrint = () => {
  (
    // 이 소괄호 없어지면 에러.
    name: "print";
  )
};

arrow function은 간편하지만 몇몇 제약점이 있다. 일단 arguments를 못 쓴다. 대신 rest parameter은 사용 가능하고 옵셔널 파라미터를 줄 수 있다.

그리고 모든 화살표 함수는 익명 함수이다!! 이름을 따로 줄 수는 없고 변수에 할당하거나 다른 함수를 호출할 때 아규먼트로 사용 가능.

또한 화살표 함수에서 this 키워드는 의미가 좀 달라진다.


this 키워드.

this는 기본적으로 전역 객체인 window를 가리킨다.

그런데 객체의 메소드로 this가 쓰이는 경우엔 this는 메소드를 호출한 해당 객체를 가리킨다.

function getName() {
  return `$(this.first) $(this.last)`;
}

const user = {};
const admin = {};
// user의 name 출력
console.log(user.getName);
// admin의 name 출력
console.log(admin.getName);

단, arrow function은 this 값을 가질 수 없다!!

정확히 말하자면 arrow 함수 내부에 선언되는 this 키워드는 객체에 따라 값이 바뀌지 않고, 직전에 사용됐던 객체의 값만 담기 때문에 this를 활용하기 어렵다.

이런 점 때문에 일반 함수가 arrow보다 더 권장되기도 한다.