프로그래밍/Javascript

자바스크립트 함수 완전 정복: 표현식, 클로저, 콜백까지 파헤치기

shimdh 2025. 2. 10. 11:53
728x90

1. 함수 표현식: 함수를 값으로 다루는 유연함

함수 표현식은 함수를 변수에 할당하는 방식을 말합니다. 이는 코드의 가독성과 재사용성을 높이며, 특히 고차 함수와 함께 사용할 때 그 진가를 발휘합니다.

1.1. 함수 표현식의 기본: 정의와 형태

  • 정의: 함수 표현식은 function 키워드 뒤에 이름이 없거나(익명 함수), 변수 이름이 앞에 오는 형태로 작성됩니다.
  • 형태:
const myFunction = function() {
  console.log("Hello, World!");
};

myFunction(); // "Hello, World!" 출력

1.2. 익명 함수와 이름 있는 함수: 상황에 따른 선택

  • 익명 함수 (Anonymous Function): 이름이 없는 함수로, 다른 변수에 할당하거나 다른 함수의 인자로 전달할 때 주로 사용됩니다.
const greet = function(name) {
  console.log(`Hello, ${name}!`);
};

greet("John"); // "Hello, John!" 출력

// 예제: 배열의 forEach 메서드에 익명 함수 사용
const numbers = [1, 2, 3];
numbers.forEach(function(number) {
  console.log(number * 2); // 2, 4, 6 출력
});
  • 이름 있는 함수 (Named Function): 함수 표현식에 이름을 붙일 수도 있습니다. 이는 재귀 호출이나 디버깅 시 유용하게 활용됩니다.
const factorial = function calculateFactorial(n) {
  if (n === 0) {
    return 1;
  } else {
    return n * calculateFactorial(n - 1); // 재귀 호출
  }
};

console.log(factorial(5)); // 120 출력

// 예제: 피보나치 수열 계산 함수
const fibonacci = function calculateFibonacci(n) {
  if (n <= 1) {
    return n;
  }
  return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);
};

console.log(fibonacci(10)); // 55 출력

1.3. 즉시 실행 함수 표현식 (IIFE): 스코프 보호와 모듈화

IIFE (Immediately Invoked Function Expression)는 정의되자마자 즉시 실행되는 함수를 말합니다. 전역 스코프 오염을 방지하고, 모듈화를 위해 자주 사용되는 중요한 패턴입니다.

(function() {
  console.log("This runs immediately!");
})(); // "This runs immediately!" 출력

// 예제: IIFE를 사용한 변수 스코프 제한
(function() {
  const message = "Hello from IIFE!";
  console.log(message); // "Hello from IIFE!" 출력
})();

// console.log(message); // ReferenceError: message is not defined (message는 IIFE 내부에서만 유효)

1.4. 함수 표현식을 활용한 실용 예제: 계산기 객체 만들기

함수 표현식을 활용하여 간단하면서도 확장성 있는 계산기 객체를 만들어 보겠습니다.

// 덧셈 함수
const add = function(a, b) {
  return a + b;
};

// 뺄셈 함수
const subtract = function(a, b) {
  return a - b;
};

// 곱셈 함수
const multiply = function(a, b) {
  return a * b;
};

// 나눗셈 함수
const divide = function(a, b) {
  if (b === 0) {
    return "Division by zero is not allowed.";
  }
  return a / b;
};

// 나머지 연산 함수
const modulo = function(a, b) {
  if (b === 0) {
    return "Modulo by zero is not allowed";
  }
  return a % b;
}

// 계산기 객체
const calculator = {
  add,        // 더하기
  subtract,   // 빼기
  multiply,   // 곱하기
  divide,     // 나누기
  modulo      // 나머지
};

// 사용 예시
console.log(calculator.add(5, 3));        // 8 출력
console.log(calculator.subtract(10, 4));   // 6 출력
console.log(calculator.multiply(2, 6));   // 12 출력
console.log(calculator.divide(8, 2));     // 4 출력
console.log(calculator.divide(8, 0));     // "Division by zero is not allowed." 출력
console.log(calculator.modulo(10, 3));    // 1 출력

위 예제에서는 add, subtract, multiply, divide, modulo 다섯 가지 연산을 수행하는 계산기 객체를 함수 표현식을 사용하여 구현했습니다. 각 연산은 독립적인 함수로 구현되어 있어 필요에 따라 쉽게 기능을 확장할 수 있습니다.


2. 클로저: 데이터를 품은 함수로 강력한 기능 구현하기

