이미지 로딩 중...
AI Generated
2025. 11. 5. · 5 Views
JavaScript 핵심 개념 완벽 정리
JavaScript를 처음 배우는 개발자들을 위한 필수 개념 가이드입니다. 변수 선언부터 비동기 처리까지, 실무에서 가장 많이 사용하는 JavaScript의 핵심 개념들을 실전 예제와 함께 쉽고 친절하게 설명합니다. 이 가이드 하나면 JavaScript 기초를 완벽하게 마스터할 수 있습니다.
목차
- let과 const - 변수 선언의 올바른 방법
- 화살표 함수 - 간결한 함수 작성법
- 구조 분해 할당 - 데이터 추출의 혁신
- 스프레드 연산자 - 배열과 객체 복사
- 템플릿 리터럴 - 문자열 처리의 새로운 방식
- Promise - 비동기 처리의 기본
- async/await - 비동기 코드를 동기처럼
- 배열 메서드 - map, filter, reduce
- let과 const
- 화살표 함수
- 구조 분해 할당
- 스프레드 연산자
- 템플릿 리터럴
- Promise
- async/await
- 배열 메서드 - map, filter, reduce
1. let과 const - 변수 선언의 올바른 방법
2. 화살표 함수 - 간결한 함수 작성법
3. 구조 분해 할당 - 데이터 추출의 혁신
4. 스프레드 연산자 - 배열과 객체 복사
5. 템플릿 리터럴 - 문자열 처리의 새로운 방식
6. Promise - 비동기 처리의 기본
7. async/await - 비동기 코드를 동기처럼
8. 배열 메서드 - map, filter, reduce
1. let과 const
시작하며
여러분이 JavaScript로 프로젝트를 개발하다가 변수 값이 예상치 못하게 변경되어 버그를 찾느라 몇 시간을 허비한 경험이 있나요? 특히 팀 프로젝트에서 누군가 실수로 전역 변수를 덮어써서 전체 애플리케이션이 오작동하는 상황은 정말 끔찍합니다.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 과거에 사용하던 var 키워드는 함수 스코프를 가지고 있어서, 블록 밖에서도 접근이 가능하고 재선언이 자유로워 의도치 않은 버그를 만들어냅니다.
게다가 호이스팅으로 인해 선언 전에 변수를 사용할 수 있어서 코드의 흐름을 이해하기 어렵게 만듭니다. 바로 이럴 때 필요한 것이 let과 const입니다.
이 두 키워드는 블록 스코프를 제공하여 변수의 생명주기를 명확하게 관리하고, 예측 가능한 코드를 작성할 수 있게 해줍니다.
개요
간단히 말해서, let과 const는 ES6에서 도입된 새로운 변수 선언 방식으로, var의 문제점을 해결하고 더 안전한 코드를 작성할 수 있게 해주는 키워드입니다. let은 재할당이 가능한 변수를 선언할 때 사용하고, const는 재할당이 불가능한 상수를 선언할 때 사용합니다.
예를 들어, 사용자의 나이처럼 변경될 수 있는 값은 let으로, API 엔드포인트 URL처럼 변하지 않는 값은 const로 선언하는 것이 좋습니다. 기존에는 var로 모든 변수를 선언했다면, 이제는 기본적으로 const를 사용하고 재할당이 필요한 경우에만 let을 사용하는 것이 권장됩니다.
var는 더 이상 사용하지 않는 것이 좋습니다. 이들의 핵심 특징은 블록 스코프, 재선언 불가, 그리고 호이스팅 시 초기화되지 않는 TDZ(Temporal Dead Zone)입니다.
이러한 특징들이 코드의 예측 가능성을 높이고, 버그를 사전에 방지하며, 코드의 의도를 명확하게 전달할 수 있게 해줍니다.
코드 예제
// const: 재할당 불가능한 상수 선언
const API_URL = 'https://api.example.com';
const MAX_RETRY = 3;
// let: 재할당 가능한 변수 선언
let userAge = 25;
let isLoggedIn = false;
// 블록 스코프 동작 예시
if (true) {
const blockVar = 'block scope';
let count = 0;
console.log(blockVar); // 'block scope'
}
// console.log(blockVar); // ReferenceError: blockVar is not defined
// 루프에서의 사용
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 0, 1, 2 출력
}
설명
이것이 하는 일: let과 const는 변수의 스코프를 블록 단위로 제한하여, 변수가 선언된 중괄호 {} 내부에서만 접근 가능하도록 만듭니다. 이를 통해 변수의 생명주기를 명확하게 관리하고 의도치 않은 변수 접근을 방지합니다.
첫 번째로, const로 선언된 변수는 선언과 동시에 초기화되어야 하며, 한 번 할당된 값을 변경할 수 없습니다. API_URL이나 MAX_RETRY처럼 프로그램 전체에서 변하지 않아야 하는 값을 선언할 때 사용합니다.
이렇게 하면 실수로 값을 변경하는 것을 방지하고, 코드를 읽는 사람에게 "이 값은 변하지 않는다"는 의도를 명확히 전달할 수 있습니다. 그 다음으로, let은 재할당이 필요한 변수를 선언할 때 사용합니다.
예제의 userAge나 isLoggedIn처럼 프로그램 실행 중에 값이 변경될 수 있는 경우에 적합합니다. let도 const와 마찬가지로 블록 스코프를 가지므로, if문이나 for문 내부에서 선언하면 해당 블록을 벗어나면 접근할 수 없습니다.
세 번째 단계와 최종 결과로, 블록 스코프는 코드의 안정성을 크게 향상시킵니다. 예제의 for 루프에서 let으로 선언된 i는 각 반복마다 새로운 바인딩을 생성하므로, setTimeout 콜백이 실행될 때 각각 올바른 값(0, 1, 2)을 출력합니다.
var를 사용했다면 모두 3이 출력되었을 것입니다. 여러분이 이 코드를 사용하면 변수의 생명주기를 명확하게 관리할 수 있고, 의도치 않은 재할당을 방지할 수 있으며, 블록 단위로 변수를 격리하여 더 안전한 코드를 작성할 수 있습니다.
실무에서는 기본적으로 const를 사용하고, 재할당이 필요할 때만 let을 사용하는 습관을 들이면 코드 품질이 크게 향상됩니다.
실전 팁
💡 기본적으로 const를 사용하고, 재할당이 필요한 경우에만 let을 사용하세요. 이렇게 하면 코드의 의도가 명확해지고 실수로 인한 버그를 방지할 수 있습니다. 💡 const로 선언한 객체나 배열의 내부 속성은 변경 가능합니다. 완전한 불변성이 필요하다면 Object.freeze()를 사용하세요. 💡 루프에서는 항상 let을 사용하세요. var를 사용하면 클로저 문제로 인해 예상치 못한 동작이 발생할 수 있습니다. 💡 전역 스코프에서 변수를 선언할 때는 더욱 신중하게 const를 사용하여 의도치 않은 재할당을 방지하세요. 💡 ESLint의 prefer-const 규칙을 활성화하면 재할당하지 않는 let 변수를 자동으로 const로 변경하도록 제안받을 수 있습니다.
2. 화살표 함수
시작하며
여러분이 이벤트 핸들러나 콜백 함수를 작성할 때, this 키워드가 예상과 다르게 동작해서 'Cannot read property of undefined' 에러를 만난 적이 있나요? 특히 React 클래스 컴포넌트에서 메서드를 이벤트 핸들러로 전달할 때 이런 문제가 자주 발생합니다.
이런 문제는 JavaScript의 this 바인딩 규칙 때문에 발생합니다. 일반 함수는 호출 방식에 따라 this가 동적으로 결정되므로, 함수를 다른 곳으로 전달하면 this가 예상과 다른 객체를 가리키게 됩니다.
이를 해결하기 위해 bind()를 사용하거나 클로저를 만들어야 했고, 이는 코드를 복잡하게 만들었습니다. 바로 이럴 때 필요한 것이 화살표 함수입니다.
화살표 함수는 자신만의 this를 생성하지 않고 상위 스코프의 this를 그대로 사용하므로, this 바인딩 문제를 근본적으로 해결해줍니다.
개요
간단히 말해서, 화살표 함수는 => 문법을 사용하여 함수를 더 간결하게 작성할 수 있게 해주는 ES6의 새로운 함수 표현식으로, 렉시컬 this 바인딩을 제공합니다. 화살표 함수가 필요한 이유는 코드의 간결성과 this 바인딩의 예측 가능성입니다.
예를 들어, 배열의 map, filter, reduce 같은 고차 함수에서 콜백을 작성할 때, 화살표 함수를 사용하면 코드가 훨씬 읽기 쉬워지고 this 관련 버그를 방지할 수 있습니다. 기존에는 function 키워드로 함수를 선언하고 bind(this)를 사용해야 했다면, 이제는 화살표 함수로 간단하게 해결할 수 있습니다.
특히 React의 함수형 컴포넌트나 콜백 함수에서 매우 유용합니다. 화살표 함수의 핵심 특징은 렉시컬 this, 간결한 문법, 암묵적 반환입니다.
이러한 특징들이 코드를 더 읽기 쉽게 만들고, this 관련 버그를 예방하며, 함수형 프로그래밍 스타일을 자연스럽게 적용할 수 있게 해줍니다.
코드 예제
// 기본 화살표 함수 문법
const greet = (name) => {
return `Hello, ${name}!`;
};
// 암묵적 반환 (중괄호와 return 생략)
const multiply = (a, b) => a * b;
// 배열 메서드와 함께 사용
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(num => num * 2);
const evens = numbers.filter(num => num % 2 === 0);
// 렉시컬 this 예시
class Counter {
constructor() {
this.count = 0;
// 화살표 함수는 상위 스코프의 this를 사용
setInterval(() => {
this.count++;
console.log(this.count); // Counter의 this를 올바르게 참조
}, 1000);
}
}
설명
이것이 하는 일: 화살표 함수는 함수 표현식을 더 간결하게 작성하면서도, this 키워드가 함수가 정의된 위치의 스코프를 참조하도록 만듭니다. 이를 통해 this 바인딩 관련 혼란을 제거하고 코드의 의도를 명확하게 표현합니다.
첫 번째로, 기본 문법에서 function 키워드 대신 =>를 사용하여 함수를 선언합니다. 매개변수가 하나면 괄호를 생략할 수 있고, 함수 본문이 단일 표현식이면 중괄호와 return 키워드도 생략할 수 있습니다.
multiply 함수처럼 a * b라고만 작성하면 자동으로 결과가 반환됩니다. 이렇게 하면 코드가 훨씬 간결해지고 읽기 쉬워집니다.
그 다음으로, 배열 메서드와 함께 사용할 때 화살표 함수의 진가가 드러납니다. map(num => num * 2)처럼 한 줄로 간결하게 변환 로직을 표현할 수 있어서, 데이터 처리 코드가 선언적이고 직관적으로 변합니다.
일반 함수로 작성했다면 훨씬 더 장황했을 코드가 단 한 줄로 표현됩니다. 세 번째 단계와 최종 결과로, Counter 클래스 예시에서 가장 중요한 특징인 렉시컬 this를 볼 수 있습니다.
setInterval 내부의 화살표 함수는 자신의 this를 만들지 않고 Counter 생성자의 this를 그대로 사용합니다. 일반 함수를 사용했다면 this.count++에서 에러가 발생했을 것이지만, 화살표 함수를 사용하면 자연스럽게 Counter 인스턴스의 count를 증가시킬 수 있습니다.
여러분이 이 코드를 사용하면 함수 작성이 훨씬 간결해지고, this 관련 버그를 원천적으로 방지할 수 있으며, 콜백 함수나 고차 함수를 사용할 때 코드의 가독성이 크게 향상됩니다. 실무에서는 대부분의 경우 화살표 함수를 사용하되, 메서드로 사용하거나 생성자 함수가 필요한 경우에만 일반 함수를 사용하는 것이 좋습니다.
실전 팁
💡 객체의 메서드를 정의할 때는 화살표 함수를 사용하지 마세요. 메서드에서는 호출한 객체를 this로 참조해야 하는데, 화살표 함수는 상위 스코프의 this를 사용하므로 의도와 다르게 동작합니다. 💡 화살표 함수는 생성자로 사용할 수 없습니다. new 키워드와 함께 사용하면 에러가 발생하므로, 클래스를 만들 때는 일반 함수나 class 문법을 사용하세요. 💡 매개변수가 하나일 때는 괄호를 생략할 수 있지만, 팀 코딩 컨벤션에 따라 일관성 있게 작성하세요. ESLint의 arrow-parens 규칙을 사용하면 좋습니다. 💡 객체를 암묵적으로 반환할 때는 소괄호로 감싸야 합니다. const getUser = () => ({ name: 'John', age: 30 })처럼 작성하지 않으면 중괄호가 함수 본문으로 인식됩니다. 💡 화살표 함수는 arguments 객체를 가지지 않습니다. 가변 인자가 필요하면 나머지 매개변수(...args)를 사용하세요.
3. 구조 분해 할당
시작하며
여러분이 API 응답 객체에서 필요한 데이터만 추출하려고 할 때, response.data.user.name, response.data.user.email처럼 긴 경로를 반복해서 작성하느라 지치신 적 있나요? 특히 깊게 중첩된 객체나 배열에서 여러 값을 꺼내야 할 때는 코드가 길어지고 실수하기 쉽습니다.
이런 문제는 실제 개발 현장에서 매우 흔하게 발생합니다. 객체나 배열에서 값을 하나씩 꺼내서 변수에 할당하는 과정은 반복적이고 지루하며, 코드의 가독성을 떨어뜨립니다.
게다가 깊은 중첩 구조에서는 null이나 undefined 체크를 빼먹어 런타임 에러가 발생하기 쉽습니다. 바로 이럴 때 필요한 것이 구조 분해 할당입니다.
이 기능을 사용하면 객체나 배열의 값을 간결한 문법으로 추출하여 변수에 할당할 수 있어, 코드가 훨씬 깔끔하고 읽기 쉬워집니다.
개요
간단히 말해서, 구조 분해 할당은 배열이나 객체의 속성을 해체하여 개별 변수에 담을 수 있게 해주는 JavaScript 표현식으로, 데이터 추출을 한 줄로 간결하게 처리할 수 있습니다. 이 기능이 필요한 이유는 코드의 간결성과 가독성 향상입니다.
예를 들어, React 컴포넌트에서 props를 받을 때 const { name, age, email } = props처럼 한 번에 여러 값을 추출할 수 있어 매우 편리합니다. 함수 매개변수, API 응답 처리, 상태 관리 등 거의 모든 곳에서 유용하게 사용됩니다.
기존에는 const name = user.name; const age = user.age;처럼 각각 변수를 선언해야 했다면, 이제는 const { name, age } = user;로 한 번에 처리할 수 있습니다. 배열의 경우도 const first = arr[0]; const second = arr[1]; 대신 const [first, second] = arr;로 간단하게 작성할 수 있습니다.
핵심 특징은 기본값 설정, 이름 변경, 나머지 요소 수집, 중첩 구조 분해입니다. 이러한 특징들이 null/undefined 안전성을 제공하고, 변수명 충돌을 방지하며, 필요한 데이터만 선택적으로 추출할 수 있게 해줍니다.
코드 예제
// 객체 구조 분해 할당
const user = { name: 'John', age: 30, email: 'john@example.com' };
const { name, age, email } = user;
// 기본값 설정
const { name: userName, role = 'user' } = user; // role이 없으면 'user' 사용
// 배열 구조 분해 할당
const colors = ['red', 'green', 'blue'];
const [first, second, third] = colors;
// 일부 요소 건너뛰기
const [primary, , tertiary] = colors; // green 건너뛰기
// 나머지 요소 수집
const [head, ...tail] = [1, 2, 3, 4, 5];
// head = 1, tail = [2, 3, 4, 5]
// 함수 매개변수에서 사용
function displayUser({ name, age }) {
console.log(`${name} is ${age} years old`);
}
displayUser(user);
// 중첩 객체 분해
const response = { data: { user: { id: 1, profile: { bio: 'Developer' } } } };
const { data: { user: { profile: { bio } } } } = response;
설명
이것이 하는 일: 구조 분해 할당은 배열이나 객체의 내부 값을 패턴 매칭 방식으로 추출하여, 여러 변수를 동시에 선언하고 초기화합니다. 이를 통해 데이터 접근 코드를 대폭 줄이고 의도를 명확하게 표현할 수 있습니다.
첫 번째로, 객체 구조 분해는 중괄호 {}를 사용하여 속성 이름과 일치하는 변수를 만듭니다. const { name, age, email } = user;는 user.name을 name 변수에, user.age를 age 변수에 자동으로 할당합니다.
콜론(:)을 사용하면 변수 이름을 바꿀 수 있고, 등호(=)로 기본값을 설정할 수 있어서 속성이 없거나 undefined일 때 대비할 수 있습니다. 그 다음으로, 배열 구조 분해는 대괄호 []를 사용하여 위치에 따라 값을 추출합니다.
const [first, second, third] = colors;는 배열의 인덱스 순서대로 변수에 할당하며, 쉼표로 요소를 건너뛸 수도 있습니다. 스프레드 연산자(...)를 사용하면 나머지 요소를 새 배열로 수집할 수 있어서, 첫 번째 요소와 나머지를 분리하는 패턴에서 매우 유용합니다.
세 번째로, 함수 매개변수에서 구조 분해를 사용하면 코드가 훨씬 깔끔해집니다. displayUser({ name, age })처럼 작성하면 함수 내부에서 user.name 대신 바로 name을 사용할 수 있습니다.
React 함수형 컴포넌트에서 props를 받을 때도 function Component({ title, onClick })처럼 작성하면 props.title 대신 title을 바로 사용할 수 있어 매우 편리합니다. 여러분이 이 코드를 사용하면 반복적인 객체 접근 코드를 줄일 수 있고, 필요한 값만 명시적으로 추출하여 코드의 의도를 명확히 할 수 있으며, 기본값 설정으로 undefined 에러를 방지할 수 있습니다.
실무에서는 API 응답 처리, 함수 매개변수, React props 등 거의 모든 곳에서 구조 분해 할당을 활용하면 코드 품질이 크게 향상됩니다.
실전 팁
💡 깊게 중첩된 객체를 분해할 때는 Optional Chaining(?.)과 함께 사용하세요. const { data: { user } = {} } = response; 처럼 작성하면 data가 없어도 에러가 발생하지 않습니다. 💡 함수의 반환값이 배열이나 객체라면 구조 분해로 받으세요. const [error, result] = await fetchData();처럼 작성하면 Go 언어 스타일의 에러 처리가 가능합니다. 💡 여러 개의 값을 교환할 때 임시 변수 없이 [a, b] = [b, a];로 간단하게 처리할 수 있습니다. 매우 유용한 패턴입니다. 💡 구조 분해한 변수명이 너무 짧거나 모호하면 콜론으로 의미 있는 이름으로 변경하세요. const { n: userName }보다는 명확한 이름을 사용하는 것이 좋습니다. 💡 React에서 useState의 반환값을 구조 분해할 때는 const [state, setState] = useState();처럼 일관된 명명 규칙을 사용하세요.
4. 스프레드 연산자
시작하며
여러분이 배열이나 객체를 복사하거나 합칠 때, 원본 데이터가 의도치 않게 변경되어 예상치 못한 버그를 만난 적 있나요? 특히 React에서 상태를 업데이트할 때 원본 배열을 직접 수정하면 렌더링이 제대로 되지 않아 몇 시간을 디버깅한 경험이 있을 겁니다.
이런 문제는 JavaScript의 참조 타입 특성 때문에 발생합니다. 배열이나 객체를 단순히 변수에 할당하면 실제 값이 복사되는 게 아니라 메모리 주소가 복사되므로, 한쪽을 수정하면 다른 쪽도 영향을 받습니다.
concat(), Object.assign() 같은 메서드로 해결할 수 있지만 코드가 복잡해집니다. 바로 이럴 때 필요한 것이 스프레드 연산자입니다.
세 개의 점(...)만으로 배열이나 객체를 펼쳐서 새로운 배열이나 객체를 만들 수 있어, 불변성을 지키면서도 간결한 코드를 작성할 수 있습니다.
개요
간단히 말해서, 스프레드 연산자는 ...을 사용하여 배열이나 객체의 요소를 개별적으로 펼쳐주는 ES6 문법으로, 복사, 병합, 확장을 한 줄로 처리할 수 있게 해줍니다. 이 연산자가 필요한 이유는 불변성 유지와 코드 간결화입니다.
예를 들어, React에서 상태를 업데이트할 때 setState([...items, newItem])처럼 작성하면 기존 배열을 변경하지 않고 새 배열을 만들어 불변성을 보장합니다. Redux, Zustand 같은 상태 관리 라이브러리에서도 필수적으로 사용됩니다.
기존에는 arr1.concat(arr2)나 Object.assign({}, obj1, obj2)처럼 메서드를 사용해야 했다면, 이제는 [...arr1, ...arr2]나 {...obj1, ...obj2}로 직관적으로 작성할 수 있습니다. 특히 중첩된 구조에서 일부만 업데이트할 때 매우 유용합니다.
핵심 특징은 얕은 복사, 배열/객체 병합, 함수 인자 전개, 나머지 요소와의 조합입니다. 이러한 특징들이 불변 데이터 구조를 쉽게 만들고, 여러 데이터를 조합하며, 가변 인자 함수를 유연하게 처리할 수 있게 해줍니다.
코드 예제
// 배열 복사 (얕은 복사)
const original = [1, 2, 3];
const copied = [...original]; // 새로운 배열 생성
copied.push(4); // original은 변경되지 않음
// 배열 병합
const fruits = ['apple', 'banana'];
const vegetables = ['carrot', 'potato'];
const food = [...fruits, ...vegetables];
// 배열에 요소 추가
const numbers = [1, 2, 3];
const moreNumbers = [0, ...numbers, 4, 5]; // [0, 1, 2, 3, 4, 5]
// 객체 복사 및 병합
const user = { name: 'John', age: 30 };
const updatedUser = { ...user, age: 31, city: 'Seoul' };
// { name: 'John', age: 31, city: 'Seoul' }
// 함수 인자로 배열 전개
const nums = [1, 2, 3, 4, 5];
console.log(Math.max(...nums)); // 5
// React에서의 실전 사용
const [items, setItems] = useState([]);
const addItem = (newItem) => {
setItems([...items, newItem]); // 불변성 유지
};
설명
이것이 하는 일: 스프레드 연산자는 iterable한 객체(배열, 문자열 등)나 객체의 모든 요소를 개별적으로 펼쳐서, 새로운 배열이나 객체를 생성합니다. 이를 통해 원본 데이터를 변경하지 않고도 복사, 병합, 확장을 수행할 수 있습니다.
첫 번째로, 배열 복사에서 [...original]은 original의 모든 요소를 새 배열에 담아 반환합니다. 이는 얕은 복사(shallow copy)이므로 1차원 데이터에서는 완전히 독립적인 배열이 만들어지지만, 중첩된 객체가 있으면 내부 객체는 여전히 참조를 공유합니다.
그래서 copied.push(4)를 실행해도 original은 영향을 받지 않지만, 내부 객체를 수정하면 둘 다 변경됩니다. 그 다음으로, 배열이나 객체 병합에서 스프레드 연산자의 진가가 드러납니다.
[...fruits, ...vegetables]는 두 배열의 모든 요소를 순서대로 펼쳐 새 배열을 만들며, 원하는 위치에 새 요소도 추가할 수 있습니다. 객체에서는 {...user, age: 31}처럼 작성하면 user의 모든 속성을 복사하면서 age만 덮어쓸 수 있어, 부분 업데이트가 매우 간단해집니다.
세 번째로, 함수 인자 전개는 배열의 요소를 개별 인자로 풀어줍니다. Math.max(...nums)는 Math.max(1, 2, 3, 4, 5)와 동일하게 동작하므로, 배열을 받는 함수에 가변 인자를 전달할 수 있습니다.
apply() 메서드를 사용하던 과거 방식보다 훨씬 직관적입니다. 여러분이 이 코드를 사용하면 불변성을 유지하면서도 데이터를 쉽게 조작할 수 있고, 여러 배열이나 객체를 간결하게 병합할 수 있으며, React 같은 프레임워크에서 상태 업데이트를 안전하게 처리할 수 있습니다.
실무에서는 상태 관리, 배열 조작, props 전달 등 거의 모든 곳에서 스프레드 연산자를 활용하면 코드가 훨씬 깔끔하고 안전해집니다.
실전 팁
💡 스프레드 연산자는 얕은 복사만 수행합니다. 중첩된 객체나 배열을 완전히 복사하려면 JSON.parse(JSON.stringify()) 또는 structuredClone()을 사용하세요. 💡 객체에서 속성 순서가 중요할 때는 덮어쓸 속성을 뒤에 배치하세요. {...defaultConfig, ...userConfig}는 userConfig가 defaultConfig를 덮어씁니다. 💡 배열의 중간에 요소를 삽입할 때는 [...arr.slice(0, index), newItem, ...arr.slice(index)]처럼 슬라이스와 조합하세요. 💡 React에서 배열 상태를 업데이트할 때는 항상 스프레드 연산자로 새 배열을 만드세요. push, splice 같은 변경 메서드를 사용하면 리렌더링이 되지 않습니다. 💡 성능이 중요한 경우 대량의 데이터를 스프레드로 복사하는 것은 비효율적일 수 있습니다. 이럴 때는 Immer 같은 라이브러리를 고려하세요.
5. 템플릿 리터럴
시작하며
여러분이 동적으로 HTML을 생성하거나 로그 메시지를 만들 때, 문자열 연결 연산자(+)와 따옴표를 번갈아가며 작성하다가 실수로 따옴표를 빼먹거나 공백을 잊어버린 경험이 있나요? 특히 여러 줄에 걸친 문자열이나 복잡한 표현식을 포함할 때는 코드가 금방 지저분해지고 읽기 어려워집니다.
이런 문제는 문자열 처리가 많은 프로젝트에서 매우 흔하게 발생합니다. 'Hello ' + name + ', you have ' + count + ' messages'처럼 변수를 문자열에 삽입하려면 +를 여러 번 사용해야 하고, 줄바꿈을 넣으려면 \n을 사용하거나 문자열을 여러 개로 나눠야 했습니다.
이는 코드의 가독성을 크게 떨어뜨립니다. 바로 이럴 때 필요한 것이 템플릿 리터럴입니다.
백틱(`)을 사용하여 변수와 표현식을 문자열에 직접 삽입할 수 있고, 여러 줄 문자열도 자연스럽게 작성할 수 있어 코드가 훨씬 깔끔하고 직관적으로 변합니다.
개요
간단히 말해서, 템플릿 리터럴은 백틱(`)으로 감싼 문자열로, ${} 문법을 사용하여 변수나 표현식을 직접 삽입할 수 있고, 줄바꿈을 그대로 표현할 수 있는 ES6의 강력한 문자열 기능입니다. 이 기능이 필요한 이유는 문자열 조작의 편의성과 코드 가독성입니다.
예를 들어, 사용자에게 보여줄 메시지를 만들 때 Welcome, ${userName}! You have ${messageCount} new messages처럼 변수 위치에 실제 값이 들어간 형태로 작성할 수 있어, 최종 결과를 미리 눈으로 확인하며 코딩할 수 있습니다.
기존에는 'line1\n' + 'line2\n' + 'line3'처럼 여러 줄 문자열을 만들기 위해 이스케이프 시퀀스나 배열의 join()을 사용해야 했다면, 이제는 백틱 안에서 Enter 키를 누르기만 하면 됩니다. HTML 템플릿이나 SQL 쿼리를 작성할 때 특히 유용합니다.
핵심 특징은 표현식 삽입, 멀티라인 지원, 태그드 템플릿, 자동 이스케이프입니다. 이러한 특징들이 동적 문자열 생성을 쉽게 하고, 복잡한 템플릿을 직관적으로 표현하며, styled-components 같은 라이브러리의 기반이 됩니다.
코드 예제
// 기본 변수 삽입
const name = 'Alice';
const age = 25;
const message = `Hello, my name is ${name} and I'm ${age} years old.`;
// 표현식 사용
const price = 100;
const quantity = 3;
const total = `Total: ${price * quantity}원 (VAT: ${price * quantity * 0.1}원)`;
// 멀티라인 문자열
const html = `
<div class="card">
<h2>${name}</h2>
<p>Age: ${age}</p>
<p>Status: ${age >= 18 ? 'Adult' : 'Minor'}</p>
</div>
`;
// 함수 호출
const formatDate = (date) => date.toLocaleDateString('ko-KR');
const greeting = `Today is ${formatDate(new Date())}`;
// 중첩 템플릿
const users = ['Alice', 'Bob', 'Charlie'];
const userList = `
<ul>
${users.map(user => `<li>${user}</li>`).join('')}
</ul>
`;
설명
이것이 하는 일: 템플릿 리터럴은 문자열 안에 ${표현식} 형태로 JavaScript 코드를 직접 삽입하고 평가하여, 동적으로 문자열을 생성합니다. 이를 통해 복잡한 문자열 연결 없이도 읽기 쉬운 형태로 동적 문자열을 만들 수 있습니다.
첫 번째로, 기본 변수 삽입에서 ${name}과 ${age}는 각각 해당 변수의 값으로 자동 변환됩니다. 이것은 단순한 문자열 치환이 아니라 실제로 표현식을 평가하므로, ${price * quantity}처럼 연산도 가능하고, ${age >= 18 ?
'Adult' : 'Minor'}처럼 삼항 연산자나 다른 JavaScript 표현식도 모두 사용할 수 있습니다. 이렇게 하면 문자열 안에서 로직을 직접 표현할 수 있어 코드가 매우 간결해집니다.
그 다음으로, 멀티라인 문자열 지원은 HTML이나 SQL 같은 긴 문자열을 다룰 때 빛을 발합니다. 백틱 안에서 Enter를 누르면 실제 줄바꿈 문자가 포함되므로, \n을 사용하거나 문자열을 여러 개로 나눌 필요가 없습니다.
html 변수의 예시처럼 들여쓰기를 포함한 형태로 작성할 수 있어, 최종 출력 결과를 코드에서 바로 확인할 수 있습니다. 세 번째로, 함수 호출과 중첩 템플릿은 더 복잡한 동적 콘텐츠를 생성할 수 있게 해줍니다.
${formatDate(new Date())}처럼 함수를 호출하여 그 반환값을 삽입할 수 있고, ${users.map(user => <li>${user}</li>).join('')}처럼 배열을 순회하면서 각 요소마다 템플릿을 생성하는 것도 가능합니다. 이는 React의 JSX와 비슷한 방식으로 동적 UI를 구성할 수 있게 해줍니다.
여러분이 이 코드를 사용하면 문자열 연결 연산자를 거의 사용하지 않게 되고, 동적 콘텐츠 생성이 훨씬 직관적이 되며, 복잡한 템플릿도 읽기 쉽게 작성할 수 있습니다. 실무에서는 로그 메시지, API 엔드포인트 URL, HTML 템플릿, SQL 쿼리 등 거의 모든 문자열 작업에서 템플릿 리터럴을 사용하면 코드 품질이 크게 향상됩니다.
실전 팁
💡 XSS 공격을 방지하려면 사용자 입력을 템플릿 리터럴에 직접 삽입하지 말고, DOMPurify 같은 라이브러리로 먼저 새니타이징하세요.
💡 복잡한 로직은 템플릿 안에 직접 작성하지 말고 별도 함수로 분리하세요. ${calculateComplexValue()}처럼 호출하면 코드가 더 읽기 쉽습니다.
💡 들여쓰기가 포함된 멀티라인 문자열에서 불필요한 공백을 제거하려면, 각 줄의 시작 부분에 공백을 넣지 말거나 trim() 메서드를 사용하세요.
💡 태그드 템플릿(Tagged Templates)을 사용하면 styled-components처럼 커스텀 문자열 처리를 할 수 있습니다. csscolor: red;처럼 활용하세요.
💡 정규식에서 백틱을 사용할 때는 new RegExp(pattern)보다는 /pattern/ 리터럴 문법을 사용하는 것이 더 명확합니다.
6. Promise
시작하며
여러분이 API를 호출하거나 파일을 읽는 비동기 작업을 할 때, 콜백 함수가 중첩되어 코드가 오른쪽으로 계속 들여쓰기되는 "콜백 지옥"을 경험한 적 있나요? getData(function(a) { getMoreData(a, function(b) { getEvenMore(b, function(c) { ...
})}) })처럼 코드가 피라미드 모양으로 변하면서 읽기도 어렵고 에러 처리도 복잡해집니다. 이런 문제는 JavaScript의 비동기 특성 때문에 발생합니다.
네트워크 요청이나 파일 I/O 같은 작업은 완료될 때까지 기다리지 않고 다음 코드를 실행하므로, 결과를 받아서 처리하려면 콜백 함수를 전달해야 했습니다. 작업이 여러 단계로 이어지면 콜백이 중첩되면서 코드 구조가 망가지고, 에러가 발생했을 때 어디서 처리해야 할지도 불명확해집니다.
바로 이럴 때 필요한 것이 Promise입니다. Promise는 비동기 작업의 완료 또는 실패를 나타내는 객체로, then과 catch를 체이닝하여 순차적으로 작성할 수 있어 콜백 지옥을 해결하고 에러 처리를 명확하게 만들어줍니다.
개요
간단히 말해서, Promise는 비동기 작업의 최종 완료 또는 실패와 그 결과값을 나타내는 객체로, 아직 완료되지 않았지만 미래에 완료될 작업을 표현하는 JavaScript의 핵심 패턴입니다. Promise가 필요한 이유는 비동기 코드의 구조화와 에러 처리 개선입니다.
예를 들어, 사용자 정보를 가져온 후 권한을 확인하고 데이터를 로드하는 일련의 작업을 getUserInfo().then(checkPermission).then(loadData).catch(handleError)처럼 읽기 쉬운 체인으로 작성할 수 있습니다. 기존에는 비동기 작업마다 콜백 함수를 전달하고, 각 콜백 안에서 에러를 개별적으로 처리해야 했다면, 이제는 Promise 체인의 마지막에 catch 하나로 모든 에러를 처리할 수 있습니다.
fetch API, axios 같은 현대 라이브러리들은 모두 Promise를 반환합니다. 핵심 특징은 세 가지 상태(pending, fulfilled, rejected), then/catch 체이닝, Promise.all/race 같은 유틸리티입니다.
이러한 특징들이 비동기 흐름을 동기 코드처럼 읽기 쉽게 만들고, 에러 처리를 한 곳에서 집중하며, 여러 비동기 작업을 효율적으로 조합할 수 있게 해줍니다.
코드 예제
// Promise 생성 및 사용
const fetchUserData = (userId) => {
return new Promise((resolve, reject) => {
// 비동기 작업 시뮬레이션
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, name: 'John', email: 'john@example.com' });
} else {
reject(new Error('Invalid user ID'));
}
}, 1000);
});
};
// Promise 체이닝
fetchUserData(1)
.then(user => {
console.log('User:', user);
return user.id; // 다음 then으로 전달
})
.then(userId => {
console.log('Loading posts for user:', userId);
return fetchPosts(userId); // 또 다른 Promise 반환
})
.then(posts => {
console.log('Posts:', posts);
})
.catch(error => {
console.error('Error occurred:', error.message); // 모든 에러 처리
})
.finally(() => {
console.log('Cleanup operations'); // 항상 실행
});
// 여러 Promise 동시 실행
Promise.all([
fetchUserData(1),
fetchUserData(2),
fetchUserData(3)
])
.then(users => console.log('All users:', users))
.catch(error => console.error('One of the requests failed:', error));
설명
이것이 하는 일: Promise는 비동기 작업을 캡슐화하여, 작업이 성공하면 resolve를 호출하고 실패하면 reject를 호출하는 구조를 제공합니다. 이를 통해 비동기 작업의 결과를 나중에 then이나 catch로 받아서 처리할 수 있게 만듭니다.
첫 번째로, Promise 생성 시 생성자에 executor 함수를 전달합니다. 이 함수는 resolve와 reject 두 개의 콜백을 받는데, 비동기 작업이 성공하면 resolve를 결과와 함께 호출하고, 실패하면 reject를 에러와 함께 호출합니다.
fetchUserData 예시에서는 1초 후에 userId가 유효하면 사용자 객체를 resolve하고, 그렇지 않으면 에러를 reject합니다. 이렇게 하면 비동기 작업의 성공과 실패를 명확하게 구분할 수 있습니다.
그 다음으로, then 메서드는 Promise가 이행(fulfilled)되었을 때 실행될 콜백을 등록하고, 새로운 Promise를 반환합니다. 이것이 체이닝의 핵심입니다.
첫 번째 then에서 user를 받아 처리하고 user.id를 반환하면, 이 값이 다음 then으로 전달됩니다. 만약 then에서 또 다른 Promise를 반환하면, 그 Promise가 이행될 때까지 기다렸다가 다음 then으로 결과를 전달합니다.
이런 방식으로 여러 비동기 작업을 순차적으로 연결할 수 있습니다. 세 번째로, catch 메서드는 체인 어디에서든 발생한 에러를 잡아서 처리합니다.
첫 번째 Promise에서 reject가 호출되거나, then 안에서 예외가 발생하면, 가장 가까운 catch로 이동합니다. finally는 성공이든 실패든 항상 실행되므로, 로딩 스피너를 끄거나 리소스를 정리하는 작업에 사용합니다.
Promise.all은 여러 Promise를 배열로 받아 모두 성공하면 결과 배열을 반환하고, 하나라도 실패하면 즉시 reject되므로, 여러 API를 동시에 호출할 때 유용합니다. 여러분이 이 코드를 사용하면 중첩된 콜백을 평평한 체인으로 만들 수 있고, 에러 처리를 한 곳에서 통합하며, 여러 비동기 작업을 효율적으로 관리할 수 있습니다.
실무에서는 fetch API로 데이터를 가져오거나, 파일 업로드, 복잡한 비즈니스 로직 등 거의 모든 비동기 작업에서 Promise를 사용하여 코드의 가독성과 유지보수성을 크게 향상시킬 수 있습니다.
실전 팁
💡 Promise를 생성할 때 이미 완료된 값이 있다면 Promise.resolve(value)나 Promise.reject(error)를 사용하여 즉시 이행/거부된 Promise를 만드세요. 💡 then에서 에러를 처리하지 않으면 다음 catch로 전파됩니다. 특정 단계에서만 에러를 처리하려면 then의 두 번째 인자로 에러 핸들러를 전달하세요. 💡 Promise.all은 모든 작업이 성공해야 하지만, Promise.allSettled를 사용하면 각 Promise의 성공/실패 여부와 결과를 모두 받을 수 있습니다. 💡 Promise.race는 가장 먼저 완료되는 Promise의 결과를 반환하므로, 타임아웃 구현에 유용합니다. Promise.race([fetchData(), timeout(5000)])처럼 사용하세요. 💡 Unhandled Promise Rejection을 방지하려면 모든 Promise에 catch를 붙이거나, 최상위 레벨에서 window.addEventListener('unhandledrejection', handler)로 처리하세요.
7. async/await
시작하며
여러분이 여러 단계의 비동기 작업을 순차적으로 처리해야 할 때, Promise 체인이 길어지면서 then과 catch가 반복되어 여전히 코드가 복잡하게 느껴진 적 있나요? getUserInfo().then(user => getPermissions(user.id)).then(perms => loadData(perms)).catch(handleError)처럼 작성하면 Promise를 사용하지 않을 때보다는 낫지만, 여전히 동기 코드처럼 직관적이지는 않습니다.
이런 문제는 then 체이닝이 여러 단계로 이어지면서 발생합니다. 각 단계의 결과를 다음 단계에서 사용해야 하는데, then 안에서 또 then을 호출하다 보면 구조가 복잡해지고, 중간 결과를 변수에 저장하기도 어렵습니다.
에러 처리도 catch를 여러 개 붙이거나 try-catch 없이 처리해야 해서 익숙하지 않습니다. 바로 이럴 때 필요한 것이 async/await입니다.
async 함수 안에서 await 키워드를 사용하면 Promise가 완료될 때까지 기다리는 것처럼 코드를 작성할 수 있어, 비동기 코드를 동기 코드처럼 읽기 쉽게 만들어줍니다.
개요
간단히 말해서, async/await는 Promise를 더 쉽게 사용할 수 있게 해주는 ES2017의 문법적 설탕(syntactic sugar)으로, 비동기 코드를 마치 동기 코드처럼 작성하고 읽을 수 있게 해줍니다. async/await가 필요한 이유는 코드의 직관성과 익숙한 제어 흐름입니다.
예를 들어, const user = await getUserInfo(); const perms = await getPermissions(user.id); const data = await loadData(perms);처럼 작성하면 마치 일반적인 함수 호출처럼 보이면서도 비동기로 동작합니다. try-catch로 에러를 처리할 수 있어 동기 코드와 동일한 방식으로 예외를 관리할 수 있습니다.
기존에는 then 체인을 길게 연결하거나 Promise를 중첩해야 했다면, 이제는 await로 각 단계의 결과를 변수에 저장하고 다음 줄에서 그 변수를 사용할 수 있습니다. 이는 코드의 흐름을 위에서 아래로 읽을 수 있게 만들어 훨씬 이해하기 쉽습니다.
핵심 특징은 async 함수는 항상 Promise를 반환, await는 Promise가 이행될 때까지 대기, try-catch로 에러 처리, for...of와의 조합입니다. 이러한 특징들이 비동기 코드를 명령형 스타일로 작성하게 하고, 복잡한 비동기 로직을 단순화하며, 디버깅을 쉽게 만들어줍니다.
코드 예제
// async 함수 선언
async function fetchUserProfile(userId) {
try {
// await로 Promise가 완료될 때까지 대기
const user = await fetchUserData(userId);
console.log('User fetched:', user);
// 이전 결과를 바로 사용 가능
const permissions = await fetchPermissions(user.id);
console.log('Permissions:', permissions);
// 조건부 비동기 작업
if (permissions.includes('admin')) {
const adminData = await fetchAdminData();
return { ...user, adminData };
}
return user;
} catch (error) {
// 모든 에러를 한 곳에서 처리
console.error('Failed to fetch profile:', error.message);
throw error; // 상위로 에러 전파
}
}
// 화살표 함수에서도 사용 가능
const loadData = async () => {
const data = await fetch('/api/data').then(res => res.json());
return data;
};
// 병렬 실행 (Promise.all과 함께)
async function loadMultipleUsers() {
const userIds = [1, 2, 3];
// 동시에 실행하여 성능 향상
const users = await Promise.all(
userIds.map(id => fetchUserData(id))
);
return users;
}
// for...of 루프에서 순차 실행
async function processUsers(userIds) {
for (const id of userIds) {
const user = await fetchUserData(id);
await processUser(user); // 각 사용자를 순차적으로 처리
}
}
설명
이것이 하는 일: async/await는 Promise 기반 비동기 작업을 절차적(procedural) 스타일로 작성할 수 있게 해주며, await 키워드가 Promise의 결과를 기다렸다가 다음 줄로 진행하는 것처럼 동작하게 만듭니다. 첫 번째로, async 키워드를 함수 앞에 붙이면 그 함수는 자동으로 Promise를 반환합니다.
return user;라고 작성해도 실제로는 Promise.resolve(user)가 반환되므로, 함수를 호출하는 쪽에서도 then이나 await로 결과를 받을 수 있습니다. 이것이 async 함수가 다른 async 함수와 자연스럽게 조합될 수 있는 이유입니다.
일반 함수처럼 보이지만 내부적으로는 완전히 비동기로 동작합니다. 그 다음으로, await 키워드는 Promise가 이행될 때까지 함수 실행을 일시 중단하고, 이행되면 결과값을 반환하여 다음 줄로 진행합니다.
const user = await fetchUserData(userId);는 fetchUserData가 완료될 때까지 기다린 후 user 변수에 결과를 저장하고 다음 줄을 실행합니다. 이렇게 하면 user를 바로 사용할 수 있어서, permissions를 가져올 때 user.id를 직접 참조할 수 있습니다.
then 체인에서처럼 콜백 안에 값이 갇히지 않습니다. 세 번째로, try-catch 블록을 사용하면 동기 코드와 똑같은 방식으로 에러를 처리할 수 있습니다.
fetchUserData, fetchPermissions, fetchAdminData 중 어디에서든 에러가 발생하면 catch 블록으로 이동하여 error 객체를 받습니다. 이는 then/catch 체인보다 훨씬 익숙하고 명확합니다.
throw error로 에러를 상위로 전파하면 호출한 쪽에서도 try-catch나 catch로 처리할 수 있습니다. 여러분이 이 코드를 사용하면 비동기 코드를 마치 동기 코드처럼 위에서 아래로 읽을 수 있고, 중간 결과를 변수에 저장하여 자유롭게 사용할 수 있으며, 익숙한 try-catch로 에러를 처리할 수 있습니다.
실무에서는 API 호출, 데이터베이스 쿼리, 파일 I/O 등 거의 모든 비동기 작업에서 async/await를 사용하면 코드의 가독성과 유지보수성이 극적으로 향상됩니다. 특히 여러 단계가 이어지는 복잡한 비즈니스 로직에서 그 가치가 빛을 발합니다.
실전 팁
💡 여러 비동기 작업이 서로 독립적이라면 await를 순차적으로 사용하지 말고 Promise.all로 병렬 실행하세요. 성능이 크게 향상됩니다. 💡 for...of는 순차 실행, Promise.all은 병렬 실행입니다. 각 작업이 이전 작업에 의존한다면 for...of를, 독립적이라면 Promise.all을 사용하세요. 💡 await는 반드시 async 함수 안에서만 사용할 수 있습니다. 최상위 레벨에서는 사용할 수 없으므로 즉시 실행 async 함수로 감싸세요: (async () => { await doSomething(); })(); 💡 try-catch를 남발하지 말고, 여러 함수에 걸쳐 발생할 수 있는 에러는 최상위 async 함수에서 한 번에 처리하는 것이 좋습니다. 💡 async 함수는 항상 Promise를 반환하므로, 반환값을 사용하는 쪽에서도 await나 then으로 받아야 합니다. 그냥 호출만 하면 Promise 객체를 받게 됩니다.
8. 배열 메서드 - map, filter, reduce
시작하며
여러분이 배열 데이터를 처리할 때, for 루프 안에서 if 조건을 체크하고 임시 배열에 push하는 코드를 반복해서 작성하느라 지친 적 있나요? const result = []; for (let i = 0; i < arr.length; i++) { if (arr[i] > 5) { result.push(arr[i] * 2); } }처럼 작성하면 코드가 길어지고 의도가 명확하게 드러나지 않습니다.
이런 문제는 명령형(imperative) 프로그래밍 방식으로 배열을 처리할 때 발생합니다. 어떻게(how) 반복할지를 명시해야 하므로 인덱스 관리, 임시 변수 생성, 조건 체크가 섞여서 "무엇을(what)" 하려는지 파악하기 어렵습니다.
버그가 발생하기도 쉽고, 코드를 재사용하기도 어렵습니다. 바로 이럴 때 필요한 것이 map, filter, reduce 같은 배열 메서드입니다.
이들은 선언적(declarative) 방식으로 배열을 변환, 필터링, 집계할 수 있게 해주어, 코드의 의도를 명확하게 표현하고 가독성을 크게 향상시킵니다.
개요
간단히 말해서, map, filter, reduce는 배열의 각 요소를 순회하면서 변환, 선택, 집계하는 고차 함수(higher-order function)로, 함수형 프로그래밍 스타일로 데이터를 처리할 수 있게 해줍니다. 이 메서드들이 필요한 이유는 코드의 선언성과 재사용성입니다.
예를 들어, 사용자 목록에서 성인만 필터링하고 이름만 추출하는 작업을 users.filter(u => u.age >= 18).map(u => u.name)처럼 체이닝으로 간결하게 표현할 수 있습니다. 각 단계가 무엇을 하는지 명확하게 보입니다.
기존에는 for 루프로 인덱스를 관리하고 if문으로 조건을 체크하며 임시 배열을 만들어야 했다면, 이제는 각 메서드가 하는 일(변환/필터/집계)에 따라 적절한 메서드를 선택하고 콜백 함수만 전달하면 됩니다. 원본 배열을 변경하지 않고 새 배열을 반환하므로 불변성도 자동으로 지켜집니다.
핵심 특징은 map은 변환, filter는 선택, reduce는 집계, 체이닝 가능, 불변성 유지입니다. 이러한 특징들이 데이터 처리 파이프라인을 직관적으로 구성하고, 각 단계를 독립적으로 테스트하며, 함수형 프로그래밍의 장점을 활용할 수 있게 해줍니다.
코드 예제
const users = [
{ id: 1, name: 'Alice', age: 25, score: 85 },
{ id: 2, name: 'Bob', age: 17, score: 92 },
{ id: 3, name: 'Charlie', age: 30, score: 78 },
{ id: 4, name: 'David', age: 22, score: 88 }
];
// map: 배열의 각 요소를 변환하여 새 배열 생성
const names = users.map(user => user.name);
// ['Alice', 'Bob', 'Charlie', 'David']
const upperNames = users.map(user => user.name.toUpperCase());
// ['ALICE', 'BOB', 'CHARLIE', 'DAVID']
// filter: 조건을 만족하는 요소만 선택하여 새 배열 생성
const adults = users.filter(user => user.age >= 18);
// Alice, Charlie, David만 포함된 배열
const highScorers = users.filter(user => user.score >= 85);
// Alice, Bob, David만 포함된 배열
// reduce: 배열을 단일 값으로 집계
const totalScore = users.reduce((sum, user) => sum + user.score, 0);
// 343 (85 + 92 + 78 + 88)
const avgScore = users.reduce((sum, user) => sum + user.score, 0) / users.length;
// 85.75
// 체이닝: 여러 메서드를 연결하여 복잡한 변환
const adultNames = users
.filter(user => user.age >= 18) // 성인만 선택
.map(user => user.name) // 이름만 추출
.sort(); // 알파벳 순 정렬
// ['Alice', 'Charlie', 'David']
// reduce로 객체 만들기
const userById = users.reduce((acc, user) => {
acc[user.id] = user;
return acc;
}, {});
// { 1: {Alice 객체}, 2: {Bob 객체}, ... }
설명
이것이 하는 일: map, filter, reduce는 배열의 각 요소에 대해 제공된 콜백 함수를 실행하고, 그 결과를 바탕으로 새로운 배열이나 값을 생성합니다. 이를 통해 데이터 변환 로직을 명확하고 재사용 가능하게 표현할 수 있습니다.
첫 번째로, map은 배열의 각 요소를 변환합니다. users.map(user => user.name)은 각 user 객체에서 name 속성만 추출하여 새로운 배열을 만듭니다.
콜백 함수가 반환한 값이 새 배열의 요소가 되므로, user.name.toUpperCase()처럼 변환을 적용할 수 있습니다. 중요한 점은 원본 배열(users)은 전혀 변경되지 않고, 항상 새 배열을 반환한다는 것입니다.
이렇게 하면 불변성을 유지하면서 데이터를 변환할 수 있어 React 같은 프레임워크에서 안전합니다. 그 다음으로, filter는 조건을 만족하는 요소만 선택합니다.
users.filter(user => user.age >= 18)은 각 user에 대해 조건을 검사하고, true를 반환하는 요소만 새 배열에 포함시킵니다. 여러 조건을 조합하려면 && 또는 ||를 사용하거나, 여러 filter를 체이닝할 수 있습니다.
filter도 map과 마찬가지로 원본을 변경하지 않고 새 배열을 반환합니다. 세 번째로, reduce는 배열을 순회하면서 누적값(accumulator)을 업데이트하여 최종적으로 하나의 값을 만듭니다.
users.reduce((sum, user) => sum + user.score, 0)에서 0은 초기값이고, 각 반복에서 sum에 user.score를 더한 값이 다음 반복의 sum이 됩니다. 합계, 평균 같은 집계뿐만 아니라, 객체나 배열을 만드는 데도 사용할 수 있습니다.
userById 예시처럼 배열을 키-값 객체로 변환하는 것도 reduce의 강력한 활용입니다. 여러분이 이 코드를 사용하면 for 루프와 임시 변수를 거의 사용하지 않게 되고, 데이터 처리 로직이 선언적으로 표현되어 읽기 쉬워지며, 각 단계를 독립적으로 테스트하고 재사용할 수 있습니다.
실무에서는 API 응답 데이터 가공, 통계 계산, UI 렌더링용 데이터 준비 등 거의 모든 배열 처리에서 이 메서드들을 활용하면 코드 품질이 크게 향상됩니다. 특히 React에서 컴포넌트 목록을 렌더링할 때 map은 필수적으로 사용됩니다.
실전 팁
💡 map, filter는 항상 새 배열을 반환하므로 메모리를 사용합니다. 대량의 데이터에서는 for 루프가 더 효율적일 수 있으니, 성능 측정 후 결정하세요. 💡 filter().map() 순서로 체이닝하면 불필요한 변환을 줄일 수 있습니다. map().filter()보다 먼저 필터링하여 처리할 요소를 줄이세요. 💡 reduce는 강력하지만 복잡해지기 쉽습니다. 가독성이 떨어진다면 for 루프로 작성하는 것이 더 나을 수 있습니다. 💡 배열이 비어있을 때를 대비하여 reduce의 초기값을 항상 명시하세요. 초기값을 생략하면 빈 배열에서 에러가 발생합니다. 💡 React에서 map으로 컴포넌트를 렌더링할 때는 각 요소에 고유한 key prop을 제공해야 합니다. user.id를 key로 사용하는 것이 좋습니다.