이미지 로딩 중...
AI Generated
2025. 11. 5. · 7 Views
개발자 입문 완벽 가이드
프로그래밍을 처음 시작하는 분들을 위한 필수 개념과 실전 노하우를 담았습니다. 변수부터 함수, 조건문, 반복문까지 실무에서 바로 활용할 수 있는 기초 개념을 친절하게 설명합니다. 초보자도 쉽게 따라할 수 있는 예제 코드와 함께 개발자로 성장하는 첫걸음을 시작해보세요.
목차
- 변수와 데이터 타입 - 프로그래밍의 기본 블록
- 함수 - 코드의 재사용과 모듈화
- 조건문 - 프로그램의 의사결정
- 반복문 - 효율적인 반복 작업
- 객체 - 관련 데이터의 구조화
- 배열 메서드 - 데이터 변환의 핵심
- 에러 처리 - 안정적인 프로그램 만들기
- 비동기 프로그래밍 - 시간이 걸리는 작업 다루기
1. 변수와 데이터 타입 - 프로그래밍의 기본 블록
시작하며
여러분이 쇼핑 앱을 사용할 때 장바구니에 담긴 상품들의 정보가 어떻게 저장될까요? 상품명, 가격, 수량 같은 정보들이 화면에 표시되고, 여러분이 수량을 변경하면 즉시 반영되는 것을 보셨을 겁니다.
이런 마법 같은 일이 가능한 이유는 바로 '변수'라는 개념 덕분입니다. 프로그래밍에서 변수는 마치 라벨이 붙은 상자처럼, 데이터를 담아두고 필요할 때마다 꺼내서 사용할 수 있게 해줍니다.
변수를 모르고는 단 한 줄의 유용한 코드도 작성할 수 없습니다. 오늘 여러분은 프로그래밍의 가장 기본이 되는 변수와 데이터 타입에 대해 완전히 이해하게 될 것입니다.
개요
간단히 말해서, 변수는 데이터를 저장하는 메모리 공간에 붙인 이름입니다. 마치 파일 캐비닛에 라벨을 붙여 나중에 쉽게 찾을 수 있게 하는 것과 같습니다.
왜 변수가 필요할까요? 만약 변수 없이 프로그램을 작성한다면, 사용자의 이름이나 나이 같은 정보를 저장할 방법이 없습니다.
예를 들어, 로그인 기능을 만든다면 사용자가 입력한 아이디와 비밀번호를 어딘가에 임시로 보관했다가 서버로 전송해야 하는데, 이때 변수가 필요합니다. 기존에는 var라는 키워드로 변수를 선언했다면, 현대 JavaScript에서는 let과 const를 사용합니다.
var는 스코프 문제로 예상치 못한 버그를 만들 수 있기 때문입니다. JavaScript의 주요 데이터 타입에는 숫자(Number), 문자열(String), 불린(Boolean), 객체(Object), 배열(Array) 등이 있습니다.
이러한 타입들을 이해하면 어떤 종류의 데이터든 효과적으로 다룰 수 있게 됩니다. 타입을 잘못 사용하면 "undefined is not a function" 같은 에러를 만나게 되므로, 각 타입의 특성을 정확히 아는 것이 중요합니다.
코드 예제
// 변수 선언: 값이 변경될 수 있는 변수는 let 사용
let userName = "김개발";
let userAge = 25;
let isLoggedIn = true;
// 상수 선언: 값이 변경되지 않는 변수는 const 사용
const MAX_LOGIN_ATTEMPTS = 3;
const API_URL = "https://api.example.com";
// 배열: 여러 값을 순서대로 저장
const hobbies = ["코딩", "독서", "운동"];
// 객체: 관련된 데이터를 하나로 묶어서 저장
const user = {
name: "김개발",
age: 25,
email: "dev@example.com"
};
console.log(`안녕하세요, ${userName}님!`); // 템플릿 리터럴로 변수 사용
설명
이것이 하는 일: 위 코드는 다양한 타입의 데이터를 변수에 저장하고, 이를 활용하는 방법을 보여줍니다. 실제 웹 애플리케이션에서 사용자 정보를 다루는 것과 동일한 패턴입니다.
첫 번째로, let 키워드로 선언한 userName, userAge, isLoggedIn은 값이 변경될 수 있는 변수들입니다. 예를 들어 userName은 사용자가 이름을 변경하면 새로운 값으로 업데이트될 수 있습니다.
let을 사용하면 나중에 userName = "박개발"처럼 값을 재할당할 수 있습니다. 그 다음으로, const 키워드로 선언한 MAX_LOGIN_ATTEMPTS와 API_URL은 상수입니다.
한 번 설정하면 프로그램 실행 중에 절대 변경되지 않는 값들이죠. 설정 값이나 URL 같은 것들은 const로 선언하면 실수로 값을 변경하는 것을 방지할 수 있습니다.
배열과 객체는 여러 데이터를 묶어서 관리할 때 사용합니다. hobbies 배열은 인덱스로 접근하고(hobbies[0] = "코딩"), user 객체는 속성 이름으로 접근합니다(user.name = "김개발").
이 두 가지는 복잡한 데이터 구조를 다룰 때 필수적입니다. 마지막으로, 템플릿 리터럴(백틱 ` 사용)을 통해 문자열 안에 변수를 깔끔하게 삽입할 수 있습니다.
기존의 "안녕하세요, " + userName + "님!" 방식보다 훨씬 읽기 쉽고 실수할 가능성도 적습니다. 여러분이 이 코드를 사용하면 데이터를 체계적으로 관리하고, 코드의 의도를 명확히 표현하며, 버그를 사전에 방지할 수 있습니다.
특히 const를 적극 활용하면 예상치 못한 값 변경으로 인한 버그를 크게 줄일 수 있습니다.
실전 팁
💡 기본적으로 모든 변수를 const로 선언하고, 값이 변경되어야 할 때만 let으로 바꾸세요. 이렇게 하면 의도치 않은 재할당을 방지할 수 있습니다. 💡 변수명은 camelCase로 작성하세요(userName, isLoggedIn). 상수는 UPPER_SNAKE_CASE(MAX_LOGIN_ATTEMPTS)를 사용하면 코드에서 한눈에 구분됩니다. 💡 절대 var를 사용하지 마세요. var는 함수 스코프를 가져 블록 밖에서도 접근 가능해 예상치 못한 버그를 만듭니다. 모던 JavaScript에서는 let과 const만 사용합니다. 💡 배열과 객체를 const로 선언해도 내부 값은 변경할 수 있습니다. const는 재할당만 막을 뿐, user.name = "새이름" 같은 수정은 가능합니다. 💡 typeof 연산자로 변수의 타입을 확인할 수 있습니다. 디버깅할 때 console.log(typeof userName)처럼 사용하면 예상치 못한 타입 문제를 빠르게 찾을 수 있습니다.
2. 함수 - 코드의 재사용과 모듈화
시작하며
여러분이 계산기 앱을 만든다고 상상해보세요. 두 수를 더하는 코드를 10번, 20번, 100번 반복해서 작성해야 한다면 얼마나 비효율적일까요?
코드가 길어질수록 수정하기도 어려워지고, 버그가 생길 확률도 높아집니다. 이런 문제는 실제 개발 현장에서 매일 발생합니다.
같은 로직을 여러 곳에서 복사-붙여넣기 하다 보면, 나중에 하나를 수정할 때 모든 곳을 다 찾아서 수정해야 하는 악몽을 경험하게 됩니다. 바로 이럴 때 필요한 것이 '함수'입니다.
함수는 반복되는 코드를 하나로 묶어서, 필요할 때마다 이름만 부르면 실행되게 해줍니다.
개요
간단히 말해서, 함수는 특정 작업을 수행하는 코드 블록에 이름을 붙인 것입니다. 마치 믹서기에 '스무디 만들기' 버튼을 누르면 자동으로 과일을 갈아주는 것처럼, 함수 이름을 호출하면 정해진 작업이 자동으로 실행됩니다.
왜 함수가 필요한가요? 첫째, 코드 재사용성이 높아집니다.
한 번 작성한 함수는 프로그램 어디서든 몇 번이든 호출할 수 있습니다. 둘째, 유지보수가 쉬워집니다.
로직 수정이 필요하면 함수 하나만 고치면 그 함수를 사용하는 모든 곳에 변경사항이 반영됩니다. 예를 들어, 사용자 인증 로직을 함수로 만들어두면, 인증 방식이 바뀌어도 한 곳만 수정하면 됩니다.
기존에는 function 키워드로만 함수를 만들었다면, 현대 JavaScript에서는 화살표 함수(Arrow Function)도 사용할 수 있습니다. 화살표 함수는 더 간결하고, this 바인딩 문제도 해결해줍니다.
함수의 핵심 특징은 입력(매개변수), 처리(함수 본문), 출력(반환값)으로 구성된다는 점입니다. 이 세 가지를 잘 설계하면 블랙박스처럼 내부 구현을 몰라도 사용할 수 있는 깔끔한 인터페이스를 만들 수 있습니다.
이것이 바로 좋은 코드의 시작입니다.
코드 예제
// 함수 선언식: 두 수를 더하는 함수
function addNumbers(a, b) {
// 매개변수 a, b를 받아서 처리
const sum = a + b;
return sum; // 결과를 반환
}
// 화살표 함수: 더 간결한 문법
const multiply = (a, b) => {
return a * b;
};
// 한 줄짜리 화살표 함수는 더 간단하게 작성 가능
const subtract = (a, b) => a - b;
// 함수 사용하기
const result1 = addNumbers(10, 5); // 15
const result2 = multiply(4, 3); // 12
const result3 = subtract(20, 8); // 12
console.log(`10 + 5 = ${result1}`);
// 기본 매개변수: 값이 없으면 기본값 사용
function greet(name = "손님") {
return `안녕하세요, ${name}님!`;
}
console.log(greet("김개발")); // 안녕하세요, 김개발님!
console.log(greet()); // 안녕하세요, 손님님!
설명
이것이 하는 일: 위 코드는 함수를 선언하고 호출하는 다양한 방법을 보여줍니다. 실무에서 가장 많이 사용하는 패턴들을 담았습니다.
첫 번째로, addNumbers 함수는 전통적인 함수 선언식입니다. function 키워드로 시작하고, 중괄호 안에 실행할 코드를 작성합니다.
매개변수 a와 b를 받아서 더한 후, return으로 결과를 돌려줍니다. 함수를 호출할 때는 addNumbers(10, 5)처럼 괄호 안에 실제 값을 전달합니다.
그 다음으로, 화살표 함수 문법을 사용한 multiply와 subtract를 볼 수 있습니다. multiply는 일반 화살표 함수로, function 대신 =>를 사용합니다.
subtract는 더 간결한 형태로, 한 줄짜리 계산은 중괄호와 return을 생략할 수 있습니다. 화살표 함수는 콜백 함수로 많이 사용되며, this 바인딩이 렉시컬 스코프를 따라서 React 같은 프레임워크에서 특히 유용합니다.
함수를 호출하면 반환값을 변수에 저장할 수 있습니다. const result1 = addNumbers(10, 5)처럼 작성하면, result1에는 15가 저장됩니다.
이 값을 다른 계산에 사용하거나 화면에 표시할 수 있습니다. greet 함수는 기본 매개변수를 보여줍니다.
name = "손님"처럼 작성하면, 함수 호출 시 인자를 전달하지 않았을 때 기본값이 사용됩니다. 이렇게 하면 사용자가 이름을 입력하지 않았을 때도 에러 없이 동작하는 안전한 코드를 만들 수 있습니다.
여러분이 이 패턴들을 사용하면 코드 중복을 제거하고, 테스트하기 쉬운 모듈화된 코드를 작성할 수 있습니다. 특히 하나의 함수가 하나의 일만 하도록 설계하면(단일 책임 원칙), 나중에 버그를 찾고 수정하기가 훨씬 쉬워집니다.
실전 팁
💡 함수 이름은 동사로 시작하세요(calculateTotal, getUserInfo, validateEmail). 함수가 무엇을 하는지 이름만 봐도 알 수 있어야 합니다. 💡 하나의 함수는 하나의 일만 하게 만드세요. 함수가 너무 많은 일을 하면 재사용하기 어렵고 테스트도 복잡해집니다. 20줄이 넘어가면 쪼갤 방법을 고민해보세요. 💡 화살표 함수는 this를 바인딩하지 않으므로, 객체 메서드나 생성자 함수로는 사용하지 마세요. 대신 콜백 함수나 배열 메서드(map, filter 등)에서 사용하면 최적입니다. 💡 매개변수가 3개 이상이면 객체로 전달하는 것을 고려하세요. createUser({name, age, email})처럼 작성하면 순서를 외울 필요가 없고, 선택적 매개변수 관리도 쉬워집니다. 💡 순수 함수를 만들려고 노력하세요. 같은 입력에 항상 같은 출력을 반환하고, 외부 상태를 변경하지 않는 함수는 테스트와 디버깅이 훨씬 쉽습니다.
3. 조건문 - 프로그램의 의사결정
시작하며
여러분이 온라인 쇼핑몰에서 결제하려고 할 때, "20세 이상만 구매 가능합니다" 또는 "쿠폰 사용 시 10% 할인"이라는 메시지를 본 적 있으신가요? 이런 조건에 따라 다른 결과를 보여주는 것이 바로 조건문의 힘입니다.
실제 프로그램은 수많은 선택의 연속입니다. 사용자가 로그인했는지, 입력값이 유효한지, 재고가 있는지 등을 판단하고 그에 따라 다른 동작을 수행해야 합니다.
조건문 없이는 이런 동적인 동작을 구현할 수 없습니다. 바로 이럴 때 필요한 것이 if-else 문과 switch 문입니다.
조건문을 통해 프로그램은 상황에 맞는 지능적인 결정을 내릴 수 있게 됩니다.
개요
간단히 말해서, 조건문은 특정 조건이 참(true)인지 거짓(false)인지 판단하여 실행할 코드를 결정하는 구조입니다. 마치 갈림길에서 표지판을 보고 어느 길로 갈지 결정하는 것과 같습니다.
왜 조건문이 필요할까요? 모든 애플리케이션은 다양한 상황에 대응해야 합니다.
예를 들어, 회원가입 폼에서 비밀번호가 8자 미만이면 "비밀번호가 너무 짧습니다" 에러를 보여주고, 8자 이상이면 다음 단계로 진행시켜야 합니다. 이런 분기 처리가 없다면 모든 사용자에게 똑같은 결과만 보여주는 정적인 프로그램밖에 만들 수 없습니다.
기존에는 if-else만 사용했다면, 여러 값 중 하나를 선택할 때는 switch 문이 더 깔끔합니다. 또한 삼항 연산자(?
:)를 사용하면 간단한 조건을 한 줄로 표현할 수 있습니다. 조건문의 핵심은 비교 연산자(===, !==, >, <)와 논리 연산자(&&, ||, !)를 조합하여 복잡한 조건을 만드는 것입니다.
이들을 잘 활용하면 "로그인한 사용자이면서 프리미엄 회원인 경우"처럼 여러 조건을 동시에 검사할 수 있습니다. 조건문을 잘 다루는 것이 프로그래밍 로직의 핵심입니다.
코드 예제
// if-else 문: 나이에 따른 입장 가능 여부 판단
const age = 18;
if (age >= 20) {
console.log("입장 가능합니다.");
} else if (age >= 13) {
console.log("보호자 동의 필요합니다.");
} else {
console.log("입장 불가능합니다.");
}
// 논리 연산자: 여러 조건 동시 검사
const isPremium = true;
const isLoggedIn = true;
if (isLoggedIn && isPremium) {
console.log("프리미엄 콘텐츠 접근 가능");
} else if (isLoggedIn) {
console.log("일반 콘텐츠만 접근 가능");
} else {
console.log("로그인이 필요합니다");
}
// 삼항 연산자: 간단한 조건은 한 줄로
const score = 85;
const grade = score >= 80 ? "합격" : "불합격";
console.log(grade); // 합격
// switch 문: 여러 값 중 하나 선택
const day = "월요일";
switch (day) {
case "월요일":
case "화요일":
console.log("주중입니다");
break;
case "토요일":
case "일요일":
console.log("주말입니다");
break;
default:
console.log("그 외 요일입니다");
}
설명
이것이 하는 일: 위 코드는 다양한 조건문 패턴을 통해 상황에 따른 분기 처리를 보여줍니다. 실무에서 사용자 권한 체크, 입력값 검증, 상태 관리 등에 필수적인 패턴들입니다.
첫 번째로, if-else 문은 가장 기본적인 조건 분기입니다. age >= 20을 검사해서 참이면 첫 번째 블록을 실행하고, 거짓이면 else if로 넘어가서 age >= 13을 검사합니다.
모든 조건이 거짓이면 마지막 else 블록이 실행됩니다. 이렇게 여러 단계의 조건을 체인처럼 연결할 수 있습니다.
그 다음으로, 논리 연산자를 사용한 복합 조건을 볼 수 있습니다. && 연산자는 "그리고"를 의미해서, isLoggedIn && isPremium은 두 조건이 모두 참일 때만 참이 됩니다.
|| 연산자는 "또는"을 의미해서 둘 중 하나만 참이어도 됩니다. 실무에서는 "로그인했으면서 결제한 사용자" 같은 복잡한 권한 체크에 자주 사용됩니다.
삼항 연산자는 조건 ? 참일때값 : 거짓일때값 형태로, if-else를 한 줄로 표현합니다.
간단한 조건에서는 코드를 훨씬 간결하게 만들어주지만, 너무 복잡한 조건에 사용하면 오히려 가독성이 떨어지니 주의해야 합니다. 변수 할당이나 JSX 조건부 렌더링에서 특히 유용합니다.
switch 문은 하나의 값을 여러 케이스와 비교할 때 if-else보다 깔끔합니다. 각 case는 break로 끝내야 다음 케이스로 넘어가지(fall-through) 않습니다.
"월요일"과 "화요일" 케이스처럼 break 없이 여러 케이스를 묶으면 같은 코드를 실행하게 할 수 있습니다. default는 모든 케이스에 해당하지 않을 때 실행됩니다.
여러분이 이 조건문들을 적재적소에 활용하면 사용자 입력 검증, 권한 체크, 상태에 따른 UI 변경 등 동적인 프로그램을 만들 수 있습니다. 특히 조건을 명확하고 읽기 쉽게 작성하는 것이 유지보수에 매우 중요합니다.
실전 팁
💡 동등 비교 시 ==이 아닌 ===를 사용하세요. ==는 타입 변환을 하기 때문에 "5" == 5가 true가 되는 예상치 못한 결과를 만듭니다. ===는 타입까지 엄격하게 비교합니다. 💡 조건이 3개 이상 중첩되면 함수로 분리하거나 early return 패턴을 사용하세요. if (!isLoggedIn) return; 처럼 조기 종료하면 중첩을 줄일 수 있습니다. 💡 switch 문에서 break를 빼먹으면 의도치 않게 다음 케이스까지 실행됩니다(fall-through). 각 case 끝에 break를 꼭 작성하거나, 의도적인 fall-through라면 주석으로 표시하세요. 💡 falsy 값(0, "", null, undefined, false, NaN)을 조건에 사용할 때 주의하세요. if (value)는 value가 0일 때도 false가 되므로, 명확하게 if (value !== undefined)처럼 작성하는 것이 안전합니다. 💡 복잡한 조건은 변수나 함수로 추출하세요. if (isValidUser(user) && hasPermission(user, "write"))처럼 작성하면 코드의 의도가 명확해집니다.
4. 반복문 - 효율적인 반복 작업
시작하며
여러분이 1부터 100까지의 숫자를 화면에 출력하는 프로그램을 작성한다고 생각해보세요. console.log(1), console.log(2), ...
console.log(100)을 100번 타이핑하시겠습니까? 또는 100명의 사용자 정보를 처리해야 한다면 같은 코드를 100번 복사하시겠습니까?
이런 반복 작업은 프로그래밍에서 가장 흔한 패턴입니다. 게시판의 글 목록 보여주기, 장바구니의 모든 상품 가격 합산하기, 파일의 모든 줄 읽기 등 수많은 상황에서 같은 작업을 여러 번 수행해야 합니다.
바로 이럴 때 필요한 것이 '반복문'입니다. 반복문을 사용하면 몇 줄의 코드로 수천, 수만 번의 작업을 자동화할 수 있습니다.
개요
간단히 말해서, 반복문은 특정 조건이 만족될 때까지 같은 코드를 반복해서 실행하는 구조입니다. 마치 공장의 컨베이어 벨트처럼, 같은 작업을 여러 번 자동으로 처리해줍니다.
왜 반복문이 필요할까요? 첫째, 코드의 양을 획기적으로 줄여줍니다.
100번 반복해야 하는 작업을 3-4줄로 표현할 수 있습니다. 둘째, 데이터의 개수가 변해도 코드 수정이 필요 없습니다.
예를 들어, 사용자가 10명이든 10만 명이든 같은 반복문 코드로 처리할 수 있습니다. 셋째, 배열이나 객체의 모든 요소를 순회하며 작업할 수 있습니다.
기존에는 for와 while 문만 있었다면, 현대 JavaScript에서는 forEach, map, filter 같은 배열 메서드와 for...of 문을 사용합니다. 이들은 더 간결하고 읽기 쉬운 코드를 만들어줍니다.
반복문의 핵심은 초기화, 조건, 증감의 세 요소입니다. for (let i = 0; i < 10; i++)에서 i = 0은 초기화, i < 10은 반복 조건, i++는 증감입니다.
이 세 가지를 잘 조합하면 어떤 복잡한 반복 패턴도 구현할 수 있습니다. 배열 메서드를 사용하면 이런 패턴을 더 선언적으로 표현할 수 있어 실수를 줄일 수 있습니다.
코드 예제
// 배열 준비
const fruits = ["사과", "바나나", "오렌지", "포도"];
const numbers = [1, 2, 3, 4, 5];
// for 문: 인덱스가 필요할 때
for (let i = 0; i < fruits.length; i++) {
console.log(`${i + 1}번째 과일: ${fruits[i]}`);
}
// for...of 문: 값만 필요할 때 (가장 권장)
for (const fruit of fruits) {
console.log(`좋아하는 과일: ${fruit}`);
}
// forEach: 배열 메서드로 순회
fruits.forEach((fruit, index) => {
console.log(`인덱스 ${index}: ${fruit}`);
});
// map: 새로운 배열 생성
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// filter: 조건에 맞는 요소만 선택
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4]
// while 문: 조건이 참인 동안 반복
let count = 0;
while (count < 3) {
console.log(`카운트: ${count}`);
count++; // 증가를 빼먹으면 무한 루프!
}
// 실전 예제: 장바구니 총액 계산
const cart = [
{ name: "노트북", price: 1200000 },
{ name: "마우스", price: 30000 },
{ name: "키보드", price: 80000 }
];
const total = cart.reduce((sum, item) => sum + item.price, 0);
console.log(`총액: ${total.toLocaleString()}원`); // 총액: 1,310,000원
설명
이것이 하는 일: 위 코드는 다양한 반복문 패턴을 통해 배열 데이터를 처리하는 방법을 보여줍니다. 실무에서 리스트 렌더링, 데이터 변환, 필터링, 집계 등에 필수적인 패턴들입니다.
첫 번째로, 전통적인 for 문은 가장 기본적인 반복 구조입니다. let i = 0으로 카운터를 초기화하고, i < fruits.length 조건이 참인 동안 반복하며, 매 반복마다 i++로 카운터를 증가시킵니다.
인덱스가 필요한 경우(첫 번째, 두 번째 같은 순서 표시)에 유용하지만, 현대 JavaScript에서는 더 간결한 방법들을 선호합니다. 그 다음으로, for...of 문은 배열의 각 요소를 직접 순회합니다.
인덱스를 관리할 필요가 없어 코드가 간결하고 실수할 가능성이 적습니다. "각 과일에 대해"라는 의도가 명확하게 드러나므로 가독성도 뛰어납니다.
단순히 모든 요소를 처리할 때는 이것이 가장 권장됩니다. forEach, map, filter 같은 배열 메서드는 함수형 프로그래밍 스타일입니다.
forEach는 각 요소에 대해 함수를 실행하고(부수 효과 목적), map은 각 요소를 변환하여 새 배열을 만들고, filter는 조건에 맞는 요소만 걸러냅니다. 이들은 원본 배열을 변경하지 않아 안전하고, 체이닝으로 연결할 수 있어 복잡한 데이터 처리를 선언적으로 표현할 수 있습니다.
while 문은 반복 횟수를 미리 알 수 없을 때 사용합니다. 조건이 거짓이 될 때까지 계속 반복하므로, 조건을 거짓으로 만드는 코드(count++)를 반드시 포함해야 합니다.
그렇지 않으면 무한 루프에 빠져 브라우저가 멈출 수 있습니다. 실전 예제의 reduce는 배열의 모든 요소를 하나의 값으로 축약합니다.
장바구니의 모든 상품 가격을 합산하는 것처럼, 집계 작업에 완벽합니다. sum은 누적값이고, item은 현재 처리 중인 요소이며, 0은 초기값입니다.
매 반복마다 sum + item.price를 계산하여 다음 반복의 sum으로 전달합니다. 여러분이 이 반복문들을 상황에 맞게 선택하면 코드가 간결해지고 버그도 줄어듭니다.
특히 배열 메서드를 사용하면 "무엇을 할지"에 집중할 수 있어 의도가 명확한 코드를 작성할 수 있습니다.
실전 팁
💡 가능하면 for...of나 배열 메서드를 사용하세요. 인덱스 관리 실수(i < length vs i <= length)를 방지할 수 있고, 코드 의도도 명확해집니다. 💡 map은 새 배열이 필요할 때, forEach는 부수 효과만 필요할 때 사용하세요. map의 반환값을 사용하지 않는다면 forEach가 더 적합합니다. 💡 무한 루프를 방지하려면 while 문에서 조건을 거짓으로 만드는 코드를 반드시 포함하세요. 또는 안전장치로 최대 반복 횟수를 설정할 수 있습니다. 💡 중첩 반복문은 성능 문제를 일으킬 수 있습니다. O(n²) 복잡도는 데이터가 커지면 급격히 느려지므로, Map이나 Set 같은 자료구조로 최적화를 고려하세요. 💡 배열 메서드를 체이닝할 때는 각 단계가 새 배열을 만든다는 점을 기억하세요. filter().map().reduce()는 3번 순회하므로, 성능이 중요하면 하나의 reduce로 합칠 수 있습니다.
5. 객체 - 관련 데이터의 구조화
시작하며
여러분이 회원가입 폼을 작성할 때를 떠올려보세요. 이름, 이메일, 비밀번호, 전화번호, 주소 등 수많은 정보를 입력하는데, 이 모든 정보를 각각 다른 변수에 저장한다면 관리가 얼마나 복잡할까요?
userName, userEmail, userPassword, userPhone, userAddress... 변수가 끝없이 늘어날 것입니다.
실제 프로그램에서는 서로 관련된 데이터를 하나로 묶어서 관리하는 것이 필수입니다. 사용자 정보, 상품 정보, 주문 정보 등 현실 세계의 개념을 코드로 표현하려면 여러 속성을 가진 구조가 필요합니다.
바로 이럴 때 필요한 것이 '객체'입니다. 객체는 관련된 데이터와 기능을 하나의 단위로 묶어주는 JavaScript의 핵심 자료구조입니다.
개요
간단히 말해서, 객체는 키(key)와 값(value) 쌍으로 이루어진 데이터 집합입니다. 마치 사전에서 단어(키)를 찾아 뜻(값)을 보는 것처럼, 속성 이름으로 값에 접근할 수 있습니다.
왜 객체가 필요할까요? 첫째, 관련된 데이터를 논리적으로 그룹화할 수 있습니다.
사용자의 모든 정보를 user라는 하나의 객체에 담으면 user.name, user.email처럼 일관되게 접근할 수 있습니다. 둘째, 함수도 객체의 속성으로 포함할 수 있어 데이터와 그 데이터를 다루는 행동을 함께 묶을 수 있습니다.
예를 들어, user.login() 메서드로 사용자 로그인 기능을 객체에 포함시킬 수 있습니다. 객체는 참조 타입이므로, 변수에 할당하면 실제 값이 아닌 메모리 주소가 복사됩니다.
이것이 원시 타입(숫자, 문자열 등)과 가장 큰 차이점입니다. 객체의 핵심 특징은 점 표기법(user.name)과 대괄호 표기법(user["name"])으로 속성에 접근할 수 있다는 점입니다.
구조 분해 할당을 사용하면 const { name, email } = user처럼 필요한 속성만 빠르게 추출할 수 있습니다. 또한 객체 스프레드 연산자(...)로 객체를 복사하거나 병합할 수 있어 불변성 유지에 유용합니다.
이러한 기능들은 React 같은 현대 프레임워크에서 필수적입니다.
코드 예제
// 객체 생성: 사용자 정보를 하나로 묶기
const user = {
name: "김개발",
age: 28,
email: "dev@example.com",
isActive: true,
// 메서드: 객체에 포함된 함수
greet: function() {
return `안녕하세요, ${this.name}입니다.`;
},
// 축약 문법으로 메서드 정의
getInfo() {
return `${this.name} (${this.age}세)`;
}
};
// 점 표기법으로 속성 접근
console.log(user.name); // 김개발
console.log(user.greet()); // 안녕하세요, 김개발입니다.
// 대괄호 표기법 (동적 키 접근 가능)
const key = "email";
console.log(user[key]); // dev@example.com
// 속성 추가 및 수정
user.job = "프론트엔드 개발자"; // 새 속성 추가
user.age = 29; // 기존 속성 수정
// 구조 분해 할당: 필요한 속성만 추출
const { name, email } = user;
console.log(name, email); // 김개발 dev@example.com
// 객체 복사 (스프레드 연산자)
const updatedUser = {
...user,
age: 30, // 특정 속성만 변경
city: "서울" // 새 속성 추가
};
// 객체 배열: 여러 사용자 관리
const users = [
{ id: 1, name: "김개발", role: "developer" },
{ id: 2, name: "이디자인", role: "designer" },
{ id: 3, name: "박기획", role: "pm" }
];
// 특정 사용자 찾기
const developer = users.find(u => u.role === "developer");
console.log(developer.name); // 김개발
설명
이것이 하는 일: 위 코드는 객체를 생성하고 조작하는 핵심 패턴들을 보여줍니다. 실무에서 API 응답 처리, 상태 관리, 컴포넌트 props 전달 등에 필수적인 기술입니다.
첫 번째로, 객체 리터럴 { }로 user 객체를 만들었습니다. name, age, email은 데이터 속성이고, greet와 getInfo는 메서드(객체에 속한 함수)입니다.
메서드 안에서 this는 현재 객체를 가리키므로, this.name은 user.name과 같습니다. 이렇게 데이터와 관련 기능을 하나로 묶으면 코드 조직화가 깔끔해집니다.
그 다음으로, 속성에 접근하는 두 가지 방법이 있습니다. 점 표기법(user.name)은 속성 이름을 직접 쓸 때 사용하고, 대괄호 표기법(user[key])은 변수나 계산된 값으로 접근할 때 사용합니다.
예를 들어 폼 필드 이름이 변수에 저장되어 있을 때 대괄호 표기법이 필수입니다. 객체는 생성 후에도 속성을 자유롭게 추가하거나 수정할 수 있습니다.
user.job = "개발자"처럼 작성하면 job 속성이 없어도 자동으로 추가됩니다. 하지만 const로 선언된 객체는 재할당은 불가능하되, 속성 변경은 가능합니다.
이것이 const의 중요한 특징입니다. 구조 분해 할당은 객체에서 필요한 속성만 변수로 추출하는 편리한 문법입니다.
const { name, email } = user는 user.name과 user.email을 각각 name, email 변수에 할당합니다. 함수 매개변수에서도 사용할 수 있어 function updateUser({ name, age })처럼 필요한 속성만 받을 수 있습니다.
스프레드 연산자(...)는 객체를 복사하거나 병합할 때 사용합니다. { ...user }는 user의 모든 속성을 새 객체로 복사합니다.
이어서 age: 30을 쓰면 복사된 객체의 age만 덮어씁니다. 이 패턴은 React에서 상태를 불변하게 업데이트할 때 필수적입니다.
여러분이 객체를 잘 다루면 복잡한 데이터 구조를 명확하게 표현하고, API 데이터를 효율적으로 처리하며, 유지보수하기 쉬운 코드를 작성할 수 있습니다. 특히 객체 배열을 다루는 능력은 현대 웹 개발의 핵심입니다.
실전 팁
💡 속성명과 변수명이 같으면 축약할 수 있습니다. { name: name, age: age } 대신 { name, age }로 작성하면 간결합니다. 💡 옵셔널 체이닝(?.)을 사용하세요. user?.address?.city처럼 작성하면 중간 속성이 없어도 에러 없이 undefined를 반환합니다. 💡 객체를 복사할 때 스프레드 연산자는 얕은 복사만 합니다. 중첩 객체가 있으면 JSON.parse(JSON.stringify(obj))나 structuredClone()을 사용해야 합니다. 💡 Object.keys(), Object.values(), Object.entries()로 객체를 배열로 변환하여 반복문을 사용할 수 있습니다. 특히 entries()는 [key, value] 쌍을 반환해 편리합니다. 💡 객체의 속성 존재 여부는 'key' in obj나 obj.hasOwnProperty('key')로 확인하세요. obj.key는 값이 falsy면 false가 되므로 정확하지 않습니다.
6. 배열 메서드 - 데이터 변환의 핵심
시작하며
여러분이 쇼핑몰에서 "5만원 이상 상품만 보기" 필터를 사용하거나, 장바구니에서 "총 가격 계산" 버튼을 누를 때를 생각해보세요. 이런 기능들은 배열 데이터를 특정 방식으로 변환하거나 집계하는 작업입니다.
실제 웹 개발에서는 서버에서 받은 데이터 배열을 화면에 맞게 가공하는 일이 매우 흔합니다. 100개의 상품 중 재고 있는 것만 필터링하고, 각 상품에 할인가를 계산하고, 전체 매출을 합산하는 등의 작업을 매번 반복문으로 작성하면 코드가 길어지고 실수하기 쉽습니다.
바로 이럴 때 필요한 것이 '배열 메서드'입니다. map, filter, reduce 같은 메서드들은 데이터 변환을 선언적이고 간결하게 표현할 수 있게 해줍니다.
개요
간단히 말해서, 배열 메서드는 배열의 각 요소를 순회하면서 특정 작업을 수행하는 내장 함수들입니다. 마치 공장의 조립 라인처럼, 각 단계에서 데이터를 원하는 형태로 가공합니다.
왜 배열 메서드가 필요할까요? 첫째, 코드의 의도가 명확해집니다.
"모든 가격을 2배로"는 prices.map(p => p * 2)로 표현되어, 무엇을 하는지 한눈에 알 수 있습니다. 둘째, 원본 배열을 변경하지 않고 새 배열을 만들어 불변성을 유지합니다.
셋째, 메서드 체이닝으로 복잡한 변환을 단계별로 표현할 수 있습니다. 예를 들어, "재고 있는 상품만 필터링 → 할인가 계산 → 가격 합산"을 한 줄로 작성할 수 있습니다.
기존에는 for 반복문으로 임시 배열을 만들고 push로 추가했다면, 이제는 map, filter, reduce로 더 간결하게 표현합니다. 함수형 프로그래밍 스타일이 주류가 된 현대 JavaScript에서 필수 기술입니다.
배열 메서드의 핵심은 각 메서드의 목적을 정확히 이해하는 것입니다. map은 변환(각 요소를 다른 값으로), filter는 선택(조건에 맞는 요소만), reduce는 집계(모든 요소를 하나의 값으로), find는 검색(첫 번째 일치 요소), some/every는 검증(조건 충족 여부)에 사용됩니다.
이들을 조합하면 SQL의 SELECT, WHERE, GROUP BY처럼 강력한 데이터 처리가 가능합니다.
코드 예제
// 샘플 데이터: 상품 목록
const products = [
{ id: 1, name: "노트북", price: 1200000, inStock: true },
{ id: 2, name: "마우스", price: 30000, inStock: true },
{ id: 3, name: "키보드", price: 80000, inStock: false },
{ id: 4, name: "모니터", price: 350000, inStock: true }
];
// map: 모든 상품명만 추출 (변환)
const names = products.map(p => p.name);
console.log(names); // ["노트북", "마우스", "키보드", "모니터"]
// filter: 재고 있는 상품만 선택
const available = products.filter(p => p.inStock);
console.log(available.length); // 3개
// find: 특정 상품 찾기
const laptop = products.find(p => p.name === "노트북");
console.log(laptop.price); // 1200000
// reduce: 총 가격 계산 (집계)
const total = products
.filter(p => p.inStock) // 재고 있는 것만
.reduce((sum, p) => sum + p.price, 0);
console.log(total); // 1,580,000
// some: 하나라도 조건 충족하는지 확인
const hasExpensive = products.some(p => p.price > 1000000);
console.log(hasExpensive); // true
// every: 모두 조건 충족하는지 확인
const allInStock = products.every(p => p.inStock);
console.log(allInStock); // false (키보드 품절)
// 복합 변환: 재고 있고 10만원 이상 상품의 할인가
const discounted = products
.filter(p => p.inStock && p.price >= 100000)
.map(p => ({
...p,
discountedPrice: p.price * 0.9 // 10% 할인
}));
console.log(discounted);
// [{ id: 1, name: "노트북", price: 1200000, inStock: true, discountedPrice: 1080000 }, ...]
설명
이것이 하는 일: 위 코드는 실무에서 가장 많이 사용하는 배열 메서드들을 보여줍니다. API에서 받은 데이터를 화면에 표시하기 적합한 형태로 가공하는 실제 시나리오입니다.
첫 번째로, map은 배열의 각 요소를 변환하여 새 배열을 만듭니다. products.map(p => p.name)은 각 상품 객체에서 name 속성만 추출하여 문자열 배열을 반환합니다.
원본 products 배열은 변경되지 않고 그대로 유지됩니다. React에서 리스트를 렌더링할 때 가장 많이 사용하는 패턴입니다.
그 다음으로, filter는 조건에 맞는 요소만 선택합니다. p => p.inStock은 각 상품에 대해 inStock이 true인지 검사하고, 참인 것만 새 배열에 담습니다.
"검색 결과 필터링", "카테고리별 상품 보기" 같은 기능에 필수적입니다. find는 조건을 만족하는 첫 번째 요소를 반환합니다.
filter와 달리 배열이 아닌 단일 요소를 반환하며, 찾지 못하면 undefined를 반환합니다. "ID로 사용자 찾기", "장바구니에서 특정 상품 찾기" 같은 검색 작업에 사용합니다.
reduce는 가장 강력하지만 처음에는 이해하기 어려운 메서드입니다. 배열의 모든 요소를 순회하면서 누적값(accumulator)을 만들어갑니다.
(sum, p) => sum + p.price에서 sum은 이전까지의 합계이고, p는 현재 상품입니다. 매 반복마다 sum + p.price를 계산하여 다음 sum으로 전달합니다.
마지막 인자 0은 초기값입니다. 합계, 평균, 최댓값 찾기 등 집계 작업에 필수입니다.
some과 every는 불린 값을 반환합니다. some은 "하나라도 조건 충족?"을 검사하고(|| 연산자와 비슷), every는 "모두 조건 충족?"을 검사합니다(&& 연산자와 비슷).
폼 검증에서 "하나라도 에러가 있는지" 또는 "모든 필드가 유효한지" 확인할 때 유용합니다. 복합 변환 예제는 메서드 체이닝의 힘을 보여줍니다.
filter로 조건에 맞는 것만 선택하고, map으로 각 요소에 새 속성을 추가합니다. 스프레드 연산자로 기존 속성을 유지하면서 discountedPrice를 추가하는 것이 포인트입니다.
이런 패턴은 React/Redux에서 상태를 불변하게 업데이트할 때 매일 사용합니다. 여러분이 배열 메서드를 자유자재로 사용하면 데이터 중심 애플리케이션을 효율적으로 개발할 수 있습니다.
특히 서버 데이터를 UI에 맞게 가공하는 능력은 프론트엔드 개발자의 핵심 역량입니다.
실전 팁
💡 map의 반환값을 사용하지 않는다면 forEach를 사용하세요. map은 새 배열을 만드는 비용이 있으므로, 부수 효과만 필요하면 forEach가 더 적절합니다. 💡 filter 후 map 순서를 지키세요. 순서를 바꾸면 불필요한 변환을 많이 하게 됩니다. 먼저 필터링으로 데이터를 줄인 후 변환하는 것이 효율적입니다. 💡 reduce의 초기값을 꼭 제공하세요. 빈 배열에 초기값 없이 reduce를 호출하면 에러가 발생합니다. 객체를 만들 때는 {}를 초기값으로 사용합니다. 💡 find 대신 filter[0]을 사용하지 마세요. find는 첫 번째를 찾는 즉시 중단하지만, filter는 끝까지 순회해서 비효율적입니다. 💡 복잡한 체이닝은 변수로 분리하세요. 한 줄에 4개 이상 메서드를 연결하면 디버깅이 어려우므로, 중간 결과를 변수에 담으면 가독성과 디버깅이 쉬워집니다.
7. 에러 처리 - 안정적인 프로그램 만들기
시작하며
여러분이 인터넷 뱅킹 앱에서 송금 버튼을 눌렀는데 아무 반응이 없다면 어떨까요? 또는 갑자기 앱이 종료되면서 "예상치 못한 에러가 발생했습니다"라는 메시지만 나타난다면?
사용자 경험이 엉망이 될 것입니다. 실제 프로그램에서는 수많은 에러 상황이 발생합니다.
네트워크 연결 실패, 잘못된 사용자 입력, 존재하지 않는 파일 접근, 서버 응답 지연 등 예상치 못한 일들이 항상 일어납니다. 이런 상황을 처리하지 않으면 프로그램이 멈추거나 잘못된 동작을 하게 됩니다.
바로 이럴 때 필요한 것이 '에러 처리'입니다. try-catch 문과 적절한 에러 핸들링으로 예외 상황에서도 안정적으로 동작하는 프로그램을 만들 수 있습니다.
개요
간단히 말해서, 에러 처리는 프로그램 실행 중 발생할 수 있는 예외 상황을 감지하고 적절히 대응하는 메커니즘입니다. 마치 안전벨트나 에어백처럼, 문제가 생겨도 큰 피해 없이 대응할 수 있게 해줍니다.
왜 에러 처리가 필요할까요? 첫째, 프로그램 중단을 방지합니다.
에러가 발생해도 catch 블록에서 잡아서 대안 동작을 수행할 수 있습니다. 둘째, 사용자에게 친절한 메시지를 보여줄 수 있습니다.
"undefined를 읽을 수 없습니다" 대신 "데이터를 불러오는데 실패했습니다. 다시 시도해주세요"라고 알려줄 수 있습니다.
예를 들어, API 호출 실패 시 에러를 잡아서 재시도 버튼을 보여주거나 캐시된 데이터를 사용할 수 있습니다. 기존에는 에러를 무시하거나 if 문으로 확인했다면, 현대 JavaScript에서는 try-catch와 async/await의 조합으로 비동기 에러까지 일관되게 처리합니다.
Promise의 .catch()도 자주 사용됩니다. 에러 처리의 핵심은 에러를 예상하고 방어적으로 코딩하는 것입니다.
사용자 입력은 항상 검증하고, 외부 API 호출은 실패할 수 있다고 가정하며, null/undefined 체크를 습관화해야 합니다. 또한 에러 메시지에 충분한 컨텍스트를 포함시켜 디버깅을 쉽게 만들어야 합니다.
로깅과 모니터링도 에러 처리의 중요한 부분입니다.
코드 예제
// 기본 try-catch: 에러 발생 가능한 코드 감싸기
function divideNumbers(a, b) {
try {
if (b === 0) {
// 의도적으로 에러 발생시키기
throw new Error("0으로 나눌 수 없습니다");
}
return a / b;
} catch (error) {
// 에러 잡아서 처리
console.error("에러 발생:", error.message);
return null; // 안전한 기본값 반환
}
}
console.log(divideNumbers(10, 2)); // 5
console.log(divideNumbers(10, 0)); // null (에러 처리됨)
// 비동기 함수의 에러 처리
async function fetchUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
// HTTP 에러 체크
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
// 네트워크 에러나 JSON 파싱 에러 처리
console.error("사용자 데이터 로딩 실패:", error.message);
return null;
}
}
// finally: 성공/실패 상관없이 항상 실행
function processData(data) {
let file = null;
try {
file = openFile(data); // 파일 열기
// 파일 처리 로직
} catch (error) {
console.error("파일 처리 중 에러:", error);
} finally {
// 에러 발생 여부와 관계없이 항상 실행
if (file) {
file.close(); // 리소스 정리
}
}
}
// 커스텀 에러 클래스
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function validateEmail(email) {
if (!email.includes("@")) {
throw new ValidationError("유효하지 않은 이메일 형식입니다");
}
return true;
}
try {
validateEmail("invalid-email");
} catch (error) {
if (error instanceof ValidationError) {
console.log("검증 실패:", error.message);
} else {
console.log("알 수 없는 에러:", error);
}
}
설명
이것이 하는 일: 위 코드는 동기/비동기 상황에서 에러를 안전하게 처리하는 다양한 패턴을 보여줍니다. 실무에서 API 호출, 파일 처리, 사용자 입력 검증 등에 필수적입니다.
첫 번째로, try-catch 블록의 기본 구조입니다. try 안에는 에러가 발생할 수 있는 코드를 작성하고, catch 안에는 에러 발생 시 실행할 코드를 작성합니다.
throw new Error()로 의도적으로 에러를 발생시킬 수 있습니다. divideNumbers 함수는 0으로 나누는 경우를 감지하여 에러를 던지고, catch에서 잡아서 안전한 기본값(null)을 반환합니다.
이렇게 하면 함수를 호출한 쪽에서 프로그램이 멈추지 않습니다. 그 다음으로, async/await와 함께 사용하는 에러 처리입니다.
fetch는 네트워크 에러나 잘못된 URL에서 에러를 던집니다. response.json()도 잘못된 JSON 형식이면 에러를 던집니다.
이 모든 에러를 하나의 catch 블록에서 잡을 수 있습니다. 또한 response.ok를 체크하여 404나 500 같은 HTTP 에러도 처리합니다.
이 패턴은 REST API 호출에서 표준입니다. finally 블록은 에러 발생 여부와 관계없이 항상 실행됩니다.
파일을 열었거나, 데이터베이스 연결을 만들었거나, 로딩 스피너를 표시했다면 반드시 정리해야 합니다. try에서 성공하든 catch에서 에러를 처리하든, finally에서 file.close() 같은 정리 코드가 실행되어 리소스 누수를 방지합니다.
커스텀 에러 클래스는 에러 타입을 구분할 수 있게 해줍니다. ValidationError처럼 의미 있는 이름을 붙이면 instanceof로 에러 종류를 판단하여 다르게 처리할 수 있습니다.
예를 들어, 검증 에러는 사용자에게 메시지를 보여주고, 네트워크 에러는 재시도 버튼을 표시하는 식으로 구분할 수 있습니다. 여러분이 적절한 에러 처리를 하면 사용자는 문제 상황에서도 좌절하지 않고 대안을 찾을 수 있습니다.
또한 개발자는 에러 로그를 통해 문제를 빠르게 파악하고 수정할 수 있습니다. 에러 처리는 프로페셔널한 소프트웨어와 아마추어 코드를 구분하는 중요한 기준입니다.
실전 팁
💡 에러 메시지에 충분한 정보를 포함하세요. "에러 발생" 대신 "사용자 ID 123의 프로필 로딩 실패: 네트워크 타임아웃"처럼 컨텍스트를 담으면 디버깅이 쉬워집니다. 💡 빈 catch 블록은 절대 사용하지 마세요. catch (error) { } 처럼 에러를 무시하면 문제를 찾을 수 없습니다. 최소한 console.error로 로깅하거나 모니터링 서비스로 전송하세요. 💡 Promise는 .catch()로 에러를 처리할 수 있지만, async/await + try-catch가 더 읽기 쉽습니다. 일관성을 위해 프로젝트에서 한 가지 스타일을 정하세요. 💡 전역 에러 핸들러를 설정하세요. window.onerror나 window.addEventListener('unhandledrejection')로 잡히지 않은 에러를 모니터링하면 예상치 못한 버그를 발견할 수 있습니다. 💡 에러를 사용자에게 보여줄 때는 기술 용어를 피하고 해결 방법을 제시하세요. "CORS policy violation" 대신 "데이터를 불러올 수 없습니다. 페이지를 새로고침해주세요"가 훨씬 친절합니다.
8. 비동기 프로그래밍 - 시간이 걸리는 작업 다루기
시작하며
여러분이 식당에서 음식을 주문하면 주방에서 요리하는 동안 계속 기다리고만 있나요? 아니죠, 다른 사람과 대화하거나 휴대폰을 보면서 시간을 보냅니다.
음식이 준비되면 직원이 가져다주죠. 프로그래밍에서도 비슷한 상황이 있습니다.
서버에서 데이터를 가져오거나, 큰 파일을 읽거나, 타이머가 끝나기를 기다리는 동안 프로그램이 멈춰 있으면 사용자는 답답함을 느낍니다. 웹페이지가 데이터를 로딩하는 동안 완전히 얼어붙는다면 최악의 사용자 경험이 될 것입니다.
바로 이럴 때 필요한 것이 '비동기 프로그래밍'입니다. 시간이 걸리는 작업을 백그라운드에서 처리하고, 결과가 준비되면 콜백이나 Promise로 알려받는 방식입니다.
개요
간단히 말해서, 비동기 프로그래밍은 작업이 완료될 때까지 기다리지 않고 다음 코드를 계속 실행하는 방식입니다. 마치 세탁기를 돌려놓고 빨래가 끝날 때까지 다른 일을 하는 것과 같습니다.
왜 비동기 프로그래밍이 필요할까요? JavaScript는 싱글 스레드 언어라서, 한 번에 하나의 작업만 처리할 수 있습니다.
만약 서버 응답을 받는 3초 동안 프로그램이 멈춘다면, 그 시간 동안 사용자는 아무 버튼도 클릭할 수 없습니다. 비동기 처리를 사용하면 네트워크 요청을 보내놓고, UI는 계속 반응하게 할 수 있습니다.
예를 들어, 인스타그램에서 스크롤하면서 새 이미지를 로딩하는 것이 가능한 이유가 비동기 처리 덕분입니다. 기존에는 콜백 함수로만 비동기를 처리했다면(콜백 지옥 문제), 현대 JavaScript에서는 Promise와 async/await로 훨씬 깔끔하게 작성합니다.
async/await는 비동기 코드를 동기 코드처럼 읽기 쉽게 만들어줍니다. 비동기 프로그래밍의 핵심은 "언제 완료될지 모르는" 작업을 다루는 것입니다.
Promise는 미래의 결과를 나타내는 객체로, pending(진행 중), fulfilled(성공), rejected(실패)의 상태를 가집니다. async/await는 이 Promise를 마치 동기 코드처럼 작성할 수 있게 해주는 문법적 설탕입니다.
특히 여러 비동기 작업을 순차적으로 또는 병렬로 처리하는 패턴을 이해하는 것이 중요합니다.
코드 예제
// Promise 기본: 비동기 작업을 나타내는 객체
function delay(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`${ms}ms 완료`);
}, ms);
});
}
// Promise 체이닝
delay(1000)
.then(result => {
console.log(result); // "1000ms 완료"
return delay(500);
})
.then(result => {
console.log(result); // "500ms 완료"
})
.catch(error => {
console.error("에러:", error);
});
// async/await: Promise를 더 읽기 쉽게
async function fetchAndProcess() {
try {
// await는 Promise가 완료될 때까지 기다림
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log("데이터 수신:", data);
return data;
} catch (error) {
console.error("데이터 로딩 실패:", error);
return null;
}
}
// 여러 비동기 작업 병렬 실행
async function loadMultipleUsers() {
try {
// Promise.all: 모든 Promise가 완료될 때까지 기다림
const [user1, user2, user3] = await Promise.all([
fetch("https://api.example.com/users/1").then(r => r.json()),
fetch("https://api.example.com/users/2").then(r => r.json()),
fetch("https://api.example.com/users/3").then(r => r.json())
]);
console.log("모든 사용자 로딩 완료");
return [user1, user2, user3];
} catch (error) {
console.error("사용자 로딩 중 하나 이상 실패:", error);
}
}
// 순차 실행 vs 병렬 실행 비교
async function sequentialExample() {
// 순차 실행: 총 3초 소요 (1초 + 1초 + 1초)
const result1 = await delay(1000);
const result2 = await delay(1000);
const result3 = await delay(1000);
}
async function parallelExample() {
// 병렬 실행: 총 1초 소요 (동시에 실행)
const results = await Promise.all([
delay(1000),
delay(1000),
delay(1000)
]);
}
// 실전 예제: 사용자 프로필과 게시물 동시 로딩
async function loadUserProfile(userId) {
try {
const [profile, posts] = await Promise.all([
fetch(`/api/users/${userId}`).then(r => r.json()),
fetch(`/api/users/${userId}/posts`).then(r => r.json())
]);
return { profile, posts };
} catch (error) {
console.error("프로필 로딩 실패:", error);
throw error;
}
}
설명
이것이 하는 일: 위 코드는 비동기 작업을 다루는 핵심 패턴들을 보여줍니다. 실무에서 API 호출, 파일 로딩, 타이머 등 거의 모든 I/O 작업에 사용됩니다.
첫 번째로, Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다. new Promise((resolve, reject) => {})로 생성하며, 작업이 성공하면 resolve()를, 실패하면 reject()를 호출합니다.
delay 함수는 setTimeout을 Promise로 감싸서, 일정 시간 후에 resolve를 호출합니다. 이렇게 하면 콜백 대신 .then()으로 결과를 받을 수 있습니다.
그 다음으로, Promise 체이닝은 .then()을 연결하여 순차적인 비동기 작업을 표현합니다. 첫 번째 then에서 반환한 Promise가 완료되면 두 번째 then이 실행됩니다.
.catch()는 체인 어디서든 발생한 에러를 잡습니다. 하지만 체인이 길어지면 읽기 어려워지므로, async/await가 더 선호됩니다.
async/await는 Promise를 동기 코드처럼 작성하게 해주는 문법입니다. async 함수 안에서 await 키워드를 사용하면, Promise가 완료될 때까지 기다렸다가 결과를 반환받습니다.
await fetch()는 네트워크 요청이 완료될 때까지 기다리고, await response.json()은 JSON 파싱이 완료될 때까지 기다립니다. 이 방식은 .then() 체이닝보다 훨씬 읽기 쉽고 디버깅도 편합니다.
Promise.all()은 여러 Promise를 동시에 실행하고 모두 완료될 때까지 기다립니다. 3명의 사용자 정보를 가져올 때, 순차적으로 하면 3초가 걸리지만 Promise.all()을 사용하면 병렬로 실행되어 1초면 됩니다.
단, 하나라도 실패하면 전체가 실패하므로, 일부 실패를 허용하려면 Promise.allSettled()를 사용해야 합니다. 순차 vs 병렬 실행의 차이를 이해하는 것이 중요합니다.
await를 연속으로 쓰면 순차 실행(앞 작업이 끝나야 다음 시작)되고, Promise.all()을 사용하면 병렬 실행(모두 동시 시작)됩니다. 서로 독립적인 작업이라면 병렬로 실행하여 성능을 크게 개선할 수 있습니다.
여러분이 비동기 프로그래밍을 마스터하면 빠르고 반응성 좋은 애플리케이션을 만들 수 있습니다. 특히 SPA(Single Page Application)에서 데이터 페칭은 핵심이므로, async/await와 Promise.all() 패턴은 필수입니다.
실전 팁
💡 await는 async 함수 안에서만 사용할 수 있습니다. 최상위 레벨에서 사용하려면 즉시 실행 함수 (async () => { await ... })()로 감싸세요. 💡 독립적인 비동기 작업은 Promise.all()로 병렬 실행하세요. 순차 실행(await를 여러 번)은 불필요하게 느립니다. 네트워크 요청은 대부분 병렬 가능합니다. 💡 async 함수는 항상 Promise를 반환합니다. return "hello"를 해도 실제로는 Promise.resolve("hello")가 반환되므로, 호출하는 쪽에서 await를 사용해야 합니다. 💡 에러 처리를 잊지 마세요. await를 사용할 때는 try-catch로, .then()을 사용할 때는 .catch()로 에러를 잡아야 합니다. 안 그러면 unhandled promise rejection 경고가 발생합니다. 💡 Promise.race()는 가장 빠른 Promise의 결과만 사용합니다. 타임아웃 구현이나 여러 서버 중 가장 빠른 응답 선택에 유용합니다.