클로저는 함수와 그 함수가 선언된 렉시컬 환경(lexical environment)의 조합입니다. 내부 함수가 외부 함수의 변수에 접근할 수 있도록 해주며, 이를 통해 데이터 은닉, 상태 유지 등 객체 지향 프로그래밍의 핵심 기능들을 구현할 수 있습니다.

2.1. 클로저의 정의: 함수, 스코프, 그리고 렉시컬 환경

  • 함수 (Function): 자바스크립트에서 함수는 자신의 스코프를 가집니다.
  • 스코프 (Scope): 변수의 유효 범위를 말하며, 내부 함수는 외부 함수의 변수에 접근할 수 있습니다.
  • 클로저 (Closure): 외부 함수의 실행이 종료된 후에도 내부 함수를 통해 외부 변수에 접근할 수 있는 기능입니다.

2.2. 클로저의 동작 원리: 스코프 체이닝 이해하기

  1. 외부 함수가 호출되면 내부 함수가 생성됩니다.
  2. 내부 함수는 외부 함수의 변수를 기억합니다. (스코프 체이닝)
  3. 외부 함수의 실행이 종료되어도, 내부 함수를 통해 외부 변수에 접근할 수 있습니다.

2.3. 클로저 예제: 카운터 함수 만들기

function createCounter() {
  let count = 0; // 외부 함수 변수

  return function() { // 내부 함수 (클로저)
    count += 1;
    return count;
  };
}

const counter = createCounter();

console.log(counter()); // 1 출력
console.log(counter()); // 2 출력
console.log(counter()); // 3 출력

// 예제: 스텝(step)을 설정할 수 있는 카운터
function createStepCounter(step) {
  let count = 0;
  return function() {
    count += step;
    return count;
  };
}

const stepCounter = createStepCounter(2);
console.log(stepCounter()); // 2 출력
console.log(stepCounter()); // 4 출력

createCounter 함수는 count 변수를 갖는 외부 함수이고, count를 증가시키고 반환하는 익명 함수(내부 함수)를 반환합니다. counter 변수에 createCounter의 반환 값, 즉 내부 함수를 할당합니다. counter 함수를 호출할 때마다 count 값이 증가하고, 그 값이 반환됩니다. createCounter 함수의 실행이 종료된 후에도 counter 함수는 count 변수에 계속 접근할 수 있습니다. 이것이 바로 클로저의 핵심입니다. createStepCounter 함수는 인자로 step을 받아서 카운트 증가폭을 조절할 수 있도록 확장한 버전입니다.

2.4. 클로저 활용: 데이터 은닉과 상태 유지

  • 데이터 은닉 (Data Hiding): 클로저를 통해 특정 데이터를 외부로부터 보호하고, 정해진 메서드를 통해서만 접근하도록 제어할 수 있습니다.
function secretHolder(secret) {
  return {
    getSecret: function() {
      return secret;
    },
    setSecret: function(newSecret) {
      secret = newSecret;
    }
  };
}

const mySecret = secretHolder("My secret password");
console.log(mySecret.getSecret()); // "My secret password" 출력
mySecret.setSecret("New secret password");
console.log(mySecret.getSecret()); // "New secret password" 출력

// 예제: 비밀번호 확인 기능 추가
function enhancedSecretHolder(secret, password) {
  let attempts = 0;

  return {
    getSecret: function(enteredPassword) {
      attempts++;
      if (enteredPassword === password) {
        console.log(`Accessed after ${attempts} attempts.`);
        return secret;
      } else {
        console.log("Incorrect password!");
        return null;
      }
    },
    setSecret: function(newSecret, enteredPassword) {
      if (enteredPassword === password) {
        secret = newSecret;
        console.log("Secret updated successfully.");
      } else {
        console.log("Incorrect password! Cannot update secret.");
      }
    }
  };
}

const secureSecret = enhancedSecretHolder("Highly confidential", "qwerty");
console.log(secureSecret.getSecret("wrongpassword")); // "Incorrect password!" 출력, null 반환
console.log(secureSecret.getSecret("qwerty")); // "Accessed after 2 attempts." 출력, "Highly confidential" 반환
secureSecret.setSecret("New top secret", "qwerty"); // "Secret updated successfully." 출력
console.log(secureSecret.getSecret("qwerty")); // "Accessed after 3 attempts." 출력, "New top secret" 반환

