함수 인자 처리 (Handling Function Arguments)

함수 인자 처리 (Handling Function Arguments)

자바스크립트(JavaScript)에서 함수 인자 처리(Handling Function Arguments)는 함수가 입력값을 어떻게 받아 처리하는지를 정의한다. 다양한 인자 패턴을 통해 유연성과 가독성을 높일 수 있다. 함수 인자 처리(Handling Function Arguments)의 정의, 동작 방식, 일반 함수와의 차이점, 그리고 사용 사례를 예제로 확인한다.


함수 인자 처리(Handling Function Arguments)는 기본값(Default Parameters), 나머지 매개변수(Rest Parameters), 스프레드 연산자(Spread Operator) 등을 활용한다. ES6에서 도입된 기능들이 포함된다.


함수 인자 처리(Handling Function Arguments)란 무엇인가?

함수 인자 처리(Handling Function Arguments)는 함수가 전달받은 인자를 관리하는 방식을 의미한다. 기본적인 예제를 살펴보자:

function greet(name = "Guest") {
    return `안녕, ${name}`;
}
console.log(greet()); // 안녕, Guest
console.log(greet("철수")); // 안녕, 철수

name에 기본값(Default Parameter)이 설정되어 인자가 없어도 동작한다. 나머지 매개변수(Rest Parameters)도 있다:

function sum(...numbers) {
    return numbers.reduce((acc, curr) => acc + curr, 0);
}
console.log(sum(1, 2, 3)); // 6

함수 인자 처리(Handling Function Arguments)의 동작 방식

함수 인자 처리(Handling Function Arguments)는 인자의 개수와 유형에 따라 동작을 조정한다. ES6 이전에는 arguments 객체를 사용했으나, 이제는 더 간결한 방법이 있다.


예제를 통해 동작 방식을 확인한다:

function combine(first, ...rest) {
    return [first, ...rest];
}
console.log(combine(1, 2, 3, 4)); // [1, 2, 3, 4]

rest는 첫 번째 인자를 제외한 나머지를 배열로 수집한다.


함수 인자 처리(Handling Function Arguments)와 일반 함수의 차이점

1. 인자 관리

- 일반 함수: arguments 객체 사용.

function oldSum() {
    return Array.from(arguments).reduce((a, b) => a + b);
}
console.log(oldSum(1, 2, 3)); // 6

- 인자 처리: rest로 대체.

function newSum(...args) {
    return args.reduce((a, b) => a + b);
}
console.log(newSum(1, 2, 3)); // 6

2. 기본값

- 일반 함수: 수동 설정 필요.

- 인자 처리: 기본값 제공.


사용 사례

함수 인자 처리(Handling Function Arguments)의 활용 사례를 예제로 확인한다.


1. 기본값 설정

function log(message = "없음") {
    console.log(message);
}
log(); // 없음

2. 가변 인자

function join(separator, ...words) {
    return words.join(separator);
}
console.log(join("-", "a", "b", "c")); // a-b-c

3. 스프레드 사용

function apply(fn, ...args) {
    return fn(...args);
}
console.log(apply(Math.max, 1, 5, 3)); // 5

4. 객체 인자

function config({ name = "익명", age = 0 } = {}) {
    return `${name}, ${age}`;
}
console.log(config({ name: "철수" })); // 철수, 0

5. 혼합 인자

function mix(first, second, ...rest) {
    return [first, second, rest];
}
console.log(mix(1, 2, 3, 4)); // [1, 2, [3, 4]]

6. 배열 확장

const arr = [1, 2];
function extend(...items) {
    return [...arr, ...items];
}
console.log(extend(3, 4)); // [1, 2, 3, 4]

7. 필터링

function filterArgs(...args) {
    return args.filter(x => x > 0);
}
console.log(filterArgs(-1, 2, 0, 3)); // [2, 3]

8. 동적 호출

function dynamic(fn, ...args) {
    return fn(...args);
}
console.log(dynamic((a, b) => a * b, 2, 3)); // 6

성능과 한계

장점

- 유연성: 다양한 인자 패턴을 지원한다.

- 간결함: arguments보다 명확하다.


한계

- 화살표 함수: rest는 가능하나 arguments는 없다.

- 오버헤드: 큰 배열 처리 시 성능 저하 가능성 있다.

인자의 개수가 많을 경우 성능을 고려해 설계한다.


마무리

함수 인자 처리(Handling Function Arguments)는 자바스크립트(JavaScript)에서 함수의 입력을 관리하는 유용한 방법이다. 사용 사례를 통해 그 활용 방식을 살펴봤다.


비동기/대기 (Async/Await)

비동기/대기 (Async/Await)

자바스크립트(JavaScript)에서 비동기/대기(Async/Await)는 프로미스(Promises)를 기반으로 비동기 작업(Asynchronous Operations)을 처리하는 문법이다. 비동기 코드를 동기 코드처럼 작성할 수 있게 하여 가독성을 높인다. 비동기/대기(Async/Await)의 정의, 동작 방식, 프로미스(Promises)와의 차이점, 그리고 사용 사례를 예제를 통해 확인한다.


비동기/대기(Async/Await)는 프로미스(Promises)의 syntactic sugar로, 비동기 프로그래밍(Asynchronous Programming)을 간소화한다. 자바스크립트(JavaScript)의 이벤트 루프(Event Loop)와 결합하여 비동기 작업의 흐름을 제어한다.


비동기/대기(Async/Await)란 무엇인가?

비동기/대기(Async/Await)는 asyncawait 키워드를 사용해 비동기 작업(Asynchronous Operations)을 처리한다. async는 함수가 프로미스(Promises)를 반환하도록 정의하며, await는 프로미스(Promises)가 해결(Resolved)될 때까지 실행을 일시 중지한다. 기본적인 예제를 살펴보자:

async function fetchData() {
 return "데이터";
}
fetchData().then(data => console.log(data)); // 데이터

async 함수는 항상 프로미스(Promises)를 반환한다. await를 사용한 예제는 다음과 같다:

async function delayedMessage() {
 const promise = new Promise(resolve => setTimeout(() => resolve("완료"), 1000));
 const result = await promise;
 console.log(result);
}
delayedMessage(); // 1초 후 "완료"

await는 프로미스(Promises)가 이행(Fulfilled)되기를 기다린 후 결과를 반환한다. awaitasync 함수 내에서만 사용 가능하다.


비동기/대기(Async/Await)의 동작 방식

비동기/대기(Async/Await)는 프로미스(Promises)를 기반으로 작동하며, 비동기 작업을 순차적으로 처리한다. await는 코드 실행을 일시 중지하고, 프로미스(Promises)가 완료되면 값을 반환한다.


동작 방식을 예제로 확인한다:

async function sequence() {
 const first = await new Promise(resolve => setTimeout(() => resolve(1), 1000));
 const second = await new Promise(resolve => setTimeout(() => resolve(first + 1), 1000));
 console.log(second);
}
sequence(); // 2초 후 2

위 코드에서 await는 각 프로미스(Promises)가 해결될 때까지 기다리며, 순차적으로 값을 처리한다. 에러 처리를 포함한 예제는 다음과 같다:

async function withError() {
 try {
 const result = await new Promise((_, reject) => setTimeout(() => reject("오류"), 1000));
 console.log(result);
 } catch (error) {
 console.log(error);
 }
}
withError(); // 1초 후 "오류"

try/catch 블록은 await로 발생한 에러를 처리한다.


비동기/대기(Async/Await)와 프로미스(Promises)의 차이점

비동기/대기(Async/Await)는 프로미스(Promises)와 비동기 코드 작성 방식에서 차이가 있다.


1. 코드 구조

- 프로미스: then 체인을 사용한다.

function fetchPromise() {
 return new Promise(resolve => setTimeout(() => resolve("데이터"), 1000))
 .then(data => console.log(data));
}
fetchPromise(); // 1초 후 "데이터"

- 비동기/대기: 동기처럼 작성한다.

