JavaScript IIFE와 Promise 활용 가이드

IIFE (Immediately Invoked Function Expression)란?

JavaScript의 IIFE(Immediately Invoked Function Expression)는 정의되자마자 즉시 실행되는 함수 표현식입니다. 함수를 선언하는 동시에 바로 호출하여 실행하는 패턴으로, JavaScript 개발에서 매우 유용한 도구입니다.

기본 문법

(function() {
    // 코드
})();

// 또는 화살표 함수로
(() => {
    // 코드
})();

동작 원리

IIFE는 두 단계로 작동합니다:

  1. 첫 번째 괄호: 함수를 표현식으로 만듭니다
  2. 두 번째 괄호: 그 함수를 즉시 실행시킵니다

함수를 괄호로 감싸지 않으면 JavaScript 엔진이 함수 선언문으로 해석하여 문법 오류가 발생하므로, 반드시 괄호로 감싸서 표현식으로 만들어야 합니다.

IIFE의 주요 활용 사례

1. 변수 스코프 격리

IIFE의 가장 중요한 용도 중 하나는 변수의 스코프를 격리하는 것입니다.

(function() {
    var privateVar = "외부에서 접근 불가";
    let anotherVar = "이것도 접근 불가";
    // 이 변수들은 IIFE 외부에서 접근할 수 없음
})();

// console.log(privateVar); // ReferenceError 발생

2. 전역 네임스페이스 오염 방지

전역 스코프에 불필요한 변수나 함수가 추가되는 것을 방지할 수 있습니다.

(function() {
    // 여기서 선언한 변수들은 전역 스코프를 오염시키지 않음
    var helperFunction = function() { 
        return "도움말 함수";
    };
    var config = { 
        apiUrl: "https://api.example.com",
        timeout: 5000
    };
    
    // 필요한 초기화 작업 수행
    console.log("앱이 초기화되었습니다.");
})();

3. 모듈 패턴 구현

IIFE를 사용하여 모듈 패턴을 구현할 수 있습니다. 이는 ES6 모듈이 도입되기 전에 널리 사용된 패턴입니다.

const myModule = (function() {
    let privateCounter = 0;
    let privateArray = [];
    
    // 비공개 함수
    function privateFunction() {
        console.log("이 함수는 외부에서 접근할 수 없습니다.");
    }
    
    // 공개 API 반환
    return {
        increment: function() {
            privateCounter++;
            privateFunction();
        },
        decrement: function() {
            privateCounter--;
        },
        getCount: function() {
            return privateCounter;
        },
        addItem: function(item) {
            privateArray.push(item);
        },
        getItems: function() {
            return [...privateArray]; // 복사본 반환
        }
    };
})();

// 사용 예시
myModule.increment(); // privateFunction 호출됨
console.log(myModule.getCount()); // 1
myModule.addItem("첫 번째 아이템");
console.log(myModule.getItems()); // ["첫 번째 아이템"]

4. 초기화 코드 실행

페이지 로드 시 한 번만 실행되어야 하는 초기화 코드를 작성할 때 유용합니다.

(function() {
    // DOM이 로드된 후 실행될 초기화 코드
    document.addEventListener('DOMContentLoaded', function() {
        console.log("페이지가 완전히 로드되었습니다.");
        
        // 이벤트 리스너 등록
        const buttons = document.querySelectorAll('.action-button');
        buttons.forEach(button => {
            button.addEventListener('click', handleButtonClick);
        });
    });
    
    function handleButtonClick(event) {
        console.log("버튼이 클릭되었습니다:", event.target.textContent);
    }
})();

IIFE의 독립적인 컨텍스트

각각의 IIFE는 완전히 독립적인 실행 컨텍스트와 스코프를 가집니다. 이는 클로저(Closure) 때문입니다.

const module1 = (function() {
    let counter = 0;
    let name = "모듈1";
    
    return {
        increment: function() {
            counter++;
        },
        getInfo: function() {
            return `${name}: ${counter}`;
        }
    };
})();

const module2 = (function() {
    let counter = 100;  // module1과 같은 변수명이지만 완전히 별개
    let name = "모듈2";
    
    return {
        increment: function() {
            counter += 10;
        },
        getInfo: function() {
            return `${name}: ${counter}`;
        }
    };
})();

// 각각 독립적으로 동작
module1.increment();
module1.increment();
console.log(module1.getInfo()); // "모듈1: 2"

module2.increment();
console.log(module2.getInfo()); // "모듈2: 110"

console.log(module1.getInfo()); // "모듈1: 2" (여전히 2, 영향받지 않음)

각 IIFE가 실행될 때마다 새로운 렉시컬 환경이 생성되고, 반환된 함수들은 각각 자신만의 클로저를 형성하여 완전히 독립적인 상태를 유지합니다.

IIFE와 Promise의 결합

Promise에서 IIFE 사용 빈도

일반적으로 Promise를 만들 때 IIFE를 많이 사용하지는 않습니다. Promise 자체가 이미 비동기 작업을 캡슐화하는 역할을 하기 때문입니다.

// 일반적인 Promise 사용 (IIFE 없이)
const fetchData = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("데이터가 로드되었습니다.");
        }, 1000);
    });
};

IIFE와 Promise를 함께 사용하는 경우

