3편. 함수 다루기: 함수 선언, this, 화살표 함수, 클로저(Closure), 콜백(Callback)
📚 목차
1. 함수 정의와 실행 원리
2. 화살표 함수 (Arrow Function)
3. 함수 실행 컨텍스트와 this 이해하기
4. 고차 함수 및 함수 패턴: 클로저(Closure), 콜백(Callback)
✔ 마무리

📂 [GitHub 실습 코드 보러가기] (https://github.com/cericube/nodejs-tutorials) /JavaScript
1. 함수 정의와 실행 원리
JavaScript에서 함수는 특정 작업을 수행하거나 값을 계산하는 코드 블록입니다. 함수를 사용하면 코드를 재사용하고 모듈화 하여 프로그램의 구조를 체계적으로 만들 수 있습니다.
🔷 함수의 정의: 함수 선언 vs. 함수 표현식의 차이와 호이스팅
JavaScript에서 함수를 정의하는 방식은 크게 두 가지가 있으며, 이들은 코드가 실행되기 전에 JavaScript 엔진이 메모리에 코드를 처리하는 방식인 호이스팅(Hoisting)에서 결정적인 차이를 보입니다.
1. 함수 선언문 (Function Declaration)
함수 선언문은 가장 전통적인 방식으로, function 키워드 뒤에 함수 이름과 매개변수 목록을 명시합니다.
▸ 구조: function 함수이름(매개변수) { ... }
▸ 호이스팅 동작: 함수 선언문은 전체가 호이스팅됩니다.
즉, 코드 실행 전 JavaScript 엔진이 해당 함수 선언 전체를 메모리에 먼저 등록합니다. 따라서 함수를 정의한 코드 라인보다 앞에서 호출해도 정상적으로 동작합니다. 이는 마치 함수가 파일의 맨 위로 끌어올려지는(hoisted) 것처럼 보입니다.
// 함수 선언문
function greet(name) {
return `안녕하세요, ${name}님!`;
}
// 호이스팅으로 인해 선언 전에도 호출 가능
console.log(sayHello('철수')); // "Hello, 철수!"
function sayHello(name) {
return `Hello, ${name}!`;
}
2. 함수 표현식 (Function Expression)
함수 표현식은 함수를 값(value)으로 취급하여 변수에 할당하는 방식입니다. 함수 이름이 없는 익명 함수(Anonymous Function)를 사용하는 경우가 많습니다.
▸ 구조: const 변수이름 = function(매개변수) { ... }
▸ 호이스팅 동작: 함수 표현식은 변수 선언 부분(const sayHi 등)만 호이스팅 되고, 변수에 함수가 할당되는 부분(= function(...) ...)은 코드가 해당 라인에 도달했을 때 비로소 실행됩니다.
따라서 선언 전에 호출하려고 하면, 변수는 존재하지만 아직 함수 값으로 초기화되지 않아 ReferenceError가 발생합니다
// 함수 표현식
const greet = function(name) {
return `안녕하세요, ${name}님!`;
};
// 에러 발생! Cannot access 'sayHi' before initialization
// console.log(sayHi('영희'));
const sayHi = function(name) {
return `Hi, ${name}!`;
};
console.log(sayHi('영희')); // "Hi, 영희!"
🔷 함수의 호출: 인자 전달 방식과 매개변수
함수를 호출할 때는 함수 정의 시 설정한 매개변수(Parameter)에 대응하는 인자(Argument)를 전달합니다. JavaScript는 인자 처리에서 높은 유연성을 제공합니다.
📌 핵심 포인트:
JavaScript는 호출 시 전달된 인자의 개수가 함수 정의의 매개변수 개수와 일치하지 않아도 에러가 발생하지 않습니다.
▸ 부족한 인자: 대응되는 매개변수는 undefined 값을 갖게 됩니다. (기본 매개변수를 설정하지 않은 경우)
▸ 초과된 인자: 초과된 인자는 무시되지만, 함수 내부의 특수 객체인 arguments를 통해 접근할 수는 있습니다.
1. 기본 매개변수 (Default Parameters)
함수를 정의할 때 매개변수에 미리 기본값을 할당해 둘 수 있습니다. 호출 시 해당 인자를 생략하면 기본값이 사용됩니다.
// 기본 매개변수 (Default Parameters)
function createUser(name, age = 20, role = 'user') {
return { name, age, role };
}
// { name: '김철수', age: 20, role: 'user' } (age와 role에 기본값 적용)
console.log(createUser('김철수'));
// { name: '이영희', age: 25, role: 'admin' } (기본값 대신 전달된 값 사용)
console.log(createUser('이영희', 25, 'admin'));
2. 나머지 매개변수 (Rest Parameters)
... (점 세 개)를 사용하여 정의하는 나머지 매개변수는 함수에 전달된 남아있는 모든 인자들을 배열 형태로 묶어서 받아옵니다. 이는 인자의 개수가 가변적일 때 매우 유용합니다.
// 나머지 매개변수 (Rest Parameters)
function sum(...numbers) {
// numbers는 전달된 모든 인자를 담은 배열이 됩니다.
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum(10, 20)); // 30
3. 구조 분해 할당을 활용한 매개변수
객체나 배열을 인자로 받을 때, 함수 정의부에서 바로 구조 분해 할당(Destructuring Assignment)을 사용하여 필요한 속성들만 추출할 수 있습니다. 이는 객체의 속성 이름을 명시적으로 보여주어 코드의 가독성을 높입니다.
// 구조 분해 할당을 활용한 매개변수
function displayUser({ name, age, email = '미등록' }) {
// 인자로 전달된 객체에서 name, age 속성을 추출하고 email은 기본값을 설정합니다.
console.log(`이름: ${name}, 나이: ${age}, 이메일: ${email}`);
}
displayUser({ name: '박민수', age: 30 });
// 이름: 박민수, 나이: 30, 이메일: 미등록
🔷 값의 반환: return 키워드와 함수의 종료
return 문은 함수가 수행한 작업의 결과를 함수 외부로 다시 전달하거나, 함수의 실행을 즉시 종료시키는 역할을 합니다.
1. 명시적 반환
return 키워드 뒤에 오는 값은 함수가 호출된 자리로 돌아가는 반환 값이 됩니다.
// 명시적 반환
function multiply(a, b) {
return a * b; // 이 값이 함수 호출의 결과가 됩니다.
console.log('이 코드는 실행되지 않습니다'); // Dead code (return 이후의 코드는 실행되지 않음)
}
2. 함수의 즉시 종료와 undefined 반환
▸ return 문을 만나면 함수는 즉시 종료됩니다. return 문 뒤에 어떤 코드도 실행되지 않습니다.
▸ return 문이 없거나, return 키워드만 사용하고 값을 지정하지 않으면, 함수는 코드 블록의 끝에 도달했을 때 undefined를 암묵적으로 반환합니다.
3. 조건부 Early Return 패턴 (조기 반환)
실무에서 매우 유용한 패턴입니다. 함수 시작 부분에서 입력 값 검증, 권한 확인 등 에러 또는 예외 상황을 먼저 검사하고, 문제가 발견되면 즉시 return 문으로 함수를 종료하고 에러 정보를 반환합니다.
▸ 장점: 불필요한 중첩된 if/else 구문을 줄여 코드의 가독성을 크게 높이고, 정상적인 로직을 코드의 주요 흐름으로 분리하여 파악하기 쉽게 만듭니다.
// 조건부 early return 패턴 (실무에서 자주 사용)
function processPayment(amount, balance) {
// 1. 잘못된 금액 검사 (조기 반환)
if (amount <= 0) {
return { success: false, error: '잘못된 금액' };
}
// 2. 잔액 부족 검사 (조기 반환)
if (balance < amount) {
return { success: false, error: '잔액 부족' };
}
// 3. 정상 처리 로직 (가장 바깥쪽에 위치하여 메인 로직임을 명시)
const newBalance = balance - amount;
return { success: true, newBalance };
}
2. 화살표 함수 (Arrow Function)
화살표 함수는 ES6(ECMAScript 2015)에서 도입된 새로운 함수 정의 방식입니다.
이는 기존의 function 키워드를 사용한 함수보다 문법적으로 간결하며, 특히 this 바인딩 방식에서 결정적인 차이를 보여 모던 JavaScript 개발에서 필수적으로 사용됩니다.
🔷 화살표 함수의 문법: 간결한 함수 정의와 암시적 반환
화살표 함수는 => (화살표) 기호를 사용하여 함수를 정의하며, 다양한 경우에 문법을 생략하여 코드를 매우 간결하게 만들 수 있습니다.
| 구분 | 예시 | 설명 |
| 기본 문법 | (a, b) => { return a + b; } | 일반 함수와 동일하게 중괄호 {} + return을 명시해야 합니다. |
| 암시적 반환 (Implicit Return) | (a, b) => a + b | 함수 본문에 표현식이 하나만 있을 경우, {}와 return을 생략할 수 있습니다. |
| 매개변수 하나 | x => x * x | 매개변수가 한 개일 때는 괄호 () 생략이 가능합니다. |
| 매개변수 없음 | () => Math.random() | 매개변수가 없을 때는 반드시 빈 괄호 ()를 사용합니다. |
| 객체 반환 | (name, age) => ({ name, age }) | 객체 리터럴을 암시적으로 반환할 때는 {}가 함수 본문으로 오해되는 것을 방지하기 위해 전체를 ()로 감싸야 합니다. |
// 기본 문법: 명시적 반환
const add = (a, b) => {
// 간단한 계산 로직 추가
const result = a + b + 10;
return result;
};
// 출력: add(5, 3) 결과: 18 (5 + 3 + 10)
console.log(`add(5, 3) 결과: ${add(5, 3)}`);
// 암시적 반환 (중괄호와 return 생략)
const add2 = (a, b) => a + b;
// 출력: add2(10, 5) 결과: 15 (10 + 5)
console.log(`add2(10, 5) 결과: ${add2(10, 5)}`);
// 매개변수가 하나일 때 괄호 생략 가능
const square = x => x * x;
// 출력: square(7) 결과: 49 (7 * 7)
console.log(`square(7) 결과: ${square(7)}`);
// 매개변수가 없을 때
// 0~99 사이의 정수 반환 로직 추가
const getRandomNumber = () => Math.floor(Math.random() * 100);
const randomNumber = getRandomNumber();
// 출력: getRandomNumber() 결과: (랜덤 값)
console.log(`getRandomNumber() 결과: ${randomNumber}`);
// 객체를 반환할 때는 괄호로 감싸야 함 (계산 로직: age를 1년 더해서 반환)
// 암시적 반환을 위해 객체를 소괄호 ()로 감싼 형태였습니다.
const createPerson = (name, age) => ({ name, age: age + 1 });
// 출력: createPerson('홍길동', 25) 결과: { name: '홍길동', age: 26 }
console.log(`createPerson('홍길동', 25) 결과:`, createPerson('홍길동', 25));
// 명시적 반환을 사용하고 함수 본문 내에서 로직을 처리하는 형태
const createPerson2 = (name, age) => {
// 1. 계산 로직을 함수 본문 내에서 실행
const nextAge = age + 1;
// 2. 객체를 명시적으로 반환
return {
name: name, // 또는 ES6 축약 문법으로 name,
age: nextAge
};
};
// 출력: createPerson2('홍길동', 25) 결과: { name: '홍길동', age: 26 }
console.log(`createPerson2('홍길동', 25) 결과:`, createPerson2('홍길동', 25));
✔️ 고차 함수에서의 간결함
배열의 map()과 같은 고차 함수(Higher-Order Function)에 콜백 함수로 사용될 때, 화살표 함수의 간결함은 더욱 빛을 발합니다.
코드가 한 줄로 요약되어 가독성이 높아집니다.
// 다양한 문법 형태
const numbers = [1, 2, 3, 4, 5];
// 전통적 방식 (많은 boilerplate 코드)
const doubled1 = numbers.map(function(n) {
return n * 2;
});
// 출력: 전통적 방식 (doubled1) 결과: [2, 4, 6, 8, 10]
console.log(`전통적 방식 (doubled1) 결과:`, doubled1);
// 화살표 함수 (매우 간결)
//numbers 배열의 현재 요소를 하나씩 받습니다. (1, 2, 3, 4, 5 순서)
const doubled2 = numbers.map(n => n * 2);
// 출력: 화살표 함수 방식 (doubled2) 결과: [2, 4, 6, 8, 10]
console.log(`화살표 함수 방식 (doubled2) 결과:`, doubled2);
▸ map()은 배열의 각 요소에 대해 콜백 함수를 한 번씩 실행하고, 그 함수의 반환 값들을 모아 기존 배열과 길이가 같은 새로운 배열을 반환하는 메서드입니다
🔷 화살표 함수와 일반 함수의 결정적 차이: this 바인딩
화살표 함수와 일반 함수의 가장 중요한 차이점은 this 바인딩 방식입니다.
📌 주의사항:
▸ 생성자 함수로 사용 불가:
화살표 함수는 생성자 함수로 사용할 수 없으며, new 키워드로 호출할 수 없습니다.
▸ arguments 객체 미지원:
일반 함수와 달리, 화살표 함수는 함수 호출 시 전달된 인자들을 담는 arguments 객체를 바인딩하지 않습니다.
대신 나머지 매개변수(...rest)를 사용해야 합니다.
1. 일반 함수의 this 바인딩 문제
일반 함수는 자신이 호출되는 방식에 따라 this가 동적으로 결정됩니다.
특히, 콜백 함수나 이벤트 핸들러로 사용될 때, 자신만의 새로운 this 컨텍스트를 생성합니다.
이로 인해 객체의 메서드 안에서 내부 함수를 사용할 경우, this가 원래 객체를 가리키지 못하고 전역 객체(Global Object, 브라우저에서는 window)를 가리키거나 undefined(Strict Mode)가 되는 문제가 발생합니다.
// 일반 함수의 this
const person1 = {
name: '김철수',
hobbies: ['독서', '운동', '음악'],
printHobbies: function() {
this.hobbies.forEach(function(hobby) {
// 문제 발생 지점: forEach의 콜백 함수는 새로운 this를 가집니다.
// 여기서 this는 person1 객체를 가리키지 않습니다. (undefined 또는 window)
console.log(this.name + '는 ' + hobby + '를 좋아합니다');
// 에러 발생! (this.name이 undefined이므로)
});
}
};
// person1.printHobbies(); // 실행하면 에러가 발생합니다.
2. 화살표 함수의 this 해결책 (Lexical this)
화살표 함수는 자신만의 this를 가지지 않습니다.
대신, 함수가 정의된 선언적(Lexical) 환경, 즉 상위 스코프의 this 값을 자동으로 상속받아 사용합니다. 이로써 this 컨텍스트가 꼬이는 문제를 근본적으로 해결합니다.
// 화살표 함수로 해결
const person2 = {
name: '이영희',
hobbies: ['그림', '여행', '요리'],
printHobbies: function() {
this.hobbies.forEach(hobby => {
// 해결: 화살표 함수는 상위 스코프(printHobbies 일반 함수)의 this를 상속받습니다.
// 따라서 여기서의 this는 올바르게 person2 객체를 가리킵니다.
console.log(`${this.name}는 ${hobby}를 좋아합니다`);
});
}
};
person2.printHobbies();
// 이영희는 그림을 좋아합니다
// 이영희는 여행을 좋아합니다
// 이영희는 요리를 좋아합니다
3. 비동기 코드에서의 활용
setTimeout이나 setInterval과 같은 비동기 함수 내에서도 동일하게 this 문제를 해결해 줍니다.
// setTimeout에서의 차이
const timer = {
seconds: 10,
start: function() {
// 일반 함수 사용 시: 콜백 내부의 this는 window를 가리켜 timer.seconds를 감소시키지 못함.
setInterval(function() {
this.seconds--;
console.log(this.seconds); // NaN
}, 1000);
},
startWithArrow: function() {
// 화살표 함수 사용 시: 콜백 내부의 this가 startWithArrow가 정의된 timer 객체를 가리킴.
setInterval(() => {
this.seconds--; // 올바른 this (timer 객체)
console.log(this.seconds); // 9, 8, 7...
}, 1000);
}
};
🔷 실무 활용: 배열 고차 함수에서의 사용
화살표 함수는 map, filter, reduce와 같은 배열 고차 함수(Array High-Order Functions)의 콜백으로 사용될 때, 간결한 문법과 명확한 this 바인딩 덕분에 모던 JavaScript 코드의 표준처럼 자리 잡았습니다.
1. 데이터 처리 및 변환
const products = [
{ name: '노트북', price: 1200000, category: '전자제품' },
{ name: '마우스', price: 30000, category: '전자제품' },
{ name: '키보드', price: 80000, category: '전자제품' },
{ name: '의자', price: 150000, category: '가구' }
];
// filter: 조건에 맞는 요소만 추출 (가격이 10만원 이하인 상품)
const electronics = products.filter(p => p.category === '전자제품');
// map: 각 요소를 변환 (상품 이름만 추출)
const productNames = products.map(p => p.name);
// reduce: 누적 계산 (총 가격 계산)
// reduce: 배열의 각 요소에 대해 주어진 콜백 함수를 실행하여,
// 최종적으로 배열을 단 하나의 값으로 축소(reduce)합니다
// sum은 누적값(accumulator), p는 현재 요소입니다. 초기값 0에서 시작합니다.
const totalPrice = products.reduce((sum, p) => sum + p.price, 0);
console.log(totalPrice); // 1460000
2. 메서드 체이닝 (Method Chaining) 활용
함수 호출을 연속적으로 연결하는 메서드 체이닝 패턴에서 화살표 함수를 사용하면 데이터 처리 파이프라인을 매우 읽기 쉽게 구성할 수 있습니다.
// 메서드 체이닝 활용
const result = products
// 1. filter: 가격이 100000 미만인 상품만 필터링
.filter(p => p.price < 100000)
// 2. map: 각 상품에 discountPrice 속성을 추가 (10% 할인)
.map(p => ({ ...p, discountPrice: p.price * 0.9 }))
// 3. sort: 할인 가격을 기준으로 오름차순 정렬
.sort((a, b) => a.discountPrice - b.discountPrice);
console.log(result);
// 가격 10만원 미만 상품을 10% 할인하여 오름차순 정렬
3. 실무 예제: 데이터 그룹화
reduce 함수를 사용하여 복잡한 데이터 구조를 원하는 형태로 그룹화할 때도 화살표 함수는 빛을 발합니다.
// 실무 예제: 데이터 그룹화
const students = [
{ name: '김철수', grade: 'A', score: 95 },
{ name: '이영희', grade: 'B', score: 85 },
{ name: '박민수', grade: 'A', score: 92 },
{ name: '정수진', grade: 'C', score: 78 }
];
// reduce
// 배열의 각 요소에 대해 주어진 콜백 함수를 실행하여,
// 최종적으로 배열을 단 하나의 값으로 축소(reduce)합니다
const groupedByGrade = students.reduce((acc, student) => {
const { grade } = student;
// acc: 누적 객체, student: 현재 학생 객체
// 해당 학년이 acc에 없으면 배열을 생성
if (!acc[grade]) acc[grade] = [];
// 현재 학생을 해당 학년 배열에 추가
acc[grade].push(student);
return acc; // 누적 객체 반환
}, {}); // 초기 누적값은 빈 객체 {}
console.log(groupedByGrade);
// { A: [ {name: '김철수', ...}, {name: '박민수', ...} ], B: [...], C: [...] }
3. 함수 실행 컨텍스트와 this 이해하기
JavaScript에서 this 키워드는 함수가 호출될 때 생성되는 실행 컨텍스트(Execution Context) 내에서 결정되는 특별한 객체입니다.
this는 다른 언어와 달리 정적으로 결정되지 않고, 함수가 어떻게 호출되었는지에 따라 동적으로 결정된다는 점이 가장 중요합니다.
🔷 this란 무엇인가?: 동적으로 결정되는 값
this는 현재 코드가 실행되는 환경(컨텍스트)을 가리킵니다.
▸ 전역 컨텍스트: 함수 밖의 최상위 스코프에서 this는 항상 전역 객체를 가리킵니다.
(브라우저에서는 window, Node.js에서는 global)
▸ 함수 내부 (일반 호출): 기본적으로 전역 객체를 가리키지만, Strict Mode에서는 undefined를 가리킵니다.
▸ 메서드 호출: 객체의 속성으로 호출될 때, 해당 객체 자체를 가리킵니다.
// 전역 컨텍스트
console.log(this); // window (브라우저) 또는 global (Node.js)
// 함수 호출 (일반 함수 호출)
function showThis() {
console.log(this);
}
showThis(); // undefined (strict mode) 또는 window
// 객체 메서드 호출
const obj = {
name: '객체',
showThis: function() {
console.log(this); // obj 객체
}
};
obj.showThis(); // { name: '객체', showThis: [Function: showThis] }
✔️ this 바인딩의 4가지 규칙 (우선순위 순)
JavaScript 엔진은 함수 호출 시 다음 네 가지 규칙에 따라 this를 결정합니다.
🔸 명시적 바인딩 (Explicit Binding)
call, apply, bind 메서드를 사용해 this를 강제로 지정하는 경우입니다.
🔸 new 바인딩 (New Binding)
생성자 함수를 new 키워드와 함께 호출할 때, this는 새로 생성된 객체를 가리킵니다.
🔸 암시적 바인딩 (Implicit Binding)
함수가 객체의 메서드로 호출될 때, this는 해당 객체를 가리킵니다.
🔸 기본 바인딩 (Default Binding)
위의 세 가지 규칙에 해당하지 않는 일반 함수 호출의 경우, this는 전역 객체를 가리킵니다. (Strict Mode에서는 undefined)
🔷 암시적 바인딩: 객체의 메서드로 호출될 때
함수가 객체의 속성으로 정의되어 점(.) 표기법을 통해 호출될 때, this는 해당 객체에 바인딩됩니다.
const user = {
name: '홍길동',
age: 30,
introduce: function() {
// this는 user 객체를 가리키므로, this.name과 this.age는 홍길동과 30이 됩니다.
console.log(`제 이름은 ${this.name}이고, ${this.age}살입니다.`);
},
getInfo: function() {
return {
name: this.name,
age: this.age,
isAdult: this.age >= 18
};
}
};
user.introduce(); // 제 이름은 홍길동이고, 30살입니다.
✔️ 암시적 바인딩 손실 (Implicit Loss)
암시적 바인딩은 함수가 객체에서 분리되어 일반 함수처럼 호출될 때 쉽게 손실됩니다. 이것은 JavaScript에서 흔히 발생하는 버그의 원인이 됩니다.
const user = {
name: '김철수',
greet: function() {
console.log(`안녕하세요, ${this.name}입니다`);
}
};
user.greet(); // "안녕하세요, 김철수입니다" (정상 작동: 암시적 바인딩)
// 함수를 변수에 할당하는 순간 객체와의 연결이 끊어집니다.
const greetFunc = user.greet;
// 이 시점에서 greetFunc는 단순히 독립된 함수가 됩니다.
greetFunc(); // "안녕하세요, undefined입니다" (기본 바인딩: this가 전역 객체를 가리킴)
✔️ 비동기 콜백에서의 this 손실
setTimeout이나 이벤트 리스너의 콜백 함수로 객체의 메서드를 전달할 때도 this 손실이 발생합니다. 이는 콜백 함수가 객체의 메서드가 아닌, 일반 함수로 실행되기 때문입니다.
const button = {
content: '클릭하세요',
click: function() {
console.log(`${this.content}가 클릭되었습니다`);
}
};
// 직접 호출은 정상 작동
button.click(); // "클릭하세요가 클릭되었습니다"
// setTimeout에 전달하면 this 손실 (기본 바인딩)
setTimeout(button.click, 1000); // "undefined가 클릭되었습니다"
// 해결 방법: 화살표 함수 또는 bind 사용
// 화살표 함수로 감싸면, button.click()을 호출하는 시점에 암시적 바인딩이 일어납니다.
setTimeout(() => button.click(), 1000); // 정상 작동
🔷 명시적 바인딩: call, apply, bind를 이용한 this 지정
명시적 바인딩은 함수에 내장된 call(), apply(), bind() 메서드를 사용하여 함수를 호출할 때, 개발자가 직접 this로 사용할 객체를 지정하는 방식입니다.
1. call(): 인자를 개별적으로 전달
▸ call(this객체, 인자1, 인자2, ...)
▸ 첫 번째 인자로 지정된 객체를 this로 사용하여 함수를 즉시 실행합니다. 나머지 인자들은 함수에 개별적으로 전달됩니다.
// call: 인자를 개별적으로 전달
function introduce(greeting, punctuation) {
console.log(`${greeting}, 저는 ${this.name}입니다${punctuation}`);
}
const person1 = { name: '김철수' };
const person2 = { name: '이영희' };
introduce.call(person1, '안녕하세요', '!'); // "안녕하세요, 저는 김철수입니다!"
introduce.call(person2, '반갑습니다', '.'); // "반갑습니다, 저는 이영희입니다."
2. apply(): 인자를 배열로 전달
▸ apply(this객체, [인자1, 인자2, ...]):
▸ call()과 동일하게 함수를 즉시 실행하지만, 함수에 전달할 인자들을 배열(Array) 형태로 전달해야 합니다.
// apply: 인자를 배열로 전달
function sum(a, b, c) {
console.log(`${this.name}의 합계: ${a + b + c}`);
}
const calculator = { name: '계산기' };
sum.apply(calculator, [10, 20, 30]); // "계산기의 합계: 60"
// 실무 활용: Math.max와 함께 사용
// Math.max는 인자를 개별적으로 받기 때문에, 배열의 요소를 풀어 전달할 때 apply가 유용합니다.
const numbers = [5, 10, 15, 3, 8];
// 첫 번째 인자(this)는 Math 객체와 관련 없으므로 null 또는 undefined를 사용합니다.
const max = Math.max.apply(null, numbers);
console.log(max); // 15
3. bind(): 새로운 함수를 반환하여 this를 고정
▸ bind(this객체, 인자1, ...):
▸ 함수를 즉시 실행하지 않고, this 값이 영구적으로 고정된 새로운 함수를 반환합니다. 이 새로운 함수는 나중에 호출될 때 지정된 this를 사용합니다.
// bind: 새로운 함수를 반환 (this가 고정됨)
const module = {
x: 42,
getX: function() {
return this.x;
}
};
const unboundGetX = module.getX;
console.log(unboundGetX()); // undefined (this 손실 - 기본 바인딩)
// module.getX 함수에 module 객체를 this로 영구적으로 고정합니다.
const boundGetX = module.getX.bind(module);
console.log(boundGetX()); // 42 (this가 고정됨)
✔️ 실무 예제: 클래스/이벤트 핸들러에서 bind 활용
클래스 기반 컴포넌트나 이벤트 핸들러를 사용할 때, 메서드를 콜백으로 전달하면 this가 손실되므로 bind를 사용해 this를 인스턴스로 고정하는 것이 일반적입니다.
// 실무 예제: 이벤트 핸들러에서 bind 활용
class Counter {
constructor() {
this.count = 0;
this.button = document.querySelector('#myButton');
// bind를 사용해 this 고정: increment가 호출될 때 this는 Counter 인스턴스가 됩니다.
this.button.addEventListener('click', this.increment.bind(this));
}
increment() {
this.count++;
console.log(`현재 카운트: ${this.count}`);
}
}
// 또는 화살표 함수 사용 (모던 JavaScript의 권장 방식)
class Counter2 {
constructor() {
this.count = 0;
this.button = document.querySelector('#myButton');
// 화살표 함수는 자동으로 상위 스코프(constructor)의 this 바인딩을 상속받습니다.
this.button.addEventListener('click', () => {
this.count++;
console.log(`현재 카운트: ${this.count}`);
});
}
}
4. 고차 함수 및 함수 패턴: 클로저(Closuer)와 콜백(Callback)
🔷 콜백 함수 패턴: 고차 함수의 개념
고차 함수(Higher-Order Function, HOF)는 함수형 프로그래밍의 핵심 개념으로, 다음 중 하나 이상을 수행하는 함수를 말합니다.
▸ 함수를 인자로 받는다. (가장 일반적인 형태)
▸ 함수를 결과로 반환한다.
고차 함수에 전달되는 함수를 콜백 함수(Callback Function)라고 부릅니다. 이 패턴은 코드의 재사용성과 추상화를 높여줍니다.
✔️ 배열 처리의 추상화 (동기 콜백)
콜백 함수는 특정 로직을 유연하게 처리할 수 있도록 추상화 계층을 제공합니다.
// 콜백 함수의 기본 개념
function processArray(arr, callback) {
const result = [];
for (let i = 0; i < arr.length; i++) {
// 배열의 각 요소를 어떻게 처리할지 callback 함수에 위임합니다.
result.push(callback(arr[i]));
}
return result;
}
const numbers = [1, 2, 3, 4, 5];
// n => n * 2 (두 배로 만드는 로직)를 콜백으로 전달
const doubled = processArray(numbers, n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// n => n * n (제곱하는 로직)를 콜백으로 전달
const squared = processArray(numbers, n => n * n);
console.log(squared); // [1, 4, 9, 16, 25]
✔️ 비동기 작업에서의 콜백
콜백 함수는 특정 작업이 완료된 후 실행되도록 약속하는 역할로, 비동기 프로그래밍의 기본입니다.
// 비동기 작업에서의 콜백 (전통적 방식)
function fetchUserData(userId, callback) {
// 1초 후(비동기 작업 완료 후) 콜백 함수를 실행합니다.
setTimeout(() => {
const user = { id: userId, name: '홍길동', email: 'hong@example.com' };
callback(user); // 작업 결과를 콜백에 전달
}, 1000);
}
fetchUserData(123, (user) => {
// 1초 뒤에 이 콜백 함수가 실행됩니다.
console.log('사용자 정보:', user);
});
✔️ 실무 패턴: 에러 우선 콜백 (Error-First Callback)
Node.js 환경에서 널리 사용되는 패턴으로, 콜백 함수의 첫 번째 인자는 항상 에러 객체를 위해 예약됩니다. 이 패턴은 비동기 작업의 성공과 실패 처리를 구조화합니다.
// 실무 패턴: 에러 우선 콜백 (Node.js 스타일)
function readFile(filename, callback) {
setTimeout(() => {
const error = null; // 에러가 없는 경우
const data = '파일 내용입니다';
if (error) {
// 1. 에러가 발생하면 첫 번째 인자에 에러를, 두 번째 인자는 null을 전달
callback(error, null);
} else {
// 2. 성공하면 첫 번째 인자에 null을, 두 번째 인자에 데이터를 전달
callback(null, data);
}
}, 1000);
}
readFile('data.txt', (err, data) => {
// 항상 에러 체크를 먼저 수행합니다.
if (err) {
console.error('에러 발생:', err);
return;
}
console.log('데이터:', data);
});
✔️ 함수를 반환하는 고차 함수
고차 함수는 설정을 기억하고 있는 새로운 함수를 생성하여 반환할 수 있습니다.
// 고차 함수 만들기
function createMultiplier(multiplier) {
// 외부 함수가 종료되어도 이 내부 함수는 multiplier 값을 기억하고 있습니다. (클로저)
return function(number) {
return number * multiplier;
};
}
// multiplier=2가 고정된 새로운 함수
// number * 2 를 반환하는 함수 리턴
const double = createMultiplier(2);
// multiplier=3이 고정된 새로운 함수
// number * 3 을 반환하는 함수 리턴
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
🔷 클로저 (Closure): 렉시컬 환경과 외부 변수 기억
✔️ 클로저의 정의와 동작 원리
클로저는 함수가 선언되었을 때의 환경을 기억하고 있는 함수입니다
▸ 렉시컬 환경 (Lexical Environment): 함수가 어디서 정의되었는지에 따라 결정되는 변수들의 묶음(스코프)입니다.
▸ 핵심 원리: 내부 함수가 외부 함수의 변수를 참조하고 있는 한, 외부 함수가 실행을 마치고 종료되어도, 해당 외부 함수의 변수 환경(Lexical Environment)은 가비지 컬렉터(Garbage Collector)에 의해 제거되지 않고 메모리에 계속 유지됩니다.
▸ 결과: 외부 함수의 변수는 오직 내부 함수(클로저)를 통해서만 접근 가능한 '보존된 상태'가 됩니다.
// 클로저의 기본 개념
function outer() {
const outerVar = '외부 변수입니다.'; // ➊ 외부 함수의 지역 변수
// ➋ inner는 outer의 내부 함수입니다.
function inner() {
console.log(outerVar); // inner는 outerVar를 참조합니다.
}
return inner; // ➌ inner 함수를 반환합니다.
}
let closureFunc = outer();
// outer 함수는 실행을 마쳤지만, 반환된 inner 함수(closureFunc)가 outerVar를 참조하고 있으므로
// outerVar는 메모리에 남아있습니다.
closureFunc(); // "외부 변수" (outerVar에 여전히 접근 가능)
// ➍ 메모리 해제: 이제 closureFunc를 참조하는 곳이 없으므로,
// JavaScript 가비지 컬렉터가 다음에 실행될 때 closureFunc 객체와 함께
// 그 안에 묶여있던 outerVar 메모리 공간을 회수합니다.
closureFunc = null;
이 예시에서 inner는 분명 내부 함수입니다.
하지만 inner가 outer() 밖으로 반환된 후에도 outerVar라는 외부 지역 변수를 계속 접근하고 기억할 수 있을 때, 이 inner 함수를 특별히 클로저라고 부르는 것입니다.
즉, 클로저는 내부 함수가 외부 환경과 엮여서 특별한 능력을 가지게 된 상태라고 이해하는 것이 가장 정확합니다.
✔️ 클로저 변수가 메모리에서 해제되는 시점
closureFunc = null;을 하지 않아도, 클로저 변수가 메모리에서 사라지는 경우가 있습니다.
1. 변수가 선언된 스코프가 종료될 때
closureFunc 변수 자체가 전역(Global) 변수가 아니라 지역(Local) 변수로 선언되었다면, 해당 변수를 감싸는 함수가 종료될 때 closureFunc에 대한 참조가 사라지고, 곧이어 클로저 변수도 해제됩니다.
function runApp() {
let closureFunc = outer(); // ➊ closureFunc는 runApp의 지역 변수
// ... 여기서 closureFunc() 호출
} // ➋ runApp() 실행 종료!
// ➌ runApp() 종료 시 closureFunc 변수에 대한 참조가 사라지므로,
// 가비지 컬렉터가 다음 번에 동작할 때 클로저와 outerVar가 메모리에서 해제됩니다.
2. 명시적으로 참조를 끊을 때
closureFunc가 전역 변수이거나 오랫동안 살아있는 스코프에 있다면, 해당 변수를 null로 설정하여 참조를 끊어야 메모리가 해제됩니다.
let closureFunc = outer(); // 전역 변수
// ... 사용 후
closureFunc = null; // ➎ 명시적으로 참조를 끊으면, GC 대상이 됩니다.
🔷 함수 메모이제이션 (Memoization) : 성능 최적화
메모이제이션(Memoization)은 컴퓨터 과학에서 함수의 실행 결과를 저장(캐시)해두고, 같은 입력(인자)이 들어오면 다시 계산하지 않고 저장된 결과를 즉시 반환하여 성능을 최적화하는 기법입니다.
이 예제는 고차 함수와 클로저를 완벽하게 활용하여 메모이제이션을 구현하는 표준 패턴입니다.
// 함수 메모이제이션 (성능 최적화)
function memoize(fn) {
const cache = {}; // 이 cache 객체를 클로저로 보존합니다.
return function(...args) { // 이 내부 함수가 반환됩니다.
const key = JSON.stringify(args); // 인자를 기반으로 캐시 키 생성
if (cache[key]) {
console.log('캐시에서 반환');
return cache[key];
}
console.log('새로 계산');
const result = fn(...args); // 실제 함수 실행
cache[key] = result;
return result;
};
}
const expensiveCalculation = memoize((n) => {
// 시간이 오래 걸리는 무거운 계산을 시뮬레이션
let sum = 0;
for (let i = 0; i < n; i++) {
sum += i;
}
return sum;
});
console.log(expensiveCalculation(1000000)); // 새로 계산 (시간 소요)
console.log(expensiveCalculation(1000000)); // 캐시에서 반환 (매우 빠름!)
1. memoize 함수는 고차 함수로서, 느린 함수(fn, 여기서는 expensiveCalculation)를 인자로 받고, 캐싱 기능이 추가된 새로운 함수(클로저)를 반환합니다.
▸ cache 객체 : 이 객체는 memoize 함수가 실행될 때 딱 한 번 생성되며, 계산된 결과를 저장하는 "기억 장소" 역할을 합니다.
▸ 반환된 내부 함수 : 이 함수는 외부 함수(memoize)가 종료된 후에도 cache 객체에 대한 참조를 유지하는 클로저입니다.
2. const expensiveCalculation = memoize((n) => { ... }); 코드가 실행되면, memoize 함수는 종료되지만, 반환된 함수(expensiveCalculation)는 cache 객체를 자신의 클로저 스코프 안에 안전하게 보존합니다.
▸ 이 덕분에 cache 객체는 외부에 노출되지 않으면서도 (private), expensiveCalculation 함수가 호출될 때마다 상태를 유지하고 접근할 수 있습니다.
3. 반환된 클로저 함수가 호출될 때마다 다음 로직이 실행됩니다.
const key = JSON.stringify(args); // 인자를 기반으로 캐시 키 생성
if (cache[key]) {
console.log('캐시에서 반환');
return cache[key]; // 👈 캐시 히트: 저장된 값 즉시 반환
}
console.log('새로 계산');
const result = fn(...args); // 👈 캐시 미스: 느린 함수를 실행
cache[key] = result; // 👈 결과를 cache에 저장
return result;
▸ key 생성: 함수에 전달된 인자들(args)을 문자열로 변환하여 고유한 키를 만듭니다. (예: expensiveCalculation(1000000) 호출 시 키는 "\[1000000\]")
▸ 캐시에 있다면 (Hit): 이미 계산한 값이므로, 무거운 fn 실행 없이 바로 값을 반환합니다. (매우 빠름!)
▸ 캐시에 없다면 (Miss): 느린 fn 함수를 실행하여 결과를 얻고, 이 결과를 cache에 저장한 후 반환합니다.
✔️ 클로저 없는 캐시 관리 패턴 : 클래스를 이용한 캐시 관리
캐시 저장소(cache 객체)와 해당 캐시를 사용하는 로직(calculate)을 하나의 클래스 인스턴스 안에 묶어 관리할 수 있습니다. 이는 모던 JavaScript에서 상태(캐시)를 관리하는 매우 흔한 방법입니다
class Memoizer {
constructor(fn) {
this.fn = fn;
this.cache = {}; // 👈 캐시 객체를 인스턴스 속성으로 저장
}
calculate(...args) {
const key = JSON.stringify(args);
if (this.cache[key]) {
console.log('캐시에서 반환 (Class)');
return this.cache[key];
}
console.log('새로 계산 (Class)');
const result = this.fn(...args);
this.cache[key] = result;
return result;
}
// 캐시를 비우는 메서드도 추가 가능
clearCache() {
this.cache = {};
}
}
// 사용할 함수 정의
const heavyFn = (n) => {
let sum = 0;
for (let i = 0; i < n; i++) { sum += i; }
return sum;
};
const calculator = new Memoizer(heavyFn);
console.log(calculator.calculate(1000));
console.log(calculator.calculate(1000)); // 캐시 사용
캐시가 인스턴스 속성으로 존재하므로, calculator 객체가 살아있는 동안 안전하게 유지되며, clearCache 같은 추가적인 관리 메서드를 쉽게 만들 수 있습니다.
✔️ 클로저 vs. 클래스: 성능 관점
▸ 대부분의 실무 코드에서는 클래스를 사용하여 구조화와 가독성을 확보하는 것이 장기적인 관점에서 더 좋은 성능 관리입니다.
▸ 하지만 특정 함수의 실행 비용을 줄이는 마이크로 최적화가 필요할 때는 클로저가 제공하는 메모이제이션 등의 함수형 패턴이 압도적인 성능 이점을 제공합니다.
✔ 마무리
이번 글에서는 함수 선언 방식의 차이에서 시작해, 화살표 함수의 이점과 주의사항, this 바인딩 규칙, 그리고 고차 함수·콜백·클로저 같은 함수형 프로그래밍 핵심 개념까지 살펴보았습니다.
특히, this 바인딩 규칙(명시적/암시적/new/기본)과 화살표 함수의 Lexical this, 그리고 클로저의 동작 원리는 실무에서 버그의 원인이 되거나 성능 최적화 지점으로 이어지는 중요한 개념입니다.
콜백 패턴과 고차 함수, 나아가 메모이제이션 같은 실전 활용 예시는 함수가 단순한 “기능 실행 도구”를 넘어 상태를 은닉하고, 동작을 조합하며, 복잡한 로직을 우아하게 구성하는 핵심 도구임을 보여줍니다.
정리하자면:
▸ 함수 선언 vs 함수 표현식은 호이스팅과 실행 시점에 차이가 있다.
▸ 화살표 함수는 간결하며 this를 상속받아 콜백·비동기 코드에서 매우 유용하다.
▸ this는 호출 방식에 따라 동적으로 결정되며, 명시적 바인딩(call/apply/bind)으로 제어할 수 있다.
▸ 클로저는 외부 변수를 기억하는 함수로, 상태 보존·데이터 은닉·메모이제이션 등에 필수적이다.
▸ 고차 함수와 콜백은 데이터 처리 패턴을 추상화하고 재사용성을 높인다.
▸ 함수는 JavaScript의 중심이며, 제대로 이해할수록 코드의 안정성과 가독성뿐 아니라 유지보수성이 크게 향상됩니다.
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'3.SW개발 > Node.js' 카테고리의 다른 글
| [JavaScript] 5편. 비동기 처리와 이벤트 루프 이해하기 : Promise, async/await, Event Loop (0) | 2025.12.06 |
|---|---|
| [JavaScript] 4편. 클래스(Class) & 프로토타입(Prototype) 이해하기 (1) | 2025.12.06 |
| [JavaScript] 2편. 객체와 배열 다루기 : 구조 분해, Spread/Rest (0) | 2025.12.05 |
| [JavaScript] 1편. 기본 문법 총정리: 변수·타입·스코프·호이스팅 이해하기 (0) | 2025.12.03 |
| Node.js 개발환경 구축하기 : JavaScript, TypeScript, Vitest (0) | 2025.12.03 |