async function fetchAsync() {
 const data = await new Promise(resolve => setTimeout(() => resolve("데이터"), 1000));
 console.log(data);
}
fetchAsync(); // 1초 후 "데이터"

2. 에러 처리

- 프로미스: catch로 처리한다.

new Promise((_, reject) => setTimeout(() => reject("실패"), 1000))
 .catch(err => console.log(err)); // 1초 후 "실패"

- 비동기/대기: try/catch를 사용한다.

async function handleError() {
 try {
 await new Promise((_, reject) => setTimeout(() => reject("실패"), 1000));
 } catch (err) {
 console.log(err);
 }
}
handleError(); // 1초 후 "실패"

사용 사례

비동기/대기(Async/Await)의 활용 사례를 예제를 통해 확인한다.


1. 데이터 요청

async function getUser(id) {
 const response = await new Promise(resolve => setTimeout(() => resolve({ id, name: "철수" }), 1000));
 console.log(response.name);
}
getUser(1); // 1초 후 "철수"

2. 순차 처리

async function steps() {
 const step1 = await new Promise(resolve => setTimeout(() => resolve("1단계"), 1000));
 const step2 = await new Promise(resolve => setTimeout(() => resolve(`${step1}, 2단계`), 1000));
 console.log(step2);
}
steps(); // 2초 후 "1단계, 2단계"

3. 병렬 요청

async function parallel() {
 const [result1, result2] = await Promise.all([
 new Promise(resolve => setTimeout(() => resolve("첫 번째"), 1000)),
 new Promise(resolve => setTimeout(() => resolve("두 번째"), 2000))
 ]);
 console.log(result1, result2);
}
parallel(); // 2초 후 "첫 번째 두 번째"

4. 에러 복구

async function recover() {
 try {
 await new Promise((_, reject) => setTimeout(() => reject("문제"), 1000));
 } catch (err) {
 console.log("복구됨");
 }
}
recover(); // 1초 후 "복구됨"

5. 반복 요청

async function fetchMultiple() {
 const ids = [1, 2, 3];
 for (const id of ids) {
 const data = await new Promise(resolve => setTimeout(() => resolve(id * 2), 1000));
 console.log(data);
 }
}
fetchMultiple(); // 1초 간격으로 2, 4, 6

6. 조건부 실행

async function conditionalFetch(flag) {
 if (flag) {
 const result = await new Promise(resolve => setTimeout(() => resolve("참"), 1000));
 console.log(result);
 } else {
 console.log("거짓");
 }
}
conditionalFetch(true); // 1초 후 "참"

7. 타임아웃 처리

async function withTimeout() {
 const promise = new Promise(resolve => setTimeout(() => resolve("늦음"), 2000));
 const timeout = new Promise((_, reject) => setTimeout(() => reject("시간 초과"), 1000));
 const result = await Promise.race([promise, timeout]);
 console.log(result);
}
withTimeout().catch(err => console.log(err)); // 1초 후 "시간 초과"

8. 외부 API 호출

async function fetchApi() {
 try {
 const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
 const data = await response.json();
 console.log(data.title);
 } catch (err) {
 console.log("API 오류");
 }
}
fetchApi(); // 실제 API 응답에 따라 출력

성능과 한계

장점

- 가독성: 비동기 코드를 동기처럼 작성한다.

- 에러 처리: try/catch로 통합된다.

- 흐름 제어: 작업 순서를 명확히 한다.


한계

- 병렬성: 순차 실행이 기본이므로 병렬 처리를 별도로 구현해야 한다.

- 오용: await를 남용하면 성능이 저하될 수 있다.

불필요한 await 사용을 피하고, 병렬 작업에는 Promise.all을 고려한다.


마무리

비동기/대기(Async/Await)는 자바스크립트(JavaScript)에서 비동기 작업(Asynchronous Operations)을 처리하는 유용한 문법이다. 사용 사례를 통해 그 활용 방식을 살펴봤다.


프로미스 (Promises)

프로미스 (Promises)

자바스크립트(JavaScript)에서 프로미스(Promises)는 비동기 작업(Asynchronous Operations)을 처리하는 객체(Object)이다. 비동기 작업의 성공 또는 실패를 나타내며, 콜백(Callback) 함수를 대체하여 코드를 더 간결하게 만든다. 프로미스(Promises)의 정의, 동작 방식, 일반 함수와의 차이점, 그리고 사용 사례를 예제를 통해 확인한다.


프로미스(Promises)는 비동기 작업의 결과를 캡슐화하며, 자바스크립트(JavaScript)의 이벤트 루프(Event Loop)와 밀접하게 연관된다. 비동기 프로그래밍(Asynchronous Programming)을 단순화하고, 에러 처리(Error Handling)를 개선한다.


프로미스(Promises)란 무엇인가?

프로미스(Promises)는 비동기 작업의 상태를 나타내는 객체(Object)로, 세 가지 상태를 가진다: 대기(Pending), 이행(Fulfilled), 거부(Rejected). 작업이 완료되면 이행(Fulfilled) 또는 거부(Rejected) 상태로 전환된다. 기본적인 예제를 살펴보자 :

const promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("성공"), 1000);
});
promise.then(result => console.log(result)); // 1초 후 "성공"

위 코드에서 Promise 생성자는 실행자 함수(Executor Function)를 인자로 받는다. 이 함수는 resolvereject를 호출하여 프로미스(Promises)의 상태를 변경한다. then 메서드는 이행(Fulfilled) 상태일 때 실행된다.


에러가 발생하는 경우도 있다:

const errorPromise = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error("실패")), 1000);
});
errorPromise.catch(err => console.log(err.message)); // 1초 후 "실패"

catch 메서드는 거부(Rejected) 상태일 때 실행된다. 프로미스(Promises)는 비동기 작업의 흐름을 체인(Chain) 형태로 연결할 수 있다:

new Promise(resolve => resolve(2))
    .then(num => num * 3)
    .then(num => console.log(num)); // 6

프로미스(Promises)의 동작 방식

프로미스(Promises)는 비동기 작업을 처리하며, 상태 전환과 결과 처리를 관리한다. 내부적으로 마이크로태스크 큐(Microtask Queue)를 활용하여 비동기 작업을 순차적으로 실행한다.


프로미스(Promises)의 체인 동작을 확인:

new Promise(resolve => setTimeout(() => resolve(1), 1000))
    .then(num => {
        console.log(num);
        return num + 1;
    })
    .then(num => console.log(num)); // 1초 후 1, 2

then 메서드는 새로운 프로미스(Promises)를 반환하며, 값을 전달한다. 에러 처리가 포함된 예제도 있다:

new Promise((resolve, reject) => reject("오류"))
    .then(() => console.log("성공"))
    .catch(err => {
        console.log(err);
        return "복구";
    })
    .then(result => console.log(result)); // 오류, 복구

프로미스(Promises)는 에러를 catch로 잡아 복구할 수 있으며, 체인을 계속 이어갈 수 있다.


프로미스(Promises)와 일반 함수의 차이점

프로미스(Promises)는 일반 함수와 비동기 처리 방식에서 차이가 있다.


1. 비동기 처리

- 일반 함수: 콜백(Callback)을 사용한다.

function fetchData(callback) {
    setTimeout(() => callback("데이터"), 1000);
}
fetchData(data => console.log(data)); // 1초 후 "데이터"

- 프로미스: 체인 형태로 처리한다.

function fetchDataPromise() {
    return new Promise(resolve => setTimeout(() => resolve("데이터"), 1000));
}
fetchDataPromise().then(data => console.log(data)); // 1초 후 "데이터"

2. 에러 처리

- 일반 함수: 별도의 콜백(Callback)이 필요하다.

function fetchWithError(callback, errorCallback) {
    setTimeout(() => errorCallback("에러"), 1000);
}
fetchWithError(() => {}, err => console.log(err)); // 1초 후 "에러"

- 프로미스: catch로 통합한다.

