// 절차적 접근
const numbers = [1, 2, 3, 4, 5, 6];
const result = [];
for (let i = 0; i < numbers.length; i++) { // ← "i를 0부터 하나씩 증가"
if (numbers[i] % 2 === 0) { // ← "짝수인지 확인해라"
result.push(numbers[i] ** 2); // ← "제곱해서 result에 넣어라"
}
}
// result: [4, 16, 36]// 선언적 접근
const numbers = [1, 2, 3, 4, 5, 6];
const result = numbers
.filter(n => n % 2 === 0) // ← "짝수인 것들만"
.map(n => n ** 2); // ← "제곱한 결과로"
// result: [4, 16, 36]i = 0 → numbers[0] = 1 → 1 % 2 !== 0 → skip
i = 1 → numbers[1] = 2 → 2 % 2 === 0 → result.push(4)
i = 2 → numbers[2] = 3 → 3 % 2 !== 0 → skip
i = 3 → numbers[3] = 4 → 4 % 2 === 0 → result.push(16)
i = 4 → numbers[4] = 5 → 5 % 2 !== 0 → skip
i = 5 → numbers[5] = 6 → 6 % 2 === 0 → result.push(36)
i = 6 → i >= numbers.length → 종료filter 호출
→ 내부적으로 새 배열 생성
→ 1 → predicate(1) = false → 포함 안 함
→ 2 → predicate(2) = true → [2]
→ 3 → predicate(3) = false → 포함 안 함
→ 4 → predicate(4) = true → [2, 4]
→ 5 → predicate(5) = false → 포함 안 함
→ 6 → predicate(6) = true → [2, 4, 6] 반환
map 호출 (filter 결과를 받아서)
→ 내부적으로 새 배열 생성
→ 2 → transform(2) = 4 → [4]
→ 4 → transform(4) = 16 → [4, 16]
→ 6 → transform(6) = 36 → [4, 16, 36] 반환Array.prototype.myFilter = function(predicate) {
const result = [];
for (let i = 0; i < this.length; i++) {
if (predicate(this[i], i, this)) { // ← "어떻게 걸러낼지"는 호출자가 결정
result.push(this[i]);
}
}
return result;
};
// 사용할 때
[1, 2, 3, 4].myFilter(n => n % 2 === 0); // ← "짝수만"이라는 의도만 전달추상화 레이어 구조:
선언적 코드 (의도 표현)
.filter(n => n % 2 === 0)
↓
고차 함수 (추상화)
Array.prototype.filter
↓
절차적 구현 (how)
for loop + predicate 호출
↓
JS 엔진
바이트코드 실행// 절차적 — 기존 배열을 직접 변경
const users = [
{ name: '김현우', active: true },
{ name: '이준호', active: false },
{ name: '박지영', active: true },
];
for (let i = 0; i < users.length; i++) {
if (!users[i].active) {
users.splice(i, 1); // ← 원본 배열을 직접 수정
i--; // ← splice 후 인덱스 보정 필요
}
}// 선언적 — 새 배열을 만든다
const activeUsers = users.filter(user => user.active); // ← 원본은 그대로// 절차적 — mutation으로 인한 버그 가능성
function processUsers(users) {
for (let i = 0; i < users.length; i++) {
if (!users[i].active) {
users.splice(i, 1); // ← 원본을 바꾼다
i--;
}
}
return users;
}
const originalUsers = [...];
const result = processUsers(originalUsers);
// originalUsers도 바뀌어 있다. 함수가 인자를 변이시켰기 때문.
console.log(originalUsers); // ← 예상과 다른 결과// 선언적 — 원본 보존
function processUsers(users) {
return users.filter(user => user.active); // ← 새 배열 반환
}
const originalUsers = [...];
const result = processUsers(originalUsers);
// originalUsers는 그대로다.
console.log(originalUsers); // ← 예상한 결과// 절차적 — 콜백 중첩 (Callback Hell)
getUserById(userId, function(err, user) {
if (err) {
console.error(err);
return;
}
getPostsByUser(user.id, function(err, posts) { // ← 중첩
if (err) {
console.error(err);
return;
}
getCommentsByPost(posts[0].id, function(err, comments) { // ← 더 깊은 중첩
if (err) {
console.error(err);
return;
}
console.log(comments);
});
});
});// 선언적 — Promise 체인
getUserById(userId)
.then(user => getPostsByUser(user.id)) // ← "user를 받아서 posts를 가져와"
.then(posts => getCommentsByPost(posts[0].id)) // ← "posts를 받아서 comments를 가져와"
.then(comments => console.log(comments))
.catch(err => console.error(err)); // ← 에러 처리는 한 곳에서// 더 선언적 — async/await
async function loadComments(userId) {
const user = await getUserById(userId);
const posts = await getPostsByUser(user.id);
const comments = await getCommentsByPost(posts[0].id);
return comments;
}loadComments(userId) 호출
→ 함수 실행 시작
→ await getUserById(userId) 만남
→ 현재 실행 컨텍스트를 저장하고 콜 스택에서 내려옴 ← 비동기 대기
→ getUserById의 Promise가 resolve되면
→ 저장된 실행 컨텍스트를 콜 스택에 다시 올림
→ user 변수에 결과를 바인딩하고 다음 줄 실행
→ ... 반복// 절차적 유효성 검사
function validateUser(user) {
const errors = {};
if (!user.name) {
errors.name = '이름은 필수입니다';
} else if (user.name.length < 2) {
errors.name = '이름은 2자 이상이어야 합니다';
}
if (!user.email) {
errors.email = '이메일은 필수입니다';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(user.email)) {
errors.email = '올바른 이메일 형식이 아닙니다';
}
if (!user.age) {
errors.age = '나이는 필수입니다';
} else if (user.age < 0 || user.age > 150) {
errors.age = '나이는 0~150 사이여야 합니다';
}
return errors;
}// 선언적 유효성 검사 — Zod 사용
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(2, '이름은 2자 이상이어야 합니다'), // ← "name은 2자 이상 문자열"
email: z.string().email('올바른 이메일 형식이 아닙니다'), // ← "email은 이메일 형식"
age: z.number().min(0).max(150), // ← "age는 0~150 숫자"
});
const result = userSchema.safeParse(user);
if (!result.success) {
console.log(result.error.issues);
}구분 | 절차적 (Imperative) | 선언적 (Declarative) |
핵심 질문 | 어떻게 할 것인가? | 무엇을 원하는가? |
상태 관리 | 직접 변이 (mutation) | 변환 후 새 값 (immutability) |
흐름 제어 | 명시적 (for, if, while) | 추상화 안으로 위임 |
에러 처리 | 각 단계마다 명시 | 체인의 끝에서 일괄 처리 가능 |
가독성 | 구현 세부사항이 보임 | 의도가 보임 |
테스트 용이성 | 부수효과로 인해 어려울 수 있음 | 순수 함수 기반으로 쉬움 |
디버깅 | 스텝별 추적 가능 | 추상화 내부 접근이 필요할 수 있음 |
// 주문 목록에서 완료된 주문의 총 금액 계산
const totalAmount = orders
.filter(order => order.status === 'completed') // ← "완료된 것만"
.map(order => order.amount) // ← "금액만 추출"
.reduce((sum, amount) => sum + amount, 0); // ← "합산"// 절차적
const grouped = {};
for (const item of items) {
if (!grouped[item.category]) {
grouped[item.category] = [];
}
grouped[item.category].push(item);
}
// 선언적
const grouped = items.reduce((acc, item) => ({
...acc,
[item.category]: [...(acc[item.category] ?? []), item], // ← 불변 방식으로 누적
}), {});// 이 경우는 절차적이 더 명확하다
async function syncUserData(userId) {
const user = await db.users.findById(userId); // 1. 조회
const externalData = await api.fetchUser(user.externalId); // 2. 외부 데이터
await db.users.update(userId, externalData); // 3. 업데이트
await cache.invalidate(`user:${userId}`); // 4. 캐시 무효화
logger.info(`User ${userId} synced`); // 5. 로깅
}// 함수 내부는 절차적이지만, 외부에는 선언적 인터페이스를 제공
function groupBy(array, keyFn) { // ← 추상화 (선언적 인터페이스)
const result = {};
for (const item of array) { // ← 내부 구현 (절차적)
const key = keyFn(item);
if (!result[key]) result[key] = [];
result[key].push(item);
}
return result;
}
// 사용하는 쪽은 선언적
const byCategory = groupBy(items, item => item.category); // ← "카테고리별로 묶어줘"// 지나치게 체이닝된 코드 — 중간 값이 뭔지 파악하기 어렵다
const result = data
.flatMap(x => x.items)
.filter(item => item.valid)
.map(item => ({ ...item, score: item.value * 1.2 }))
.sort((a, b) => b.score - a.score)
.slice(0, 10)
.reduce((acc, item) => ({ ...acc, [item.id]: item }), {});차적 ㄹconst allItems = data.flatMap(x => x.items);
const validItems = allItems.filter(item => item.valid);
const scoredItems = validItems.map(item => ({ ...item, score: item.value * 1.2 }));
const topItems = scoredItems.sort((a, b) => b.score - a.score).slice(0, 10);
const itemMap = topItems.reduce((acc, item) => ({ ...acc, [item.id]: item }), {});