자바스크립트 면접 단골 질문, 클로저
자바스크립트 기본 문법을 공부한 후 중급 레벨로 진입할 때 가장 먼저 통과해야 하는 관문이 바로 클로저입니다. 문법적 암기가 아닌 자바스크립트의 내부 실행 메커니즘을 토대로 클로저의 개념을 명쾌하게 파헤쳐 봅니다.
클로저의 쉬운 정의
클로저는 "함수가 선언된 당시의 주변 환경(렉시컬 스코프, Lexical Scope)을 기억하여, 해당 스코프 밖에서 호출되더라도 그 환경에 있는 변수에 계속 접근할 수 있는 함수"를 뜻합니다.
자바스크립트는 내부 함수가 외부 변수를 참조하고 있을 경우, 외부 함수의 실행 컨텍스트가 생명 주기를 마감하고 콜 스택에서 해제되어도 내부 스코프 상태 변수를 가비지 컬렉터(GC)에서 제거하지 않고 메모리에 온전히 보관합니다.
실무에서의 핵심 활용: 캡슐화와 정보 은닉
클로저를 활용하면 클래스의 private 지시어처럼 외부에서 특정 변수를 직접 제어하지 못하도록 캡슐화할 수 있습니다. 예를 들어 상태 업데이트 함수를 내부적으로 캡슐화한 카운터 팩토리 함수를 만들어 호출하면, 오직 지정된 통로 함수를 통해서만 변수의 변화를 허용하여 버그 없는 안전한 상태 설계가 가능해집니다.
클로저를 디버깅할 때 보는 것
클로저는 강력하지만, 의도치 않게 오래 살아남는 참조를 만들면 메모리 문제로 이어질 수 있습니다. 특히 이벤트 리스너, 타이머, 캐시 함수에서 외부 변수를 붙잡는 구조는 주기적으로 점검해야 합니다.
실무 활용 예시
- 팩토리 함수로 내부 상태를 감추고, 정해진 메서드로만 변경하게 만들 수 있습니다.
- 디바운스와 스로틀 함수는 이전 타이머 값을 클로저로 기억해 입력 이벤트를 제어합니다.
- React 훅에서 오래된 상태를 참조하는 문제도 클로저 관점으로 이해하면 원인을 찾기 쉽습니다.
클로저를 잘 쓴다는 것은 변수를 숨기는 기술보다 생명 주기를 명확히 설계하는 일에 가깝습니다.
Frontend Note 실무 노트
클로저는 외부 변수를 기억하는 함수라는 설명으로 끝내면 실무 감각이 생기지 않습니다. 실제로는 이벤트 리스너, 타이머, 디바운스, 캐시 함수, React 훅에서 자주 마주칩니다. 클로저가 유용한 순간은 상태를 숨기고 정해진 함수로만 바꾸게 만들 때입니다. 반대로 오래된 값을 계속 붙잡으면 버그가 됩니다.
function createCounter() {
let value = 0;
return {
increase() { value += 1; return value; },
reset() { value = 0; }
};
}
위 코드는 value를 직접 바꾸지 못하게 숨깁니다. 이런 패턴은 작은 상태 머신이나 테스트용 도구에서 유용합니다. 하지만 DOM 이벤트에 큰 객체를 물고 있는 클로저를 등록하고 해제하지 않으면 메모리 누수가 생길 수 있습니다. React에서는 오래된 상태를 참조하는 콜백이 문제를 만들기도 합니다.
디버깅할 때는 함수가 만들어진 시점과 실행되는 시점을 분리해서 봅니다. 비동기 콜백은 나중에 실행되므로 현재 화면의 최신 상태와 다를 수 있습니다. 이 차이를 이해하면 stale closure 문제를 빠르게 찾을 수 있습니다.
React 코드에서 자주 만나는 클로저 문제
React에서 클로저 문제는 오래된 상태를 참조하는 형태로 자주 나타납니다. 예를 들어 타이머 안에서 state를 읽거나, 의존성 배열이 비어 있는 effect 안에서 props를 참조하면 사용자가 보는 최신 값과 콜백이 기억하는 값이 달라질 수 있습니다. 이 문제는 “왜 이전 값이 찍히지?”라는 형태로 나타납니다.
function SearchBox({ keyword }) {
useEffect(() => {
const id = setInterval(() => {
console.log(keyword);
}, 1000);
return () => clearInterval(id);
}, [keyword]);
}
위 예시처럼 콜백이 참조하는 값이 바뀐다면 의존성 배열에 포함해야 합니다. 반대로 매번 effect를 다시 실행하고 싶지 않다면 ref에 최신 값을 저장하는 방식도 있습니다. 중요한 것은 클로저가 값을 “복사”하는 것이 아니라 함수가 만들어진 렉시컬 환경을 참조한다는 점입니다.
디바운스 함수도 클로저의 좋은 예입니다. 이전 타이머 id를 기억해야 다음 입력이 들어왔을 때 기존 작업을 취소할 수 있습니다. 이처럼 클로저는 상태를 숨기고 생명 주기를 관리할 때 유용하지만, 해제하지 않은 이벤트 리스너나 타이머와 결합하면 메모리 문제를 만들 수 있습니다.
디버깅 체크리스트
- 콜백이 언제 만들어지고 언제 실행되는지 분리해서 본다.
- 타이머, 이벤트 리스너, 구독은 cleanup이 있는지 확인한다.
- React effect의 의존성 배열을 의도적으로 비웠는지 확인한다.
- 큰 객체를 오래 참조하는 클로저가 있는지 확인한다.
참고 자료와 업데이트 기준
Frontend Note의 글은 실무 적용 관점에서 작성하며, 관련 기술의 공식 문서와 브라우저 지원 현황이 바뀌면 내용을 다시 점검합니다. 특히 프레임워크 버전, API 정책, 성능 측정 방식은 시간이 지나며 달라질 수 있으므로 적용 전에는 최신 문서를 함께 확인하는 것을 권장합니다.
- MDN Web Docs에서 웹 표준과 브라우저 동작을 확인합니다.
- web.dev에서 성능, 접근성, 사용자 경험 기준을 확인합니다.
- React 공식 문서에서 React 관련 API와 권장 패턴을 확인합니다.
클로저 문제를 디버깅하는 순서
클로저 버그는 값이 틀렸다는 결과만 보이고 원인이 숨어 있는 경우가 많습니다. 먼저 함수가 만들어지는 시점과 실행되는 시점을 나눠서 봅니다. 이벤트 핸들러, 타이머, Promise 콜백, React effect 내부 함수는 대부분 나중에 실행되므로 당시의 변수를 기억하고 있을 수 있습니다. 콘솔에 값만 찍으면 현재 값처럼 보일 때가 있으니, 함수가 등록되는 순간의 식별자와 실행되는 순간의 인자를 함께 출력하는 편이 안전합니다.
실무에서는 클로저를 피해야 할 문법으로 볼 필요는 없습니다. 오히려 디바운스, 메모이제이션, 모듈 내부 상태, 한 번만 초기화되는 설정처럼 유용한 패턴이 많습니다. 다만 상태가 오래 살아남는다는 특성을 의식해야 합니다. DOM 노드, 큰 배열, 이전 API 응답을 클로저가 계속 붙잡고 있으면 메모리 사용량이 늘어날 수 있습니다. React에서는 오래된 상태를 읽는 핸들러가 대표적인 문제입니다. 이럴 때는 함수형 업데이트, ref, 의존성 배열 점검 중 어떤 방식이 의도에 맞는지 선택해야 합니다.
실무 적용 전 최종 점검
이 글의 내용을 프로젝트에 적용하기 전에는 현재 코드의 목적과 사용자 흐름을 먼저 확인해야 합니다. 같은 패턴이라도 관리자 화면, 공개 랜딩 화면, 입력이 많은 폼 화면에서는 우선순위가 다릅니다. 관리자 화면은 반복 작업 속도와 오류 복구가 중요하고, 공개 화면은 초기 로딩과 접근성이 중요하며, 폼 화면은 검증 메시지와 상태 보존이 중요합니다. 따라서 예제 코드를 그대로 붙이기보다 어떤 파일에서 책임을 나눌지, 어떤 값이 외부 입력인지, 실패했을 때 사용자가 무엇을 보게 되는지를 함께 검토하는 것이 좋습니다.
작업 후에는 변경 전후를 비교할 수 있는 작은 기록을 남깁니다. 수정한 컴포넌트, 확인한 브라우저, 실패 케이스, 되돌리는 방법을 적어 두면 다음 배포에서 같은 문제를 빨리 찾을 수 있습니다. 특히 React와 TypeScript 코드는 타입 오류가 사라져도 런타임 흐름이 틀릴 수 있고, CSS 수정은 특정 화면 폭에서만 깨질 수 있습니다. 그래서 로컬 확인, 빌드 확인, 실제 화면 확인을 분리해 진행하는 습관이 필요합니다. Frontend Note의 예시는 이 과정을 돕기 위한 출발점이며, 팀의 코드 스타일과 배포 방식에 맞게 작게 조정해서 사용하는 편이 안전합니다.
작게 적용하는 연습 방법
처음 적용할 때는 전체 화면을 한 번에 바꾸지 말고 작은 컴포넌트 하나를 골라 실험하는 편이 좋습니다. 변경 범위를 좁히면 원인을 추적하기 쉽고, 기존 사용자 흐름에 주는 영향도 줄일 수 있습니다. 예제 코드를 적용한 뒤에는 정상 케이스뿐 아니라 빈 데이터, 긴 텍스트, 느린 네트워크, 모바일 화면을 함께 확인합니다. 이런 조건에서 문제가 없다면 같은 패턴을 다른 화면으로 확장해도 유지보수 부담이 적습니다.