function fetchPromiseWithError() {
    return new Promise((_, reject) => setTimeout(() => reject("에러"), 1000));
}
fetchPromiseWithError().catch(err => console.log(err)); // 1초 후 "에러"

사용 사례

프로미스(Promises)의 활용 사례를 예제를 통해 확인해보자.


1. 데이터 가져오기

function getUser(id) {
    return new Promise(resolve => {
        setTimeout(() => resolve({ id, name: "철수" }), 1000);
    });
}
getUser(1).then(user => console.log(user.name)); // 1초 후 "철수"

2. 순차적 실행

function step1() {
    return new Promise(resolve => setTimeout(() => resolve("1단계"), 1000));
}
function step2(data) {
    return new Promise(resolve => setTimeout(() => resolve(`${data}, 2단계`), 1000));
}
step1().then(step2).then(result => console.log(result)); // 2초 후 "1단계, 2단계"

3. 병렬 실행

const promise1 = new Promise(resolve => setTimeout(() => resolve("첫 번째"), 1000));
const promise2 = new Promise(resolve => setTimeout(() => resolve("두 번째"), 2000));
Promise.all([promise1, promise2]).then(results => console.log(results)); // 2초 후 ["첫 번째", "두 번째"]

4. 경쟁 실행

const fast = new Promise(resolve => setTimeout(() => resolve("빠름"), 500));
const slow = new Promise(resolve => setTimeout(() => resolve("느림"), 1000));
Promise.race([fast, slow]).then(result => console.log(result)); // 0.5초 후 "빠름"

5. 에러 복구

function riskyOperation() {
    return new Promise((_, reject) => setTimeout(() => reject("문제"), 1000));
}
riskyOperation()
    .catch(err => "대체 값")
    .then(result => console.log(result)); // 1초 후 "대체 값"

6. 지연 로딩

function loadImage(src) {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => resolve("이미지 로드 완료");
        img.onerror = () => reject("로드 실패");
        img.src = src;
    });
}
loadImage("example.jpg").then(console.log).catch(console.log);

7. 재시도 로직

function retryOperation(attempts) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() > 0.3) resolve("성공");
            else reject("실패");
        }, 1000);
    }).catch(() => attempts > 1 ? retryOperation(attempts - 1) : "최종 실패");
}
retryOperation(3).then(console.log).catch(console.log);

8. 연속 요청

function fetchStep(id) {
    return new Promise(resolve => setTimeout(() => resolve(id + 1), 1000));
}
fetchStep(1)
    .then(id => fetchStep(id))
    .then(id => fetchStep(id))
    .then(id => console.log(id)); // 3초 후 4

성능과 한계

장점

- 간결함: 콜백(Callback) 중첩을 피한다.

- 에러 처리: 통합된 에러 처리가 가능하다.

- 체인: 비동기 작업을 순차적으로 연결한다.


한계

- 복잡성: 과도한 체인은 가독성을 떨어뜨릴 수 있다.

- 상태 관리: 대기(Pending) 상태를 직접 취소할 수 없다.

단순한 비동기 작업에는 프로미스(Promises)를 사용하고, 복잡한 경우에는 상태 관리를 보완한다.


마무리

프로미스(Promises)는 자바스크립트(JavaScript)에서 비동기 작업(Asynchronous Operations)을 처리하는 유용한 메커니즘이다. 사용 사례를 통해 그 활용 방식을 살펴봤다.


클로저 (Closures)

클로저 (Closures)

자바스크립트(JavaScript)에서 클로저(Closures)는 함수(Function)가 외부 스코프(Scope)의 변수에 접근할 수 있는 메커니즘이다. 함수가 생성된 환경을 기억하여, 해당 변수에 대한 참조를 유지한다. 클로저(Closures)는 데이터 은닉(Data Privacy)과 상태 유지(State Persistence)를 구현하는 데 유용하다. 클로저(Closures)의 정의, 동작 방식, 일반 함수와의 차이점, 그리고 사용 사례를 예제를 통해 확인한다.


클로저(Closures)는 자바스크립트(JavaScript)의 렉시컬 스코핑(Lexical Scoping)을 기반으로 작동한다. 함수가 정의된 위치에서 외부 변수에 접근 가능하며, 함수형 프로그래밍(Functional Programming)에서 중요한 역할을 한다. 클로저(Closures)를 활용하면 코드의 유연성과 재사용성을 높일 수 있다.


클로저(Closures)란 무엇인가?

클로저(Closures)는 함수(Function)가 외부 스코프(Scope)의 변수에 접근하고, 그 변수의 상태를 유지하는 구조를 말한다. 함수가 생성될 때의 환경을 캡처(Capture)하여, 이후에도 해당 변수에 접근할 수 있다. 기본적인 예제를 통해 이를 확인한다:

function createCounter() {
    let count = 0;
    return function() {
        return count++;
    };
}
const counter = createCounter();
console.log(counter()); // 0
console.log(counter()); // 1

위 코드에서 createCounter는 내부 함수를 반환하며, 내부 함수는 외부 변수 count를 참조한다. count는 클로저(Closures)에 의해 유지되어, 호출할 때마다 증가한다. 이는 변수의 생명 주기(Lifecycle)를 함수와 연결하는 방식이다.


화살표 함수(Arrow Functions)를 사용한 예제도 있다:

const makeAdder = x => y => x + y;
const addFive = makeAdder(5);
console.log(addFive(3)); // 8

makeAdderx를 캡처한 클로저(Closures)를 생성하며, 반환된 함수는 x를 기억한다. 클로저(Closures)는 함수가 외부 환경을 잊지 않고 지속적으로 활용할 수 있게 한다.


클로저(Closures)의 동작 방식

클로저(Closures)는 함수가 정의된 스코프(Scope)에서 변수에 접근하는 메커니즘을 기반으로 한다. 함수가 외부 변수를 참조하면, 해당 변수는 클로저(Closures)에 포함된다.


예제를 통해 클로저(Closures)의 동작을 확인한다:

function outer() {
    let value = "외부";
    function inner() {
        console.log(value);
    }
    return inner;
}
const fn = outer();
fn(); // 외부

outer 함수가 종료된 후에도 inner 함수는 value를 기억한다. 이는 클로저(Closures)가 스코프 체인(Scope Chain)을 유지하기 때문이다.


여러 클로저(Closures)가 동일한 변수를 공유하는 경우도 있다:

function createActions() {
    let num = 0;
    return {
        increment: () => num++,
        get: () => num
    };
}
const actions = createActions();
console.log(actions.increment()); // 0
console.log(actions.get()); // 1

위 예제에서 incrementget은 동일한 num을 참조하며, 클로저(Closures)를 통해 상태를 공유한다.


클로저(Closures)와 일반 함수의 차이점

클로저(Closures)는 일반 함수와 변수 접근 및 상태 유지 측면에서 차이가 있다.


1. 변수 접근

- 일반 함수: 외부 변수에 접근하지 않거나, 접근하더라도 스코프(Scope)가 종료되면 사라진다.

function noClosure() {
    let temp = "임시";
    return "끝";
}
console.log(noClosure()); // 끝
// temp는 사라짐

- 클로저: 외부 변수를 기억하고 유지한다.

function withClosure() {
    let temp = "임시";
    return () => temp;
}
const getTemp = withClosure();
console.log(getTemp()); // 임시

2. 상태 유지

- 일반 함수: 상태를 유지하려면 외부 변수나 객체(Object)를 별도로 사용한다.

let count = 0;
function increment() {
    return count++;
}
console.log(increment()); // 0

- 클로저: 함수 내부에서 상태를 캡슐화한다.

function createIncrement() {
    let count = 0;
    return () => count++;
}
const inc = createIncrement();
console.log(inc()); // 0

사용 사례

클로저(Closures)의 활용 사례를 예제를 통해 확인한다.


1. 카운터 구현

function createCounter() {
    let count = 0;
    return {
        up: () => ++count,
        down: () => --count,
        value: () => count
    };
}
const counter = createCounter();
console.log(counter.up()); // 1
console.log(counter.down()); // 0
console.log(counter.value()); // 0

