자바스크립트 함수 - 선언식과 화살표 함수, 기본값과 나머지 매개변수
이 단원에서 배우는 것
4단원까지 오면 참가자 한 명을 판정하고 목록을 도는 코드를 쓸 수 있다. 그런데 앞 단원 예제에서 이미 "출석 횟수로 등급을 매기는 조건문"이 두 번 나왔다. 같은 판정이 화면 그리는 곳, 통계 내는 곳, 메일 보내는 곳에 각각 복사돼 있으면 기준이 바뀔 때 세 군데를 모두 고쳐야 하고, 한 군데를 빠뜨린다. 함수는 이 문제를 푸는 도구다.
- 함수 선언식·함수 표현식·화살표 함수를 구분하고, 어느 것이 언제부터 호출 가능한지 설명한다.
- 매개변수 기본값과 나머지 매개변수로 호출부를 단순하게 만든다.
- 화살표 함수를 객체 메서드로 쓰면 왜 깨지는지 이해하고 피한다.
화살표 함수와 기본값·나머지 매개변수는 ES2015 문법이다. 지금은 모든 최신 브라우저와 Node 20 이상에서 기본이다.
왜 필요한가
이런 코드가 파일 여기저기에 흩어져 있다고 하자.
const members = [
{ name: "김지훈", sessions: 8, paid: true },
{ name: "박서연", sessions: 3, paid: false },
{ name: "이도현", sessions: 0, paid: true }
];
for (const m of members) {
if (m.sessions >= 8) console.log(`${m.name}: 개근`);
else if (m.sessions >= 4) console.log(`${m.name}: 성실`);
else console.log(`${m.name}: 독려`);
}
다음 달에 개근 기준이 8회에서 6회로 바뀌면 이 조건이 복사된 모든 곳을 찾아야 한다. 함수로 빼면 기준은 한 곳에만 있다.
function gradeOf(sessions) {
if (sessions >= 8) return "개근";
if (sessions >= 4) return "성실";
return "독려";
}
for (const m of members) {
console.log(`${m.name}: ${gradeOf(m.sessions)}`);
}
김지훈: 개근
박서연: 독려
이도현: 독려
함수의 값어치는 "코드를 줄인다"가 아니라 "바뀔 것을 한 곳에 모은다"에 있다. 그리고 gradeOf라는 이름이 붙는 순간, 읽는 사람이 조건 세 줄을 해석하지 않아도 된다. 이름이 곧 설명이다.
문법과 예제
세 가지 작성 방식
// 1) 함수 선언식
function gradeOf(sessions) {
return sessions >= 8 ? "개근" : "일반";
}
// 2) 함수 표현식 — 함수를 값으로 만들어 변수에 담는다
const gradeOf2 = function (sessions) {
return sessions >= 8 ? "개근" : "일반";
};
// 3) 화살표 함수 — 표현식의 짧은 형태
const gradeOf3 = (sessions) => {
return sessions >= 8 ? "개근" : "일반";
};
// 본문이 값 하나면 중괄호와 return 을 생략할 수 있다
const gradeOf4 = (sessions) => sessions >= 8 ? "개근" : "일반";
네 개 모두 같은 일을 한다. 차이는 두 가지다.
| 구분 | 선언식 | 표현식·화살표 |
|---|---|---|
| 선언 전 호출 | 가능(끌어올려짐) | 불가 — ReferenceError |
자기 this | 있음 | 선언식·표현식은 있음, 화살표는 없음 |
console.log(gradeOf(9)); // 개근 — 아래에 선언돼 있어도 호출된다
function gradeOf(s) { return s >= 8 ? "개근" : "일반"; }
console.log(gradeOf2(9)); // ReferenceError: Cannot access 'gradeOf2' before initialization
const gradeOf2 = function (s) { return s >= 8 ? "개근" : "일반"; };
선택 기준은 팀 규칙을 따르면 되지만, 흔한 관례는 이렇다. 파일 최상단에 놓는 독립적인 함수는 선언식, 다른 함수에 넘겨주는 짧은 함수는 화살표. 8단원의 이벤트 처리 함수도 대부분 화살표로 쓴다.
매개변수 기본값
function feeMessage(name, fee = 30000) {
return `${name} 님 회비 ${fee}원`;
}
console.log(feeMessage("김지훈")); // 김지훈 님 회비 30000원
console.log(feeMessage("박서연", 20000)); // 박서연 님 회비 20000원
기본값은 앞의 매개변수를 참조할 수도 있다.
function feeWithLate(fee, late = fee * 0.1) {
return fee + late;
}
console.log(feeWithLate(30000)); // 33000
console.log(feeWithLate(30000, 0)); // 30000
나머지 매개변수
개수가 정해지지 않은 인자를 배열로 받는다.
function totalSessions(...counts) {
let sum = 0;
for (const c of counts) sum += c;
return sum;
}
console.log(totalSessions(8, 3, 0)); // 11
console.log(totalSessions()); // 0 — 인자가 없으면 빈 배열
function report(title, ...names) {
return `${title}: ${names.length}명`;
}
console.log(report("8월 스터디", "김지훈", "박서연")); // 8월 스터디: 2명
...는 반드시 마지막 매개변수여야 한다. 옛날 코드에는 같은 목적으로 arguments라는 특수 객체를 쓰는데, 진짜 배열이 아니라서 배열 기능을 그대로 못 쓰고 화살표 함수에는 아예 없다. 새 코드에서는 나머지 매개변수만 쓴다.
함수를 값으로 넘기기
함수는 값이므로 다른 함수에 인자로 넘길 수 있다. 6단원의 배열 메서드와 8단원의 이벤트가 전부 이 구조다. 미리 감을 잡아 둔다.
function printEach(list, formatter) {
for (const item of list) {
console.log(formatter(item));
}
}
const members = [
{ name: "김지훈", sessions: 8 },
{ name: "박서연", sessions: 3 }
];
printEach(members, (m) => `${m.name} (${m.sessions}회)`);
김지훈 (8회)
박서연 (3회)
printEach는 "어떻게 출력할지"를 모른다. 그 판단을 호출하는 쪽이 함수로 넘겨 준다. 이렇게 넘기는 함수를 콜백이라고 부른다.
함수 안의 변수는 밖에서 안 보인다
function gradeOf(sessions) {
const limit = 8; // 이 함수 안에서만 존재한다
return sessions >= limit ? "개근" : "일반";
}
gradeOf(9);
// console.log(limit); // ReferenceError: limit is not defined
함수 밖 변수를 함수 안에서 읽는 것은 된다. 다만 함수가 밖의 값에 의존하기 시작하면 "이 함수만 보면 결과를 알 수 있다"는 성질이 깨진다. 필요한 값은 매개변수로 받는 것이 기본이다.
실무에서 자주 틀리는 것
1. 객체 메서드를 화살표 함수로 쓴다
const study = {
title: "8월 자바스크립트 스터디",
introBad: () => `${this.title} 입니다`,
introGood() {
return `${this.title} 입니다`;
}
};
console.log(study.introGood()); // 8월 자바스크립트 스터디 입니다
console.log(study.introBad()); // undefined 입니다 (환경에 따라 빈 값)
this는 "메서드를 호출한 객체"를 가리키는 이름이다. 그런데 화살표 함수는 자기 this를 만들지 않고 바깥 것을 그대로 쓴다. 객체 리터럴은 this를 만들어 주는 자리가 아니므로, 화살표로 쓴 메서드의 this는 study가 아니라 파일 바깥 것이 된다. 브라우저 일반 스크립트에서는 window, ES 모듈이나 Node에서는 undefined다.
규칙은 단순하다. 객체의 메서드는 introGood() 형태의 축약 메서드로 쓴다. 화살표 함수는 다른 함수에 넘기는 짧은 콜백에 쓴다.
2. 화살표 함수에서 객체를 반환하려다 undefined 를 받는다
const toRowBad = (m) => { name: m.name, grade: gradeOf(m.sessions) }; // 문법 오류
const toRowAlso = (m) => { name: m.name }; // undefined 반환
const toRowGood = (m) => ({ name: m.name, grade: gradeOf(m.sessions) });
console.log(toRowGood({ name: "김지훈", sessions: 8 }));
// { name: '김지훈', grade: '개근' }
화살표 뒤의 중괄호를 자바스크립트는 함수 본문으로 읽는다. 객체를 그대로 돌려주려면 소괄호로 한 번 감싸야 한다. 두 번째 줄처럼 키가 하나면 문법 오류도 안 나고 조용히 undefined가 나오기 때문에 더 헷갈린다.
3. 기본값이 null 에는 적용되지 않는다
function feeMessage(name, fee = 30000) {
return `${name}: ${fee}`;
}
console.log(feeMessage("김지훈")); // 김지훈: 30000
console.log(feeMessage("김지훈", undefined)); // 김지훈: 30000
console.log(feeMessage("김지훈", null)); // 김지훈: null ← 기본값이 안 먹는다
console.log(feeMessage("김지훈", 0)); // 김지훈: 0
매개변수 기본값은 인자가 undefined일 때만 적용된다. 서버 응답의 빈 항목은 보통 null로 오기 때문에 실무에서 자주 걸린다. 함수 안에서 const realFee = fee ?? 30000;처럼 4단원의 ??를 한 번 더 쓰거나, 값을 넘기는 쪽에서 null을 걸러야 한다.
4. return 다음 줄에 값을 쓴다
function toRow(m) {
return
{ name: m.name };
}
console.log(toRow({ name: "김지훈" })); // undefined
자바스크립트는 return 뒤에 줄바꿈이 오면 세미콜론을 자동으로 넣어 버린다. 오류 없이 undefined가 반환된다. 반환할 값은 반드시 return과 같은 줄에서 시작한다. 길면 여는 괄호까지는 같은 줄에 둔다.
스스로 확인하기
- 다음 코드에서 한 줄은 정상 동작하고 한 줄은 오류가 난다. 어느 쪽이며 이유는?
console.log(a(1)); console.log(b(1)); function a(n) { return n; } const b = function (n) { return n; }; - 참가자 배열을 받아 이름만 골라
"김지훈, 박서연"형태의 문자열로 돌려주는 함수nameList를 작성하라. 배열이 비면"참가자 없음"을 돌려준다. (반복문과join을 쓴다.) - 다음 화살표 함수는 항상
undefined를 돌려준다. 이유와 고친 코드를 쓰라.const toRow = (m) => { name: m.name };
정답
- 첫 줄은 정상, 둘째 줄에서
ReferenceError: Cannot access 'b' before initialization이 난다. 함수 선언식은 실행 전에 통째로 끌어올려지지만,const에 담은 함수 표현식은 그 줄에 도달해야 값이 생긴다. - 예시 답:
function nameList(members) { if (members.length === 0) return "참가자 없음"; const names = []; for (const m of members) names.push(m.name); return names.join(", "); } console.log(nameList([{ name: "김지훈" }, { name: "박서연" }])); // 김지훈, 박서연 console.log(nameList([])); // 참가자 없음 - 화살표 뒤의 중괄호가 객체 리터럴이 아니라 함수 본문으로 해석되고,
name: m.name은 레이블 문법으로 읽혀 아무것도 반환하지 않는다.const toRow = (m) => ({ name: m.name });처럼 소괄호로 감싸면 된다.