그러나 특정 상황에서는 IIFE와 Promise를 함께 사용하는 것이 유용할 수 있습니다.

1. 즉시 실행되는 비동기 작업

(async function() {
    try {
        const response = await fetch('/api/user-data');
        const userData = await response.json();
        console.log("사용자 데이터:", userData);
        
        // 추가 초기화 작업
        initializeUserInterface(userData);
    } catch (error) {
        console.error("사용자 데이터 로드 실패:", error);
        showErrorMessage("데이터를 불러올 수 없습니다.");
    }
})();

2. 모듈 초기화에서 비동기 작업

const dataModule = (function() {
    let cachedData = null;
    let loadingPromise = null;
    
    const loadData = () => {
        if (cachedData) {
            return Promise.resolve(cachedData);
        }
        
        if (loadingPromise) {
            return loadingPromise;
        }
        
        loadingPromise = new Promise((resolve, reject) => {
            // 실제 데이터 로딩 시뮬레이션
            setTimeout(() => {
                const data = {
                    users: ["Alice", "Bob", "Charlie"],
                    timestamp: Date.now()
                };
                cachedData = data;
                loadingPromise = null;
                resolve(data);
            }, 2000);
        });
        
        return loadingPromise;
    };
    
    return {
        getData: loadData,
        clearCache: () => {
            cachedData = null;
            loadingPromise = null;
        }
    };
})();

3. 즉시 실행되는 async IIFE 패턴

복잡한 에러 핸들링이나 여러 단계의 fallback이 필요한 경우에 사용됩니다.

const tagPromise = (async () => {
    const path = "example/file.jpg";
    const row = { log_index: 123 };
    
    try {
        // 첫 번째 시도: 기본 버킷
        await s3.putObjectTagging({
            Bucket: process.env.BUCKET_NAME,
            Key: path,
            Tagging: {
                TagSet: [
                    {
                        Key: 'delete',
                        Value: 'true'
                    }
                ]
            }
        }).promise();
        
        return { success: true, path, logIndex: row.log_index };
        
    } catch (error) {
        if (error.code === 'NoSuchKey') {
            try {
                // 두 번째 시도: 백업 버킷
                await s3.putObjectTagging({
                    Bucket: "backup-bucket.example.com",
                    Key: path,
                    Tagging: {
                        TagSet: [
                            {
                                Key: 'delete',
                                Value: 'true'
                            }
                        ]
                    }
                }).promise();
                
                return { success: true, path, logIndex: row.log_index };
                
            } catch (retryError) {
                console.error(`백업 버킷에서 객체 ${path} 태깅 실패:`, retryError);
                return { 
                    success: false, 
                    path, 
                    logIndex: row.log_index, 
                    error: retryError 
                };
            }
        }
        
        console.error(`객체 ${path} 태깅 실패:`, error);
        return { 
            success: false, 
            path, 
            logIndex: row.log_index, 
            error 
        };
    }
})();

// 사용법
tagPromise.then(result => {
    if (result.success) {
        console.log(`태깅 성공: ${result.path}`);
    } else {
        console.log(`태깅 실패: ${result.path}`, result.error);
    }
});

이 패턴의 특징:

  • tagPromise는 함수가 아닌 Promise 객체입니다
  • 코드가 실행되는 즉시 비동기 작업이 시작됩니다
  • 복잡한 에러 핸들링과 fallback 로직을 깔끔하게 처리할 수 있습니다

현대적인 대안

ES6+ 환경에서는 IIFE 대신 다른 패턴들이 더 선호되기도 합니다.

모듈 시스템 사용

// promiseUtils.js
let requestCounter = 0;

export const createTrackedPromise = (asyncTask) => {
    const id = ++requestCounter;
    return new Promise((resolve, reject) => {
        console.log(`요청 ${id} 시작`);
        asyncTask()
            .then(result => {
                console.log(`요청 ${id} 완료`);
                resolve(result);
            })
            .catch(error => {
                console.log(`요청 ${id} 실패`);
                reject(error);
            });
    });
};

클래스 사용

class PromiseManager {
    constructor() {
        this.activePromises = new Set();
        this.completedCount = 0;
    }
    
    createManagedPromise(asyncTask) {
        const promise = new Promise((resolve, reject) => {
            asyncTask().then(resolve).catch(reject);
        });
        
        this.activePromises.add(promise);
        
        promise.finally(() => {
            this.activePromises.delete(promise);
            this.completedCount++;
        });
        
        return promise;
    }
    
    getActiveCount() {
        return this.activePromises.size;
    }
    
    getCompletedCount() {
        return this.completedCount;
    }
}

const promiseManager = new PromiseManager();

결론

IIFE는 JavaScript에서 스코프 격리, 모듈 패턴 구현, 즉시 실행이 필요한 코드 등에 매우 유용한 패턴입니다. Promise와 함께 사용할 때는 특정 상황(모듈 패턴, 즉시 실행, 복잡한 에러 핸들링)에서 효과적이지만, 현대적인 JavaScript 개발에서는 ES6 모듈 시스템이나 클래스를 활용하는 것이 더 일반적입니다.

IIFE의 핵심은 즉시 실행스코프 격리에 있으며, 이 두 특성을 잘 활용하면 더 깔끔하고 안전한 JavaScript 코드를 작성할 수 있습니다.