2. 데이터 은닉

function createPerson(name) {
    let age = 0;
    return {
        getName: () => name,
        setAge: newAge => age = newAge,
        getAge: () => age
    };
}
const person = createPerson("철수");
person.setAge(25);
console.log(person.getName()); // 철수
console.log(person.getAge()); // 25

3. 이벤트 핸들러

function createHandler(id) {
    let clicks = 0;
    return () => console.log(`ID: ${id}, 클릭 수: ${++clicks}`);
}
document.querySelector("#btn1").addEventListener("click", createHandler("btn1"));

4. 지연 실행

function delayLog(message) {
    let delay = 1000;
    return () => setTimeout(() => console.log(message), delay);
}
const logHello = delayLog("안녕");
logHello(); // 1초 후 "안녕" 출력

5. 함수 팩토리

function multiplyBy(factor) {
    return x => x * factor;
}
const triple = multiplyBy(3);
console.log(triple(5)); // 15

6. 루프 내 클로저

function createButtons() {
    for (var i = 0; i < 3; i++) {
        (function(index) {
            document.querySelector(`#btn${index}`).addEventListener("click", () => console.log(index));
        })(i);
    }
}
createButtons(); // 0, 1, 2 각각 출력

7. 캐시 구현

function createCache() {
    const cache = {};
    return key => {
        if (key in cache) return cache[key];
        return cache[key] = Math.random();
    };
}
const getRandom = createCache();
console.log(getRandom("a")); // 랜덤 값
console.log(getRandom("a")); // 동일한 값

8. 설정 저장

function createLogger(prefix) {
    let logCount = 0;
    return message => console.log(`${prefix}[${++logCount}]: ${message}`);
}
const logError = createLogger("ERROR");
logError("문제 발생"); // ERROR[1]: 문제 발생
logError("다시 발생"); // ERROR[2]: 다시 발생

성능과 한계

장점

- 데이터 은닉: 외부 접근을 차단한다.

- 상태 유지: 변수의 상태를 지속적으로 관리한다.

- 유연성: 동적인 함수 생성이 가능하다.


한계

- 메모리 사용: 클로저(Closures)가 변수를 유지하므로 메모리가 해제되지 않는다.

- 복잡성: 과도한 사용은 코드 이해를 어렵게 할 수 있다.

메모리 관리가 필요한 경우, 클로저(Closures)의 사용을 최소화한다.


마무리

클로저(Closures)는 자바스크립트(JavaScript)에서 함수와 외부 스코프(Scope)를 연결하는 유용한 메커니즘이다.


고차 함수 (Higher-Order Functions)

고차 함수 (Higher-Order Functions)

자바스크립트(JavaScript)에서 고차 함수(Higher-Order Functions)는 함수(Function)를 다루는 중요한 기법 중 하나이다. 함수를 인자로 받거나 함수를 반환하는 함수를 의미하며, 이를 통해 코드의 유연성과 재사용성을 높일 수 있다. 고차 함수(Higher-Order Functions)의 정의, 구현 방법, 일반 함수와의 차이점, 그리고 사용 사례를 예제를 통해 확인한다.


고차 함수(Higher-Order Functions)는 자바스크립트(JavaScript)가 함수를 일급 객체(First-Class Citizen)로 취급하는 특성을 활용한다. 배열(Array) 메서드나 콜백(Callback) 함수에서 자주 등장하며, 함수형 프로그래밍(Functional Programming)과도 밀접한 연관이 있다. 고차 함수(Higher-Order Functions)를 통해 코드의 구조를 개선하고 로직을 효율적으로 분리할 수 있다.


고차 함수(Higher-Order Functions)란 무엇인가?

고차 함수(Higher-Order Functions)는 함수(Function)를 인자로 받거나 반환하는 함수를 말한다. 자바스크립트(JavaScript)에서 함수는 값처럼 다룰 수 있는 일급 객체이므로, 이러한 방식이 가능하다. 기본적인 예제를 통해 이를 확인한다:

function sayHello(callback) {
    callback("철수");
}
sayHello(name => console.log(`안녕, ${name}!`)); // 안녕, 철수!

위 코드에서 sayHello 함수는 콜백 함수(Callback Function)를 인자로 받아 실행한다. 함수를 반환하는 경우도 있다:

function multiplyBy(factor) {
    return x => x * factor;
}
const double = multiplyBy(2);
console.log(double(5)); // 10

multiplyBy는 숫자를 인자로 받아 곱셈을 수행하는 함수를 반환한다. 이를 통해 double과 같은 새로운 함수를 생성할 수 있다. 고차 함수(Higher-Order Functions)는 함수를 래핑하거나 조합하는 데 유용하다.


자바스크립트(JavaScript)에서 고차 함수(Higher-Order Functions)는 이미 익숙한 배열 메서드에서도 사용된다. 예를 들어 map, filter, reduce는 모두 고차 함수의 대표적인 사례이다:

const numbers = [1, 2, 3];
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6]

고차 함수(Higher-Order Functions)는 코드의 가독성을 높이고, 로직을 분리하여 관리하기 쉽게 만든다.


고차 함수(Higher-Order Functions)의 구현 방법

고차 함수(Higher-Order Functions)는 두 가지 주요 방식으로 구현된다. 함수를 인자로 받는 방식과 함수를 반환하는 방식이다.


1. 함수를 인자로 받기

콜백(Callback)을 인자로 받아 작업을 처리하는 방식이다.

function repeat(n, action) {
    for (let i = 0; i < n; i++) {
        action(i);
    }
}
repeat(3, i => console.log(`반복 ${i}`)); // 반복 0, 반복 1, 반복 2

조건에 따라 동작을 실행하는 예제도 있다:

function unless(condition, fn) {
    if (!condition) fn();
}
unless(2 > 3, () => console.log("거짓이다")); // 거짓이다

2. 함수를 반환하기

새로운 함수를 생성하여 반환하는 방식이다.

function addOffset(offset) {
    return x => x + offset;
}
const addTen = addOffset(10);
console.log(addTen(5)); // 15

여러 요소를 조합하는 예제도 가능하다:

function wrapWith(prefix, suffix) {
    return text => `${prefix}${text}${suffix}`;
}
const boldText = wrapWith("", "");
console.log(boldText("강조")); // 강조

고차 함수(Higher-Order Functions)와 일반 함수의 차이점

고차 함수(Higher-Order Functions)는 일반 함수와 사용 방식 및 설계 측면에서 차이가 있다.


1. 함수 처리 방식

- 일반 함수: 입력을 받아 결과를 반환한다.

function add(a, b) {
    return a + b;
}
console.log(add(2, 3)); // 5

- 고차 함수: 함수를 인자로 받아 처리하거나 반환한다.

function addWith(fn) {
    return (a, b) => fn(a) + fn(b);
}
const addSquares = addWith(x => x * x);
console.log(addSquares(2, 3)); // 13 (4 + 9)

2. 유연성

- 일반 함수: 고정된 로직을 수행한다.

function doubleArray(arr) {
    return arr.map(x => x * 2);
}

- 고차 함수: 로직을 동적으로 변경할 수 있다.

function transformArray(fn) {
    return arr => arr.map(fn);
}
const tripleArray = transformArray(x => x * 3);
console.log(tripleArray([1, 2, 3])); // [3, 6, 9]

사용 사례

고차 함수(Higher-Order Functions)의 활용 사례를 예제를 통해 확인한다.


1. 배열 가공

const scores = [85, 92, 78, 95];
const adjustScore = adjust => scores.map(score => score + adjust);
console.log(adjustScore(5)); // [90, 97, 83, 100]

2. 이벤트 처리

function logEvent(type) {
    return e => console.log(`${type}: ${e.target.value}`);
}
document.querySelector("input").addEventListener("input", logEvent("입력"));

3. 타이머 조작

function delay(fn, ms) {
    return (...args) => setTimeout(() => fn(...args), ms);
}
const delayedLog = delay(console.log, 1000);
delayedLog("1초 후 출력"); // 1초 후 출력