secretHolder 함수는 secret 변수를 캡슐화하고, getSecretsetSecret 메서드를 통해서만 secret에 접근하도록 제한합니다. enhancedSecretHolder 함수는 비밀번호를 입력해야만 기밀 정보(secret)에 접근하거나 수정할 수 있도록 보안을 강화했습니다.

  • 상태 유지 (State Preservation): 클로저를 사용하여 함수 호출 간에 상태를 유지하고 관리할 수 있습니다.
function makeBankAccount(initialBalance) {
  let balance = initialBalance;

  return {
    deposit: function(amount) {
      balance += amount;
      console.log(`Deposited ${amount}, new balance is ${balance}`);
    },
    withdraw: function(amount) {
      if (amount <= balance) {
        balance -= amount;
        console.log(`Withdrew ${amount}, new balance is ${balance}`);
      } else {
        console.log("Insufficient funds!");
      }
    },
    getBalance: function() {
      return balance;
    }
  };
}

const account = makeBankAccount(100);
account.deposit(50);      // "Deposited 50, new balance is 150" 출력
account.withdraw(30);     // "Withdrew 30, new balance is 120" 출력
console.log(account.getBalance());   // 120 출력

// 예제: 거래 내역 기록 기능 추가
function makeAdvancedBankAccount(initialBalance) {
  let balance = initialBalance;
  let transactionHistory = [];

  return {
    deposit: function(amount) {
      balance += amount;
      transactionHistory.push(`Deposited ${amount}`);
      console.log(`Deposited ${amount}, new balance is ${balance}`);
    },
    withdraw: function(amount) {
      if (amount <= balance) {
        balance -= amount;
        transactionHistory.push(`Withdrew ${amount}`);
        console.log(`Withdrew ${amount}, new balance is ${balance}`);
      } else {
        console.log("Insufficient funds!");
      }
    },
    getBalance: function() {
      return balance;
    },
    getHistory: function() {
      return transactionHistory;
    }
  };
}

const advancedAccount = makeAdvancedBankAccount(500);
advancedAccount.deposit(100); // "Deposited 100, new balance is 600" 출력
advancedAccount.withdraw(50); // "Withdrew 50, new balance is 550" 출력
console.log(advancedAccount.getHistory()); // ["Deposited 100", "Withdrew 50"] 출력

makeBankAccount 함수는 balance 변수를 클로저 내부에 유지하여 계좌 잔액 상태를 관리합니다. makeAdvancedBankAccount 함수는 거래 내역을 transactionHistory 배열에 저장하여 계좌 거래 내역을 추적할 수 있도록 기능을 확장했습니다.


3. 콜백 함수: 비동기 처리와 이벤트 핸들링의 핵심

콜백 함수는 다른 함수의 인자로 전달되어 특정 시점에 실행되는 함수입니다. 비동기 처리, 이벤트 핸들링 등에서 중추적인 역할을 하며, 자바스크립트의 유연성과 반응성을 극대화하는 핵심 요소입니다.

3.1. 콜백 함수의 개념: 정의와 용도

  • 정의: 콜백 함수는 특정 작업이 완료된 후 호출되는 함수를 말합니다.
  • 용도: 주로 비동기 작업, 이벤트 리스너, 배열 메서드 등에서 활용됩니다.

3.2. 콜백 함수 예제: 비동기 처리, 이벤트 처리, 배열 메서드 활용

3.2.1. 비동기 처리: setTimeout을 활용한 데이터 가져오기

setTimeout 함수는 일정 시간 지연 후 콜백 함수를 실행합니다. 이를 활용하여 비동기 데이터 로딩을 모방해 보겠습니다.

function fetchData(callback) {
  setTimeout(function() {
    const data = { message: "Data fetched successfully!" };
    callback(data); // 데이터 로딩 완료 후 콜백 함수 실행
  }, 2000); // 2초 후 실행
}

function handleData(data) {
  console.log(data.message);
}

fetchData(handleData); // 2초 후 "Data fetched successfully!" 출력

// 예제: 에러 처리 기능 추가
function fetchDataWithErrorHandling(callback, errorCallback) {
  setTimeout(function() {
    const success = Math.random() < 0.8; // 80% 확률로 성공
    if (success) {
      const data = { message: "Data fetched successfully!" };
      callback(data);
    } else {
      const error = { message: "Error fetching data!" };
      errorCallback(error);
    }
  }, 2000);
}

function handleError(error) {
  console.error(error.message);
}

fetchDataWithErrorHandling(handleData, handleError);

