자바스크립트 퀴즈북 리마인드 Day 13: 모듈은 파일 묶음이.
ESM live binding, default/named export, circular dependency, dynamic import, CJS와 ESM 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.
오늘의 질문: “함수를 인자로 넘기는 코드가 항상 더 함수형이고 안전할까?”
function withLoading(run) {
setLoading(true);
return run().finally(() => {
setLoading(false);
});
}
withLoading(() => fetchUser(userId));
겉으로는 단순한 고차 함수입니다. 그런데 리뷰할 때는 한 가지를 더 봐야 합니다.
run은 값을 계산하는 함수일까, 아니면 네트워크와 UI 상태를 건드리는 부수 효과 묶음일까?
함수는 자바스크립트에서 값입니다. 변수에 담고, 인자로 넘기고, 반환할 수 있습니다. 그래서 함수는 추상화를 만들기 쉽지만, 동시에 부수 효과를 예쁘게 숨기기도 쉽습니다.
함수로 감싸는 것 자체는 설계가 아닙니다. 좋은 함수는 호출자가 무엇을 넣고 무엇을 받는지, 그리고 어디에서 부수 효과가 생기는지 설명할 수 있어야 합니다.
function getDisplayName(user) {
console.log("render user", user.id);
localStorage.setItem("last-user", user.id);
return `${user.name} (${user.role})`;
}
이 함수는 이름만 보면 문자열을 만드는 순수 계산처럼 보입니다. 하지만 로그와 storage write가 섞여 있습니다. 테스트에서는 문자열만 검증하고 넘어가도, 실제 앱에서는 렌더마다 storage가 갱신되는 문제가 생길 수 있습니다.
리뷰에서는 먼저 이렇게 나눕니다.
function formatDisplayName(user) {
return `${user.name} (${user.role})`;
}
function recordUserView(user) {
console.log("render user", user.id);
localStorage.setItem("last-user", user.id);
}
순수 계산과 부수 효과를 분리하면 함수의 역할이 작아지고, 테스트도 명확해집니다.
고차 함수는 함수를 받거나 함수를 돌려주는 함수입니다. 중요한 점은 “멋진 문법”이 아니라 반복되는 관계를 드러내는 데 있습니다.
const byStatus = (status) => (item) => item.status === status;
const visibleTodos = todos.filter(byStatus("visible"));
const archivedTodos = todos.filter(byStatus("archived"));
여기서는 status 조건을 만드는 반복 규칙이 드러납니다. 반대로 모든 코드를 콜백으로 감싸서 흐름을 끊는다면 오히려 읽기 어려워집니다.
run(() => validate(() => submit(() => notify())));
이런 코드는 “단계가 있다”는 사실은 보이지만, 각 단계의 입력과 실패 처리가 흐려집니다. 고차 함수는 흐름을 감추는 장식이 아니라, 반복되는 호출 규칙을 이름 붙이는 도구로 쓰는 편이 안전합니다.
async function은 항상 Promise를 반환합니다.
async function loadUser(id) {
return fetch(`/api/users/${id}`).then((r) => r.json());
}
const user = loadUser("1");
여기서 user는 실제 사용자 객체가 아니라 Promise입니다. 간단하지만 리뷰에서 자주 보이는 버그입니다. 특히 상태 업데이트와 섞이면 문제가 커집니다.
function renderProfile(id) {
const user = loadUser(id);
return `<h1>${user.name}</h1>`;
}
이 코드는 타입스크립트를 쓰면 잡히기 쉽지만, 자바스크립트 코드나 느슨한 경계에서는 그대로 들어올 수 있습니다. 리뷰 질문은 “이 함수가 동기 값을 반환하는가, 비동기 작업의 약속을 반환하는가?”입니다.
프론트엔드 코드에서는 완전히 순수한 앱을 만들 수 없습니다. fetch, DOM, storage, analytics, timer가 필요합니다. 다만 부수 효과를 어디에 둘지는 선택할 수 있습니다.
| 위치 | 좋은 신호 | 위험 신호 |
|---|---|---|
| 순수 계산 함수 | 입력만으로 결과가 결정됨 | 내부에서 storage, DOM, log를 만짐 |
| 이벤트 핸들러 | 부수 효과 시작점이 명확함 | 계산 로직까지 길게 섞임 |
| API wrapper | 네트워크/파싱/에러 모양이 드러남 | 호출자가 모르는 retry/cache가 숨음 |
| 고차 함수 | 반복되는 호출 규칙에 이름을 붙임 | callback nesting으로 흐름이 흐려짐 |
| async 함수 | Promise 반환과 실패 처리가 명확함 | 동기 값처럼 호출하거나 에러를 삼킴 |
핵심은 “부수 효과를 없애자”가 아닙니다. 부수 효과가 시작되는 위치를 코드에서 알아볼 수 있게 하자입니다.
퀴즈북의 함수 단원은 “함수도 값이다”에서 시작하지만, 프론트엔드 리뷰에서는 여기서 한 걸음 더 가야 합니다. 함수를 값으로 넘기면 코드 조각만 넘기는 게 아니라 나중에 실행될 수 있는 권한도 넘깁니다.
function once(fn) {
let called = false;
return (...args) => {
if (called) return;
called = true;
return fn(...args);
};
}
const submitOnce = once(() => submitForm(formState));
이 코드는 “한 번만 실행”이라는 규칙을 잘 드러냅니다. 하지만 submitForm(formState)가 읽는 formState가 언제의 값인지도 같이 봐야 합니다. Day 7의 클로저 관점과 연결하면 질문이 더 선명해집니다.
fn은 만들어진 시점의 값을 닫아두는가?예를 들어 최신 form 값을 써야 한다면 아래처럼 실행 시점에 인자로 넘기는 편이 더 드러날 수 있습니다.
const submitOnce = once((state) => submitForm(state));
submitOnce(getCurrentFormState());
작은 차이처럼 보이지만, 함수형 추상화와 stale closure 버그는 자주 붙어 다닙니다. 함수를 값으로 다루는 코드는 “무엇을 넘겼는가”뿐 아니라 “언제 읽는가”를 함께 리뷰해야 합니다.
고차 함수 wrapper는 중복을 줄이는 데 좋지만, 실패를 뭉개면 위험합니다.
function safely(run) {
try {
return run();
} catch (error) {
reportError(error);
return null;
}
}
동기 함수만 받는다면 의도가 분명할 수 있습니다. 그런데 호출자가 async 함수를 넘기면 얘기가 달라집니다.
safely(async () => {
await saveProfile();
throw new Error("save failed");
});
try/catch는 async 함수가 반환한 Promise의 나중 rejection을 잡지 못합니다. wrapper 이름은 safely인데 실제로는 비동기 실패가 바깥으로 흘러갈 수 있습니다. 이 경우에는 Promise를 인식하는 wrapper여야 합니다.
async function safelyAsync(run) {
try {
return await run();
} catch (error) {
reportError(error);
return null;
}
}
리뷰에서는 wrapper가 받는 함수의 계약을 확인해야 합니다. 동기 callback인지, Promise-returning callback인지, 실패를 호출자에게 던지는지, 내부에서 보고하고 삼키는지까지 이름과 타입/문서에 드러나야 합니다.
bind, constructor, method extraction도 함수 경계 문제다함수 단원에서 this, constructor, bound function은 문법 암기처럼 보이지만 실제 리뷰에서는 경계 문제로 나타납니다.
const user = {
name: "Jingyu",
greet() {
return `안녕, ${this.name}`;
},
};
const greet = user.greet;
greet();
메서드를 값으로 빼는 순간 call-site가 바뀌고 this도 달라집니다. Day 9의 this 규칙과 이어지는 지점입니다. 해결은 상황에 따라 다릅니다.
const greet = user.greet.bind(user);
또는 애초에 this에 의존하지 않는 함수로 바꿀 수도 있습니다.
function greetUser(user) {
return `안녕, ${user.name}`;
}
둘 중 무엇이 낫다는 규칙은 없습니다. 다만 함수가 값으로 이동할 때 숨은 입력이 있는가를 봐야 합니다. this, 전역 상태, module-level cache, closure는 모두 함수 시그니처 밖에 있는 입력입니다. 함수가 경계라면, 좋은 경계는 숨은 입력을 줄이거나 의도적으로 이름 붙입니다.
AI가 리팩터링한 코드에는 “함수로 쪼개서 좋아 보이지만 계약은 흐려진” 경우가 많습니다.
const pipe =
(...fns) =>
(value) =>
fns.reduce((current, fn) => fn(current), value);
const run = pipe(validate, save, notify);
validate, save, notify가 모두 동기 pure function이면 읽기 쉽습니다. 하지만 save가 Promise를 반환하고 notify가 그 결과를 기다려야 한다면 이 pipe는 틀립니다. async-aware pipe가 필요하거나, 그냥 명시적인 async 함수가 더 낫습니다.
async function run(value) {
const valid = validate(value);
const saved = await save(valid);
notify(saved);
return saved;
}
추상화가 적어 보이지만 실패 지점, await 경계, 반환값이 더 선명합니다. 함수형 도구를 쓰는 목적은 코드를 짧게 만드는 것이 아니라 반복되는 관계를 안전하게 표현하는 것입니다. AI 코드 리뷰에서는 “멋진 helper가 생겼다”보다 “이 helper가 동기/비동기, 성공/실패, 입력/출력 계약을 보존하는가”를 먼저 봐야 합니다.
this, closure, module cache 같은 숨은 입력이 함수 시그니처 밖에 남아 있지 않은가?함수는 작은 문법 조각이 아니라 아키텍처의 경계입니다. 값을 계산하는 core와 세상을 바꾸는 shell을 나눌수록 리뷰할 때 “무엇이 바뀌고 무엇이 계산되는지”가 선명해집니다.
function getTitle(post) {
localStorage.setItem('last-post', post.id);
return post.title.trim();
}Post Q&A
자바스크립트 퀴즈북 리마인드 Day 10: 함수는 값이고 경계다 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!
ESM live binding, default/named export, circular dependency, dynamic import, CJS와 ESM 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.
DOM 이벤트의 capture/bubble, delegation, default action, React synthetic event 경계를 프론트엔드 코드 리뷰 관점에서 정리합니다.
프로토타입 조회, class 문법, 인스턴스 필드와 prototype 메서드 차이를 프론트엔드 코드 리뷰 관점에서 정리합니다.
고차 함수, 순수 함수, 부수 효과, async 함수의 Promise 반환을 코드 리뷰에서 어떻게 읽을지 정리합니다.
this 바인딩을 정의 위치가 아니라 호출 형태, 메서드 분리, arrow function, bind 기준으로 읽는 방법을 코드 리뷰 관점에서 정리합니다.
콜 스택, 태스크 큐, 마이크로태스크 큐를 기준으로 Promise와 setTimeout 실행 순서를 코드 리뷰 관점에서 정리합니다.
클로저가 값 복사가 아니라 렉시컬 환경 참조라는 점을 stale closure, 루프 콜백, React 이벤트 리뷰 관점에서 정리합니다.
스코프 체인, 렉시컬 환경, var/let/const 호이스팅 차이를 콜백·상태 버그 리뷰 관점에서 다시 정리합니다.
객체 참조와 얕은 복사, Object와 Map/Set의 차이, WeakMap/WeakSet이 필요한 메모리 상황을 프론트엔드 코드 리뷰 관점에서 정리합니다.
객체 프로퍼티를 key/value가 아니라 descriptor로 읽는 법, getter/setter와 defineProperty, preventExtensions/seal/freeze의 경계를 코드 리뷰 관점에서 정리합니다.
객체가 원시값으로 바뀌는 ToPrimitive 흐름, + 연산자의 문자열 연결과 숫자 덧셈 분기, Symbol.toPrimitive가 코드 리뷰에서 왜 중요한지 정리합니다.
==, ===, Object.is, SameValueZero가 각각 어떤 비교 알고리즘을 쓰는지 정리하고, includes와 indexOf, Map/Set 키 비교에서 생기는 프론트엔드 리뷰 포인트를 잡습니다.
자바스크립트의 Number가 왜 정수처럼 보여도 부동소수점 모델 위에서 움직이는지, NaN과 -0, safe integer, BigInt를 프론트엔드 코드 리뷰 관점에서 다시 정리합니다.