4. 로깅 래퍼

function withLogging(fn) {
    return (...args) => {
        console.log(`호출: ${args}`);
        return fn(...args);
    };
}
const logAdd = withLogging((a, b) => a + b);
console.log(logAdd(2, 3)); // 호출: 2,3 → 5

5. 조건 필터

function filterBy(fn) {
    return arr => arr.filter(fn);
}
const evenNumbers = filterBy(x => x % 2 === 0);
console.log(evenNumbers([1, 2, 3, 4])); // [2, 4]

6. 함수 조합

const compose = (f, g) => x => f(g(x));
const addOne = x => x + 1;
const square = x => x * x;
const addThenSquare = compose(square, addOne);
console.log(addThenSquare(3)); // 16 ( (3 + 1)^2 )

7. 비동기 처리

function retry(fn, times) {
    return async (...args) => {
        for (let i = 0; i < times; i++) {
            try {
                return await fn(...args);
            } catch (e) {
                if (i === times - 1) throw e;
            }
        }
    };
}
const fetchWithRetry = retry(fetch, 3);
fetchWithRetry("https://api.example.com").then(res => res.json());

8. 데이터 변환

function formatData(formatter) {
    return data => data.map(formatter);
}
const toUpper = formatData(str => str.toUpperCase());
console.log(toUpper(["hi", "hello"])); // ["HI", "HELLO"]

성능과 한계

장점

- 유연성: 로직을 동적으로 변경할 수 있다.

- 재사용성: 함수를 조합하여 새로운 기능을 생성한다.

- 가독성: 코드가 간결해지고 로직이 분리된다.


한계

- 복잡성: 과도한 중첩은 가독성을 떨어뜨릴 수 있다.

- 성능: 함수 호출이 증가하면 실행 속도가 느려질 수 있다.

간단한 작업에는 고차 함수를 활용하고, 복잡한 로직은 일반 함수로 처리하는 것이 적합하다.


마무리

고차 함수(Higher-Order Functions)는 자바스크립트(JavaScript)에서 함수를 다루는 도구이고 사용 사례를 통해 그 유용성을 확인했다.


재귀 함수 (Recursive Functions)

재귀 함수 (Recursive Functions)

자바스크립트(JavaScript)에서 재귀 함수(Recursive Functions)는 함수(Function)가 스스로를 호출하여 문제를 해결하는 방식이다. 반복적인 작업을 간결하게 표현하며, 특히 트리(Tree)나 그래프(Graph) 같은 복잡한 데이터 구조를 다룰 때 유용하다. 재귀 함수(Recursive Functions)의 정의, 구현 방법, 일반 반복문과의 차이, 활용 사례를 살펴보자.


재귀 함수(Recursive Functions)는 자기 자신을 호출한다는 점에서 독특하다. 이를 통해 코드가 단순해지고, 문제 해결이 직관적으로 변한다. 자바스크립트(JavaScript)에서 재귀 함수(Recursive Functions)는 함수형 프로그래밍(Functional Programming)과도 연관되며, 성능 최적화와 결합하면 강력한 도구가 된다. 다양한 예제를 통해 재귀 함수(Recursive Functions)의 동작 방식과 실용성을 확인한다.


재귀 함수(Recursive Functions)란 무엇인가?

재귀 함수(Recursive Functions)는 함수(Function)가 내부에서 자신을 호출하여 작업을 수행한다. 기저 조건(Base Case)재귀 조건(Recursive Case)으로 구성되며, 기저 조건에서 호출이 종료된다. 재귀(Recursion)는 수학적 귀납법과 비슷한 논리를 따른다.


간단한 예제로 팩토리얼(Factorial)을 계산하는 함수를 보자:

function factorial(n) {
    if (n <= 1) return 1; // 기저 조건
    return n * factorial(n - 1); // 재귀 조건
}
console.log(factorial(5)); // 120 (5 * 4 * 3 * 2 * 1)

위 코드에서 factorial(5)는 다음과 같이 동작한다:

  • factorial(5) = 5 * factorial(4)
  • factorial(4) = 4 * factorial(3)
  • factorial(3) = 3 * factorial(2)
  • factorial(2) = 2 * factorial(1)
  • factorial(1) = 1 (기저 조건 도달)

결과적으로 호출 스택(Call Stack)이 쌓였다가 풀리며 5 * 4 * 3 * 2 * 1 = 120을 계산한다. 재귀 함수(Recursive Functions)는 종료 조건이 없으면 무한 호출에 빠질 수 있다:

function infinite() {
    return infinite(); // 무한 재귀 (Stack Overflow)
}
// infinite(); // 오류 발생

자바스크립트(JavaScript)에서 재귀 함수(Recursive Functions)는 호출 스택(Call Stack)의 크기 제한에 주의해야 한다. 너무 깊은 재귀(Recursion)는 스택 오버플로우(Stack Overflow)를 유발한다.


화살표 함수(Arrow Functions)로도 구현 가능하다:

const factorialArrow = n => n <= 1 ? 1 : n * factorialArrow(n - 1);
console.log(factorialArrow(5)); // 120

재귀 함수(Recursive Functions)는 문제를 작은 하위 문제(Subproblem)로 나누어 해결하는 분할 정복(Divide and Conquer) 전략과 밀접하다.


재귀 함수(Recursive Functions)의 구현 방법

재귀 함수(Recursive Functions)는 기본 재귀와 꼬리 재귀(Tail Recursion)로 나뉜다.


1. 기본 재귀

기저 조건과 재귀 조건을 명시한다.

function sumToN(n) {
    if (n <= 1) return n;
    return n + sumToN(n - 1);
}
console.log(sumToN(5)); // 15 (5 + 4 + 3 + 2 + 1)

피보나치 수열(Fibonacci Sequence) 예제:

function fib(n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
}
console.log(fib(6)); // 8

2. 꼬리 재귀(Tail Recursion)

마지막 연산이 재귀 호출인 경우로, 스택 프레임(Stack Frame)을 재사용할 수 있다. (자바스크립트 엔진은 최적화 미지원)

function factorialTail(n, acc = 1) {
    if (n <= 1) return acc;
    return factorialTail(n - 1, n * acc);
}
console.log(factorialTail(5)); // 120

꼬리 재귀는 스택 오버플로우(Stack Overflow)를 줄일 잠재력이 있지만, 자바스크립트(JavaScript)에서는 기본적으로 최적화되지 않는다.


재귀 함수(Recursive Functions)와 반복문의 차이

재귀(Recursion)와 반복(Iteration)은 동일한 문제를 다른 방식으로 해결한다.


1. 코드 표현

- 반복문: 명시적 루프 사용.

function sumToNLoop(n) {
    let sum = 0;
    for (let i = 1; i <= n; i++) {
        sum += i;
    }
    return sum;
}
console.log(sumToNLoop(5)); // 15

- 재귀: 함수 호출로 표현.

function sumToNRec(n) {
    if (n <= 1) return n;
    return n + sumToNRec(n - 1);
}
console.log(sumToNRec(5)); // 15

2. 성능

- 반복문: 스택 사용 없이 실행, 일반적으로 빠르다.

- 재귀: 호출 스택(Call Stack)을 쌓아 메모리 사용이 늘어난다.

console.time("Loop");
sumToNLoop(1000);
console.timeEnd("Loop");

console.time("Recursion");
sumToNRec(1000);
console.timeEnd("Recursion");
// Loop가 더 빠름

실무 예제

재귀 함수(Recursive Functions)의 실무 활용 사례를 통해 그 유용성을 확인한다.


1. 트리 순회(Tree Traversal)

const tree = {
    value: 1,
    children: [
        { value: 2, children: [{ value: 4 }] },
        { value: 3 }
    ]
};
function printTree(node) {
    if (!node) return;
    console.log(node.value);
    if (node.children) node.children.forEach(child => printTree(child));
}
printTree(tree); // 1, 2, 4, 3