fetchData 함수는 2초 후에 callback 함수를 호출하여 데이터를 전달합니다. handleData 함수는 fetchData에 전달된 콜백 함수로, 데이터를 받아 처리합니다. fetchDataWithErrorHandling 함수는 성공/실패 여부에 따라 callback 또는 errorCallback을 호출하여 에러 처리를 추가했습니다.

3.2.2. 이벤트 처리: 버튼 클릭 이벤트 핸들링

웹 페이지에서 발생하는 사용자 이벤트(예: 버튼 클릭)를 처리하기 위해 콜백 함수를 사용합니다.

<button id="myButton">Click me!</button>

<script>
  document.getElementById("myButton").addEventListener("click", function() {
    alert("Button clicked!");
  });

  // 예제: 더블 클릭 이벤트 처리
  document.getElementById("myButton").addEventListener("dblclick", function() {
    alert("Button double-clicked!");
  });
</script>

addEventListener 메서드를 사용하여 버튼에 click 이벤트 리스너를 등록하고, 콜백 함수를 연결합니다. 사용자가 버튼을 클릭하면 콜백 함수가 실행되어 "Button clicked!" 알림이 뜹니다. dblclick 이벤트 리스너를 추가하여 더블 클릭 이벤트도 처리하도록 확장했습니다.

3.2.3. 배열 메서드: map, filter, reduce를 활용한 데이터 가공

자바스크립트 배열은 map, filter, reduce와 같은 유용한 메서드를 제공하며, 이들은 모두 콜백 함수를 인자로 받아 배열 데이터를 효과적으로 가공합니다.

const numbers = [1, 2, 3, 4];

// map: 각 요소를 2배로 만들기
const doubled = numbers.map(function(num) {
  return num * 2;
});

console.log(doubled); // [2, 4, 6, 8] 출력

// filter: 짝수만 추출하기
const evens = numbers.filter(function(num) {
  return num % 2 === 0;
});

console.log(evens); // [2, 4] 출력

// reduce: 배열 요소의 합계 계산하기
const sum = numbers.reduce(function(accumulator, currentValue) {
  return accumulator + currentValue;
}, 0); // 초기값 0

console.log(sum); // 10 출력
  • map 메서드는 배열의 각 요소에 대해 콜백 함수를 실행하고, 그 결과들을 모아 새로운 배열을 만듭니다.
  • filter 메서드는 콜백 함수가 true를 반환하는 요소들만 모아 새로운 배열을 만듭니다.
  • reduce 메서드는 배열의 각 요소에 대해 콜백 함수를 실행하여 하나의 결과값을 만듭니다.

3.3. 콜백 지옥 (Callback Hell)과 해결 방안: Promise와 async/await

콜백 함수를 중첩해서 사용하면 코드가 복잡해지고 가독성이 떨어지는 "콜백 지옥" 현상이 발생할 수 있습니다.

doSomething(function(result1) {
  doSomethingElse(result1, function(result2) {
    doThirdThing(result2, function(result3) {
      console.log('All done:', result3);
    });
  });
});

이러한 문제는 Promiseasync/await와 같은 최신 자바스크립트 기능을 사용하여 해결할 수 있습니다. 이들은 비동기 코드를 보다 간결하고 가독성 좋게 작성하도록 돕는 강력한 도구입니다.


결론: 함수 표현식, 클로저, 콜백 함수 정복으로 자바스크립트 마스터로!

본 포스트에서는 자바스크립트 함수의 핵심 개념인 함수 표현식, 클로저, 콜백 함수에 대해 깊이 있게 탐구했습니다. 이들은 자바스크립트의 강력한 기능을 뒷받침하는 근간이며, 효율적이고 유연한 코드를 작성하는 데 필수적입니다.

  • 함수 표현식은 함수를 값으로 다루어 코드의 가독성과 재사용성을 높입니다.
  • 클로저는 데이터 은닉과 상태 유지를 가능하게 하여, 객체 지향 프로그래밍과 모듈화를 지원합니다.
  • 콜백 함수는 비동기 처리와 이벤트 핸들링을 위한 핵심 도구입니다.

이러한 개념들을 깊이 이해하고 활용하면, 더욱 강력하고 효율적인 자바스크립트 코드를 작성할 수 있습니다. 특히, 클로저와 콜백 함수는 처음에는 다소 헷갈릴 수 있지만, 다양한 예제를 통해 반복적으로 연습하면 금방 익숙해질 수 있습니다. 본 포스트가 여러분의 자바스크립트 함수 마스터를 향한 여정에 든든한 길잡이가 되었기를 바랍니다!

728x90