thenullpage.com
함수는 코딩 배우고 둘째 주쯤 만지는 문법이다. 값을 넣으면 값이 나온다, 그게 전부인 줄 알았다. 그런데 나를 제일 오래 붙잡아둔 버그들은 함수 안 계산이 틀려서 난 게 아니었다. 이 함수를 언제부터 쓸 수 있는지, 이 값이 어디까지 보이는지를 몰라서 났다.
함수를 만드는 방식이 두 가지인데, 하나는 미리 끌어올려지고 하나는 아니다. 화면 초기화 코드를 정리하다가 함수 하나를 화살표 함수로 바꿨더니 페이지가 통째로 안 떴다.
renderPrice(15000); // 정의보다 위에서 함수를 부른다
const renderPrice = (n) => {
console.log(n.toLocaleString() + "원");
};
// ReferenceError: Cannot access 'renderPrice' before initialization
같은 함수처럼 보여도 만드는 방식이 다르면 쓸 수 있는 시점이 달라진다. function renderPrice() {} 형태로 쓰는 함수 선언식은 자바스크립트가 코드를 실행하기 전에 파일을 한 번 훑으면서 함수 전체를 미리 등록해둔다. 그래서 선언보다 위에서 불러도 멀쩡히 돌아간다. 반면 const에 화살표 함수를 담는 함수 표현식은 이름만 자리를 잡아둘 뿐 실제 함수는 그 줄에 도달해야 들어간다. 그 위쪽 구간은 이름은 있는데 꺼내 쓸 수 없는 상태고, 이 구간을 TDZ(temporal dead zone, 일시적 사각지대)라 부른다. 호출을 정의 아래로 내리니 바로 잡혔다.
const renderPrice = (n) => {
console.log(n.toLocaleString() + "원");
};
renderPrice(15000); // 15,000원
선언식으로 쓰면 순서를 안 지켜도 돌아가긴 한다. 그래도 나는 호출을 정의 아래로 맞추는 쪽으로 통일했다.
매개변수 기본값은 값이 아예 안 넘어왔을 때만 걸린다. 등급 없는 회원한테 "일반"을 붙여주려고 기본값을 걸어놨는데, 화면에 이상한 글자가 찍혔다.
function makeBadge(name, grade = "일반") {
return name + " (" + grade + ")";
}
console.log(makeBadge("김철수")); // 김철수 (일반), 여긴 잘 나온다
console.log(makeBadge("이영희", null)); // 이영희 (null), 서버가 null을 내려주면 이렇게 된다
기본값은 인자가 undefined일 때만 대신 들어간다. 서버 응답에 등급이 null로 담겨 오면 그건 엄연히 넘어온 값이라 기본값이 비켜준다. 숫자 0이나 빈 문자열도 그대로 통과한다. 값이 빈 경우를 폭넓게 막고 싶으면 함수 안에서 const label = grade ?? "일반";처럼 한 번 더 걸러야 한다. 기본값은 호출할 때마다 새로 계산된다. 그래서 function addItem(item, list = [])처럼 써도 호출마다 빈 배열이 새로 생겨 앞 호출 결과가 딸려 오지 않고, function box(w, h = w)처럼 앞 매개변수를 기본값 자리에 갖다 쓸 수도 있다.
함수 모양만 화살표로 바꿨다가 arguments가 통째로 사라진 적도 있다. 인자를 몇 개 받든 전부 로그로 남기던 함수였다.
const logAll = () => {
console.log(arguments); // ReferenceError: arguments is not defined
};
logAll("결제", 15000);
function으로 만든 함수에는 arguments라는 상자가 자동으로 딸려 온다. 넘어온 인자가 전부 여기 담긴다. 그런데 화살표 함수는 이 상자를 아예 만들지 않는다. 바깥에 같은 이름이 있으면 엉뚱하게 그걸 집어오고, 없으면 위처럼 에러가 난다. 게다가 arguments는 배열처럼 생겼을 뿐 진짜 배열이 아니라서 map이나 filter도 못 쓴다. 요즘은 이렇게 받는다.
const logAll = (...args) => {
console.log(args.length, args); // 2 ["결제", 15000]
console.log(args.map((v) => typeof v)); // ["string", "number"]
};
logAll("결제", 15000);
매개변수 이름 앞에 점 세 개를 붙이면 남은 인자가 전부 진짜 배열로 모인다. 이걸 rest 파라미터(나머지 매개변수)라 부른다. function log(tag, ...rest)처럼 쓰면 첫 인자만 tag에 들어가고 나머지가 rest 배열이 된다. 대신 rest는 항상 마지막 자리여야 한다.
같은 점 세 개인데 자리에 따라 하는 일이 정반대다. 매개변수 자리에서는 인자를 모으고, 함수를 부르는 괄호 안에서는 반대로 펼친다. 최고 점수를 뽑는 코드에서 이걸 헷갈려 숫자 대신 NaN이 찍혔다.
const scores = [88, 92, 75];
console.log(Math.max(scores)); // NaN, 배열 하나를 통째로 넘겨서 계산을 못 한다
console.log(Math.max(...scores)); // 92, 88과 92와 75 세 개로 펼쳐서 넘긴다
Math.max는 숫자를 하나씩 나열해서 받는 함수다. 배열을 그대로 주면 그걸 숫자로 바꾸려다 실패해서 NaN이 나온다. 호출 괄호 안에서 점 세 개를 붙이면 배열이 원소 단위로 풀려 인자로 들어가는데, 이쪽을 spread(펼치기)라 한다. 함정이 하나 더 있다. 배열이 비었으면 인자가 하나도 없는 셈이라 -Infinity가 나온다. 비어 있을 수 있는 목록이면 개수부터 확인하고 넘겨야 한다.
안쪽에서는 바깥이 보이는데, 바깥에서는 안쪽이 안 보인다. 조건에 따라 계산한 배송비를 돌려주려다 그런 이름이 없다는 에러를 만났다.
function getShippingFee(total) {
if (total < 50000) {
const fee = 3000;
}
return fee; // ReferenceError: fee is not defined
}
console.log(getShippingFee(30000));
const와 let으로 만든 값은 자기를 감싼 중괄호 안에서만 산다. if 블록이 닫히는 순간 fee는 사라지니 바깥 return은 그런 이름을 찾지 못한다. 블록 밖에서도 쓸 값이라면 처음부터 바깥에 만들어두고 안에서 채워야 한다.
function getShippingFeeFixed(total) {
let fee = 0;
if (total < 50000) {
fee = 3000;
}
return fee;
}
console.log(getShippingFeeFixed(30000)); // 3000
console.log(getShippingFeeFixed(70000)); // 0
반대 방향은 자유롭다. 함수 안에서 함수 바깥의 값을 꺼내 쓰는 건 얼마든지 된다. 자바스크립트는 이름을 찾을 때 지금 있는 블록부터 보고, 없으면 한 겹 바깥, 그래도 없으면 또 한 겹 바깥으로 올라가며 뒤진다. 이 연결 고리를 스코프 체인이라 부른다. 다만 여기 기대서 바깥 값을 함수 안에서 마음대로 고치기 시작하면 그 값이 언제 바뀐 건지 추적이 안 된다. 필요한 값은 인자로 받고 결과는 return으로 내보내는 편이 낫다.
돌아보면 여기 나온 사고들은 함수가 뭘 계산하느냐와 아무 상관이 없었다. 언제부터 부를 수 있는지, 어디까지 값이 보이는지, 점 세 개가 지금 어느 쪽으로 일하는지를 몰라서 났다. 그래서 함수를 짤 때 네 가지를 습관처럼 본다. 호출이 정의보다 위에 있진 않은지, 기본값이 null에도 걸릴 거라 착각하고 있진 않은지, 인자를 모으는 자리인지 펼치는 자리인지, 쓰려는 값이 이 중괄호 안에서 살아 있는지. 문법 자체는 한 시간이면 다 훑지만 이 네 개는 한 번씩 사고를 치고 나서야 몸에 붙는다.