2. 깊은 복사(Deep Copy)

function deepCopy(obj) {
    if (typeof obj !== "object" || obj === null) return obj;
    const result = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
        result[key] = deepCopy(obj[key]);
    }
    return result;
}
const nested = { a: 1, b: { c: 2 } };
const copy = deepCopy(nested);
copy.b.c = 3;
console.log(nested.b.c); // 2

3. 배열 평탄화(Flatten Array)

function flatten(arr) {
    return arr.reduce((acc, item) => {
        return acc.concat(Array.isArray(item) ? flatten(item) : item);
    }, []);
}
const nestedArray = [1, [2, [3, 4], 5]];
console.log(flatten(nestedArray)); // [1, 2, 3, 4, 5]

4. 하노이 탑(Tower of Hanoi)

function hanoi(n, from, to, aux) {
    if (n === 1) {
        console.log(`원판 1을 ${from}에서 ${to}로 이동`);
        return;
    }
    hanoi(n - 1, from, aux, to);
    console.log(`원판 ${n}을 ${from}에서 ${to}로 이동`);
    hanoi(n - 1, aux, to, from);
}
hanoi(3, "A", "C", "B");
// 원판 1을 A에서 C로 이동
// 원판 2를 A에서 B로 이동
// 원판 1을 C에서 B로 이동
// 원판 3을 A에서 C로 이동
// ...

5. JSON 파싱

function parseJson(obj) {
    if (typeof obj !== "object" || obj === null) return obj;
    const result = {};
    for (let key in obj) {
        result[key] = parseJson(obj[key]);
    }
    return result;
}
const jsonData = { a: 1, b: { c: "2", d: { e: 3 } } };
console.log(parseJson(jsonData)); // 동일 구조 유지

6. 순열 생성(Permutations)

function permute(arr) {
    if (arr.length <= 1) return [arr];
    const result = [];
    for (let i = 0; i < arr.length; i++) {
        const current = arr[i];
        const rest = arr.slice(0, i).concat(arr.slice(i + 1));
        const perms = permute(rest);
        perms.forEach(p => result.push([current].concat(p)));
    }
    return result;
}
console.log(permute([1, 2, 3])); // [[1, 2, 3], [1, 3, 2], [2, 1, 3], ...]

7. 파일 시스템 탐색

const fsStructure = {
    name: "root",
    type: "folder",
    contents: [
        { name: "file1.txt", type: "file" },
        { name: "subfolder", type: "folder", contents: [{ name: "file2.txt", type: "file" }] }
    ]
};
function listFiles(dir) {
    if (dir.type === "file") return [dir.name];
    return dir.contents.reduce((acc, item) => acc.concat(listFiles(item)), []);
}
console.log(listFiles(fsStructure)); // ["file1.txt", "file2.txt"]

8. 숫자 분해 합

function digitSum(n) {
    if (n < 10) return n;
    return (n % 10) + digitSum(Math.floor(n / 10));
}
console.log(digitSum(1234)); // 10 (1 + 2 + 3 + 4)

성능과 한계

장점

- 간결함: 복잡한 로직을 단순화한다.

- 직관성: 분할 정복(Divide and Conquer) 문제에 적합하다.

- 유연성: 데이터 구조 처리에 강하다.


한계

- 스택 오버플로우(Stack Overflow): 깊은 재귀(Recursion)는 메모리 문제를 일으킨다.

- 성능: 반복문보다 느릴 수 있다.

메모이제이션(Memoization)이나 꼬리 재귀(Tail Recursion)로 성능을 개선할 수 있다.


마무리

재귀 함수(Recursive Functions)는 자바스크립트(JavaScript)에서 강력한 문제 해결 도구다. 사례를 통해 그 유용성을 확인했다.


메모이제이션 (Memoization)

메모이제이션 (Memoization)

자바스크립트(JavaScript)에서 메모이제이션(Memoization)은 함수(Function)의 성능을 최적화하는 기법이다. 이전에 계산된 결과를 저장하여 동일한 입력에 대해 반복적인 연산을 피한다. 함수형 프로그래밍(Functional Programming)에서 자주 활용되며, 특히 재귀 함수(Recursive Functions)나 복잡한 계산에서 효율성을 높인다. 이 포스팅에서는 메모이제이션(Memoization)의 정의, 구현 방법, 기존 함수와의 차이, 실무에서의 활용 사례를 다룬다.


메모이제이션(Memoization)은 자바스크립트(JavaScript)의 클로저(Closure)를 활용하여 캐시(Cache)를 유지한다. 이를 통해 시간 복잡도(Time Complexity)를 줄이고, 애플리케이션의 반응성을 개선한다. 다양한 예제를 통해 메모이제이션(Memoization)이 어떻게 작동하며, 어떤 상황에서 유용한지 확인한다. 자바스크립트(JavaScript)에서 메모이제이션(Memoization)은 성능 병목 현상을 해결하는 도구로 사용된다.


메모이제이션(Memoization)이란 무엇인가?

메모이제이션(Memoization)은 함수(Function)가 동일한 입력에 대해 동일한 출력을 반환할 때, 그 결과를 저장하여 이후 호출에서 재사용하는 방식이다. 계산 비용이 높은 작업을 반복하지 않도록 하여 성능을 개선한다. 이는 함수형 프로그래밍(Functional Programming)에서 중요한 개념으로, 순수 함수(Pure Function)와 잘 맞는다.


기본적인 예제로 피보나치 수열(Fibonacci Sequence)을 계산하는 함수를 보자:

function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(10)); // 55

위 함수는 재귀(Recursion)를 사용하며, n이 커질수록 계산량이 기하급수적으로 증가한다(시간 복잡도 O(2^n)). 예를 들어, fibonacci(40)을 호출하면 동일한 하위 계산이 수백만 번 반복된다. 이를 메모이제이션(Memoization)으로 개선하면:

function memoFibonacci() {
    const cache = {};
    return function fib(n) {
        if (n in cache) return cache[n];
        if (n <= 1) return n;
        cache[n] = fib(n - 1) + fib(n - 2);
        return cache[n];
    };
}
const fib = memoFibonacci();
console.log(fib(10)); // 55
console.log(fib(40)); // 102334155 (빠르게 계산됨)

위 코드에서 cache 객체는 이전 결과를 저장한다. 클로저(Closure)를 통해 cache가 유지되며, 동일한 n에 대해 재계산 대신 저장된 값을 반환한다. 시간 복잡도가 O(n)으로 줄어 성능이 크게 향상된다.


화살표 함수(Arrow Functions)를 사용한 간결한 버전:

const memoFib = (() => {
    const cache = {};
    return n => {
        if (n in cache) return cache[n];
        if (n <= 1) return n;
        return cache[n] = memoFib(n - 1) + memoFib(n - 2);
    };
})();
console.log(memoFib(40)); // 102334155

메모이제이션(Memoization)은 계산 결과를 "메모"한다는 뜻에서 이름이 붙었다. 자바스크립트(JavaScript)에서는 객체(Object)나 맵(Map)을 캐시(Cache)로 활용하며, 함수(Function)의 순수성(Purity)을 유지할 때 효과적이다.


메모이제이션(Memoization)의 구현 방법

메모이제이션(Memoization)을 구현하는 방법에는 수동 구현과 범용 유틸리티 함수를 사용하는 두 가지가 있다.


1. 수동 메모이제이션(Memoization)

함수(Function) 내부에 캐시(Cache)를 직접 추가한다.

function factorial(n) {
    const cache = {};
    return function calc(n) {
        if (n in cache) return cache[n];
        if (n <= 1) return 1;
        cache[n] = n * calc(n - 1);
        return cache[n];
    };
}
const fact = factorial();
console.log(fact(5)); // 120
console.log(fact(5)); // 120 (캐시 사용)

다중 인자를 처리하는 경우:

function multiply(a, b) {
    const cache = {};
    return function calc(a, b) {
        const key = `${a},${b}`;
        if (key in cache) return cache[key];
        cache[key] = a * b;
        return cache[key];
    };
}
const mult = multiply();
console.log(mult(3, 4)); // 12
console.log(mult(3, 4)); // 12 (캐시 사용)

2. 범용 메모이제이션(Memoization) 함수

임의의 함수(Function)를 메모이제이션(Memoization)하는 헬퍼 함수를 만든다.

function memoize(fn) {
    const cache = {};
    return function(...args) {
        const key = JSON.stringify(args);
        if (key in cache) return cache[key];
        cache[key] = fn(...args);
        return cache[key];
    };
}
const slowAdd = (a, b) => {
    console.log("계산 중...");
    return a + b;
};
const memoAdd = memoize(slowAdd);
console.log(memoAdd(2, 3)); // 계산 중... → 5
console.log(memoAdd(2, 3)); // 5 (캐시 사용)

memoize 함수는 인자를 문자열 키로 변환하여 캐시(Cache)에 저장한다. 다양한 함수에 적용 가능하다:

const slowPower = (base, exp) => {
    console.log("거듭제곱 계산 중...");
    return Math.pow(base, exp);
};
const memoPower = memoize(slowPower);
console.log(memoPower(2, 3)); // 거듭제곱 계산 중... → 8
console.log(memoPower(2, 3)); // 8 (캐시 사용)

메모이제이션(Memoization)과 일반 함수의 차이

메모이제이션(Memoization)은 일반 함수와 실행 방식, 성능에서 차이가 있다.


1. 실행 방식

- 일반 함수: 매 호출마다 계산을 반복한다.

function square(n) {
    console.log("제곱 계산");
    return n * n;
}
console.log(square(5)); // 제곱 계산 → 25
console.log(square(5)); // 제곱 계산 → 25

- 메모이제이션(Memoization): 캐시(Cache)를 활용한다.

const memoSquare = memoize(n => {
    console.log("제곱 계산");
    return n * n;
});
console.log(memoSquare(5)); // 제곱 계산 → 25
console.log(memoSquare(5)); // 25 (캐시 사용)

2. 성능 개선

- 일반 함수: 입력 크기에 따라 성능 저하가 심하다.

function fibNoMemo(n) {
    if (n <= 1) return n;
    return fibNoMemo(n - 1) + fibNoMemo(n - 2);
}
console.time("No Memo");
console.log(fibNoMemo(35)); // 느림
console.timeEnd("No Memo");

- 메모이제이션(Memoization): 캐시(Cache)로 속도를 높인다.

const fibMemo = memoize(n => {
    if (n <= 1) return n;
    return fibMemo(n - 1) + fibMemo(n - 2);
});
console.time("Memo");
console.log(fibMemo(35)); // 빠름
console.timeEnd("Memo");

실무 예제

메모이제이션(Memoization)의 실무 활용 사례를 통해 확인한다.


1. 재귀 함수 최적화

const binomial = memoize((n, k) => {
    if (k === 0 || k === n) return 1;
    return binomial(n - 1, k - 1) + binomial(n - 1, k);
});
console.log(binomial(20, 10)); // 184756

2. API 호출 캐싱

const fetchData = memoize(async id => {
    const response = await fetch(`https://api.example.com/${id}`);
    return response.json();
});
fetchData(1).then(data => console.log(data)); // 네트워크 요청
fetchData(1).then(data => console.log(data)); // 캐시 사용

3. 계산 집약적 작업

const primeCheck = memoize(n => {
    for (let i = 2; i <= Math.sqrt(n); i++) {
        if (n % i === 0) return false;
    }
    return n > 1;
});
console.log(primeCheck(97)); // true
console.log(primeCheck(97)); // true (캐시 사용)

4. 동적 프로그래밍

const knapsack = memoize((capacity, items) => {
    if (capacity <= 0 || items.length === 0) return 0;
    const [weight, value, ...rest] = items;
    if (weight > capacity) return knapsack(capacity, rest);
    return Math.max(
        value + knapsack(capacity - weight, rest),
        knapsack(capacity, rest)
    );
});
console.log(knapsack(10, [[5, 10], [4, 40], [6, 30]])); // 40

5. 문자열 변환

const slugify = memoize(text => {
    return text.toLowerCase().replace(/\s+/g, "-");
});
console.log(slugify("Hello World")); // hello-world
console.log(slugify("Hello World")); // hello-world (캐시 사용)

6. 객체(Object) 처리

const getNestedValue = memoize((obj, path) => {
    return path.split(".").reduce((acc, part) => acc[part], obj);
});
const data = { user: { profile: { name: "철수" } } };
console.log(getNestedValue(data, "user.profile.name")); // 철수
console.log(getNestedValue(data, "user.profile.name")); // 철수 (캐시 사용)

7. 이벤트 핸들러 최적화

const throttle = (fn, delay) => {
    let last = 0;
    return (...args) => {
        const now = Date.now();
        if (now - last >= delay) {
            last = now;
            return fn(...args);
        }
    };
};
const memoThrottle = memoize(throttle);
const handler = memoThrottle((e) => console.log(e.type), 1000);
window.addEventListener("resize", handler);

성능과 한계

장점

- 성능 향상: 반복 계산을 줄인다.

- 코드 효율성: 복잡한 로직을 간소화한다.

- 재사용성: 캐시(Cache)로 결과를 재활용한다.


한계

- 메모리 사용: 캐시(Cache)가 커질 수 있다.

- 순수성 요구: 입력이 변하면 캐시가 무효화된다.

캐시(Cache) 크기 관리를 위해 제한된 입력에만 사용한다.


마무리

메모이제이션(Memoization)은 자바스크립트(JavaScript)에서 성능 최적화를 위해 활용된다.


커링 (Currying)

커링 (Currying)

자바스크립트(JavaScript)에서 커링(Currying)은 함수(Function)를 여러 단계로 나누어 호출할 수 있게 만드는 기법이다. 다중 매개변수(Parameter)를 받는 함수(Function)를 단일 매개변수(Parameter)를 받는 함수들의 연속으로 변환한다. 함수형 프로그래밍(Functional Programming)에서 자주 사용되며, 코드의 재사용성과 유연성을 높인다. 이 포스팅에서는 커링(Currying)의 정의, 구현 방법, 기존 함수와의 차이, 실무에서의 활용 사례를 다룬다.


커링(Currying)은 함수(Function)의 호출 방식을 바꿔, 부분 적용(Partial Application)을 쉽게 구현한다. 이를 통해 코드의 모듈화와 가독성을 개선하며, 복잡한 로직을 단순화할 수 있다. 자바스크립트(JavaScript)에서 커링(Currying)은 화살표 함수(Arrow Functions)와 결합하여 강력한 도구로 작용한다. 다양한 예제를 통해 커링(Currying)이 어떻게 동작하고, 실제 애플리케이션에서 어떤 가치를 제공하는지 알아본다.


커링(Currying)이란 무엇인가?

커링(Currying)은 다중 인자를 받는 함수(Function)를 단일 인자를 받는 함수들의 체인으로 변환하는 과정이다. 예를 들어, 두 개의 매개변수(Parameter)를 받는 함수(Function)를 커링(Currying)하면, 첫 번째 인자를 받은 후 두 번째 인자를 받는 함수를 반환한다. 이는 함수형 프로그래밍(Functional Programming)의 핵심 개념 중 하나로, 하스켈(Haskell) 언어의 수학자 해스켈 커리(Haskell Curry)에서 이름을 따왔다.


기본적인 커링(Currying) 예제는 다음과 같다:

function add(a, b) {
    return a + b;
}
console.log(add(2, 3)); // 5

function curriedAdd(a) {
    return function(b) {
        return a + b;
    };
}
console.log(curriedAdd(2)(3)); // 5

위 코드에서 add는 일반 함수(Function)로 두 인자를 한 번에 받는다. 반면 curriedAdd는 커링(Currying)된 함수로, 첫 번째 인자 a를 받은 후 두 번째 인자 b를 받는 함수를 반환한다. 호출 방식이 add(2, 3)에서 curriedAdd(2)(3)으로 바뀌며, 이는 함수 호출을 단계적으로 처리할 수 있게 한다.


화살표 함수(Arrow Functions)를 사용하면 더 간결해진다:

const curriedAddArrow = a => b => a + b;
console.log(curriedAddArrow(2)(3)); // 5

커링(Currying)의 핵심은 함수(Function)가 모든 인자를 한 번에 받는 대신, 인자를 하나씩 받아 최종 결과를 도출할 때까지 함수를 반환하는 구조다. 이를 통해 특정 인자를 고정하고 나머지를 나중에 제공할 수 있다:

const addFive = curriedAddArrow(5);
console.log(addFive(3)); // 8
console.log(addFive(10)); // 15

위 예제에서 addFive5를 고정한 함수로, 이후 어떤 숫자를 넣든 5를 더한 결과를 반환한다. 이는 커링(Currying)이 부분 적용(Partial Application)을 가능하게 하는 방식이다.


커링(Currying)은 자바스크립트(JavaScript)가 함수를 일급 객체(First-Class Citizen)로 다루는 특성을 활용한다. 함수가 값처럼 변수에 할당되고, 다른 함수의 인자나 반환값으로 사용될 수 있기 때문에, 커링(Currying)은 자연스럽게 구현된다. 자바스크립트(JavaScript)의 클로저(Closure)도 커링(Currying)에서 중요한 역할을 한다. 클로저(Closure)는 함수가 외부 변수에 접근할 수 있게 해, 커링(Currying)된 함수가 이전 인자를 기억하도록 만든다.


커링(Currying)의 구현 방법

커링(Currying)을 구현하는 방법에는 수동 구현과 유틸리티 함수를 사용하는 두 가지 접근이 있다. 각각의 방식을 예제와 함께 알아본다.


1. 수동 커링(Currying)

함수(Function)를 직접 커링(Currying)하여 단계별로 작성한다.

function multiply(a, b, c) {
    return a * b * c;
}
function curriedMultiply(a) {
    return function(b) {
        return function(c) {
            return a * b * c;
        };
    };
}
console.log(multiply(2, 3, 4)); // 24
console.log(curriedMultiply(2)(3)(4)); // 24

화살표 함수(Arrow Functions)로 더 간결하게:

const curriedMultiplyArrow = a => b => c => a * b * c;
console.log(curriedMultiplyArrow(2)(3)(4)); // 24

부분 적용(Partial Application)을 활용한 예:

const double = curriedMultiplyArrow(2);
const sixTimes = double(3);
console.log(sixTimes(4)); // 24
console.log(double(5)(3)); // 30

2. 유틸리티 함수로 커링(Currying)

일반 함수(Function)를 자동으로 커링(Currying)하는 헬퍼 함수를 만든다.

function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn(...args);
        }
        return function(...moreArgs) {
            return curried(...args, ...moreArgs);
        };
    };
}
const addThree = (a, b, c) => a + b + c;
const curriedAddThree = curry(addThree);
console.log(curriedAddThree(1)(2)(3)); // 6
console.log(curriedAddThree(1, 2)(3)); // 6
console.log(curriedAddThree(1, 2, 3)); // 6

curry 함수는 원래 함수의 매개변수(Parameter) 수를 확인하고, 충분한 인자가 제공되면 결과를 계산하며, 그렇지 않으면 추가 인자를 기다리는 함수를 반환한다. 이 방식은 유연성을 제공한다.


더 복잡한 함수에 적용:

const formatMessage = (prefix, name, suffix) => `${prefix}, ${name}${suffix}`;
const curriedFormat = curry(formatMessage);
const helloFormat = curriedFormat("안녕");
const helloUser = helloFormat("철수");
console.log(helloUser("!")); // 안녕, 철수!
console.log(helloFormat("영희")("~")); // 안녕, 영희~

커링(Currying)과 일반 함수의 차이

커링(Currying)은 일반 함수와 호출 방식, 설계 철학에서 차이가 있다.


1. 호출 방식

- 일반 함수: 모든 인자를 한 번에 받는다.

function subtract(a, b) {
    return a - b;
}
console.log(subtract(5, 3)); // 2

- 커링(Currying): 인자를 단계별로 받는다.

const curriedSubtract = a => b => a - b;
console.log(curriedSubtract(5)(3)); // 2

2. 부분 적용(Partial Application)

- 일반 함수: 별도 처리가 필요하다.

function multiplyBy(factor, number) {
    return factor * number;
}
const triple = number => multiplyBy(3, number);
console.log(triple(4)); // 12

- 커링(Currying): 자연스럽게 지원한다.

const curriedMultiplyBy = factor => number => factor * number;
const tripleCurried = curriedMultiplyBy(3);
console.log(tripleCurried(4)); // 12

3. 함수 조합(Function Composition)

커링(Currying)은 함수 조합(Function Composition)과 잘 맞는다:

const compose = (f, g) => x => f(g(x));
const curriedAddOne = x => x + 1;
const curriedDouble = x => x * 2;
const addThenDouble = compose(curriedDouble, curriedAddOne);
console.log(addThenDouble(5)); // 12

실무 예제

커링(Currying)의 실무 활용 사례를 통해 그 유용성을 확인한다.


1. 로깅 함수

const log = level => message => console.log(`[${level}] ${message}`);
const infoLog = log("INFO");
const errorLog = log("ERROR");
infoLog("시스템 시작"); // [INFO] 시스템 시작
errorLog("서버 오류"); // [ERROR] 서버 오류

2. 단위 변환

const convert = factor => unit => value => `${value * factor} ${unit}`;
const kmToMiles = convert(0.621371);
const metersToFeet = convert(3.28084);
console.log(kmToMiles("miles")(5)); // 3.106855 miles
console.log(metersToFeet("feet")(10)); // 32.8084 feet

3. 데이터 필터링

const filterBy = key => value => obj => obj[key] === value;
const users = [
    { name: "철수", age: 25 },
    { name: "영희", age: 30 }
];
const filterByAge = filterBy("age");
const adults = users.filter(filterByAge(30));
console.log(adults); // [{ name: "영희", age: 30 }]

4. API 요청 설정

const fetchWithConfig = baseUrl => endpoint => async () => {
    const response = await fetch(`${baseUrl}${endpoint}`);
    return response.json();
};
const api = fetchWithConfig("https://api.example.com");
const getUsers = api("/users");
getUsers().then(data => console.log(data));

5. 이벤트 핸들러 생성

const createHandler = type => id => event => {
    console.log(`${type} 이벤트 발생, ID: ${id}, 값: ${event.target.value}`);
};
const inputHandler = createHandler("input");
const handlerForId1 = inputHandler("id1");
document.querySelector("#id1").addEventListener("input", handlerForId1);

6. 문자열 포매팅

const formatString = prefix => suffix => text => `${prefix}${text}${suffix}`;
const boldFormat = formatString("")("");
console.log(boldFormat("강조")); // 강조

7. 계산기 함수

const calculate = operation => a => b => {
    switch (operation) {
        case "add": return a + b;
        case "multiply": return a * b;
        default: return 0;
    }
};
const addCalc = calculate("add");
console.log(addCalc(5)(3)); // 8

성능과 한계

장점

- 재사용성: 부분 적용(Partial Application)으로 함수를 재사용한다.

- 모듈화: 복잡한 로직을 분리한다.

- 가독성: 단계적 호출로 의도를 명확히 한다.


한계

- 호출 복잡성: 다중 호출이 코드 흐름을 어렵게 할 수 있다.

- 메모리 사용: 클로저(Closure)로 인해 메모리 부담이 늘어날 수 있다.

간단한 작업에는 일반 함수, 복잡한 재사용 로직에는 커링(Currying)을 사용한다.


마무리

커링(Currying)은 자바스크립트(JavaScript)에서 함수(Function)를 단계적으로 처리하며, 재사용성과 유연성을 제공한다.


+ Recent posts