지난 글에서 JavaScript 심화 내용을 일부분 다루며 학습해보았다. 이번 글에서는 그 내용에 이어서 계속해서 JavaScript 심화라고 할 수 있는 내용을 더욱 다뤄보려 한다. Symbol이라는 개념부터 시작해볼 것이다.
🔷 11. Symbol (심볼)
✅ Symbol이란?
"고유하고 충돌되지 않는 값"을 만들기 위한 데이터 타입
이 고유한 값은 객체의 프로퍼티 키로 사용할 수 있다.
const sym = Symbol("설명");
- 설명(description)은 디버깅용 텍스트일 뿐 실제 Symbol 값에는 아무런 영향 없음
- Symbol 값은 절대 중복되지 않음. 동일한 설명을 넣어도 서로 다른 Symbol이 만들어짐
✅ 왜 Symbol이 필요할까?
JS 객체의 속성 키는 보통 문자열:
const user = { id: "user_1" };
→ 라이브러리나 외부 코드와 충돌하지 않는 진짜 고유한 키가 필요할 때가 있다.
→ 이 경우에 Symbol을 키로 사용하면 절대 충돌이 발생하지 않는 상황을 연출할 수 있다.
✅ Symbol의 주요 특징 요약
특징 | 설명 |
고유성 | Symbol("x") !== Symbol("x") |
변경 불가 | 한 번 생성된 Symbol은 바꿀 수 없음 |
객체 키로 사용 가능 | 숨겨진 키로 활용 가능 |
열거되지 않음 | for...in, Object.keys()에 안 나옴 |
✅ 간단한 예시
const id = Symbol("userId");
const user = {
name: "이름",
[id]: 12345
};
console.log(user[id]); // 12345
→ id는 외부에서 우연히 겹칠 일이 전혀 없는 키가 된다.
✅ 열거되지 않는다는 특징
for (let key in user) {
console.log(key); // name만 출력됨
}
→ Symbol 키는 for...in, Object.keys() 등 일반적인 열거 방식에서 완전히 숨겨지는 특징을 가지고 있다.
필요한 경우 다음 메서드로 확인이 가능함:
메서드 | 설명 |
Object.getOwnPropertySymbols(obj) | 객체의 모든 Symbol 키를 반환 |
Reflect.ownKeys(obj) | 문자열 키 + 심볼 키 모두 반환 |
⚙️ 요약 정리
키워드 | 설명 |
Symbol() | 고유하고 변경 불가능한 값 생성 |
객체 키 | obj[Symbol()] = value |
숨김 속성 | 열거되지 않음 (for...in 등에서 제외) |
용도 | 라이브러리 충돌 방지, 내부 키 은닉 |
🔷 12. 이터러블과 제너레이터
✅ 이터러블(Iterable)이란?
반복이 가능한 객체를 뜻한다.
for...of 반복문에서 사용할 수 있는 객체들.
📌 대표적인 이터러블
- 배열: [1, 2, 3]
- 문자열: "hello"
- Set, Map
- 제너레이터 (→ 직접 만드는 반복자)
✅ 이터러블의 핵심 조건
이터러블이 되려면 반드시 Symbol.iterator라는 메서드가 있어야 한다.
const arr = [1, 2, 3];
console.log(typeof arr[Symbol.iterator]); // 'function'
→ 즉, 이터러블은 내부적으로 Symbol.iterator()를 호출하면 "이터레이터(iterator)" 객체를 반환해야 한다.
✅ 이터레이터(Iterator)란?
순서대로 값을 하나씩 꺼낼 수 있는 객체
이터레이터는 next()라는 메서드를 가지고 있고, 호출할 때마다 다음 값을 반환하게 된다.
반환 형식:
{ value: 다음값, done: 끝났는지 여부(true/false) }
✅ 이터러블 vs 이터레이터 비교
개념 | 설명 |
이터러블 | 반복 가능한 객체 (배열, 문자열 등) |
이터레이터 | next() 메서드로 값을 순차적으로 꺼낼 수 있는 객체 |
이터레이터를 쉽게 만들 수 있도록 도와주는 함수이다.
기본 함수와 다르게 function으로 선언하고, yield 키워드로 값을 하나씩 반환한다.
function* gen() {
yield 1;
yield 2;
yield 3;
}
→ 위와같이 구성하면 gen()은 제너레이터 객체를 반환하고, next()로 값을 하나씩 꺼낼 수 있게 된다.
⚙️ 요약 정리
개념 | 설명 |
Iterable | 반복 가능한 객체 (for...of) |
Iterator | next()로 하나씩 꺼냄 |
Generator | 이터레이터를 쉽게 만들기 위한 특수 함수 |
사용 예시 | 커스텀 루프, 무한 수열, Map/Set, 내부 상태 유지 |
🔷 13. 옵셔널 체이닝 (Optional Chaining)
✅ 옵셔널 체이닝이란?
객체의 깊은 속성에 접근할 때, null이나 undefined가 있어도, 에러를 발생시키지 않고 undefined를 반환하게 해주는 연산자
문법:
object?.property
object?.[index]
object?.method?.()
→ 핵심 기호는 ?.
✅ 필요한 이유
기존 방식 (에러 발생 가능성 O)
const user = {};
console.log(user.address.city); // ❌ TypeError 발생
✅ 옵셔널 체이닝 방식
console.log(user.address?.city); // 👉 undefined (에러 ❌)
→ 즉, 중간 단계에 값이 없을 수도 있다는 걸 고려할 수 있게 해주는 매우 실용적인 문법이라고 할 수 있다.
✅ 사용 가능한 경우
사용 위치 | 예시 | 설명 |
객체 속성 접근 | user?.name | user가 undefined여도 OK |
배열 요소 접근 | users?.[0]?.name | users가 없거나 비어도 OK |
함수 호출 | user.greet?.() | 함수 없으면 undefined 반환 |
✅ null 병합 연산자와 함께 사용
??는 왼쪽 값이 null 또는 undefined일 때만 오른쪽 값 사용한다.
const user = {};
const name = user.name ?? "이름 없음";
→ 이걸 ?.랑 같이 사용할 경우,
const user = {};
const city = user.address?.city ?? "주소 없음";
→ 안전하게 속성 접근하면서 기본값도 설정 가능하게 된다.
⚙️ 요약 정리
문법 | 설명 |
?. | 안전한 속성 접근 (null/undefined 무시) |
사용 위치 | 객체, 배열, 함수 호출 |
함께 쓰기 | ?? (null 병합 연산자) |
핵심 장점 | 에러 방지 + 기본값 설정 가능 |
🔷 14. 렉시컬 스코프(Lexical Scope) & 클로저(Closure)
✅ 렉시컬 스코프란?
함수가 정의된 위치(문법적 위치)에 따라 변수에 접근할 수 있는 범위가 결정되는 규칙
→ "렉시컬"은 "문법 구조상"이라는 뜻으로 함수가 어디서 정의되었는지가 중요하며, 어디서 호출되는지는 상관없다.
✅ 예시로 보는 렉시컬 스코프
let a = 1;
function outer() {
let b = 2;
function inner() {
let c = 3;
console.log(a, b, c); // 1, 2, 3
}
inner();
}
- inner()는 outer() 안에서 정의됐기 때문에,
- outer의 스코프(b), 전역 스코프(a) 모두 접근 가능하다.
→ 이처럼 함수가 어디서 정의됐는지에 따라 스코프가 결정되는 걸 렉시컬 스코프라고 한다.
✅ 클로저란?
함수와, 그 함수가 선언될 때의 렉시컬 환경을 함께 기억하는 것
→ 즉, 함수가 외부 함수의 변수들을 기억해서 계속 사용할 수 있게 되는 현상
✅ 중요한 이유
- 함수 밖으로 나간 변수(외부 스코프의 변수)인데도 계속 기억하고 사용할 수 있다.
- 위의 내용을 통해 데이터 은닉, 상태 유지, 함수형 프로그래밍 구현이 가능해지는 장점을 가진다.
✅ 클로저 예시
function counterMaker() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = counterMaker();
console.log(counter()); // 1
console.log(counter()); // 2
- 내부 함수가 외부 변수 count를 계속 기억하고 있다.
- counter()가 실행될 때마다 count는 유지된 채로 증가하는 걸 확인할 수 있다.
⚙️ 요약 정리
개념 | 설명 |
렉시컬 스코프 | 정의된 위치 기준으로 스코프 결정 |
클로저 | 외부 함수의 변수(스코프)를 기억하는 함수 |
특징 | 외부 변수에 지속적으로 접근 가능 |
실무 용도 | 상태 유지, 은닉, 캡슐화, 고차 함수 |
🔷 15. this 키워드
✅ this란?
현재 실행 중인 함수가 “어떤 객체를 통해 호출되었는가”에 따라 달라지는 특수한 값으로,
“누가 나를 불렀는가”에 따라 그 객체를 가리킨다.
✅ this가 달라지는 기준
→ 호출 방식에 따라 달라진다.
호출 방식 | this가 가리키는 대상 |
일반 함수 호출 | undefined (strict mode) / 전역 객체 (window) |
메서드 호출 | 해당 메서드를 소유한 객체 |
생성자 호출 (new) | 새로 생성된 인스턴스 |
화살표 함수 | 상위 스코프의 this (렉시컬 this) |
call, apply, bind | 명시적으로 지정한 객체 |
📌 1. 일반 함수 호출
function show() {
console.log(this);
}
show(); // ❗ strict mode면 undefined, 아니면 window
📌 2. 객체의 메서드로 호출
const user = {
name: "이름",
sayHi() {
console.log(this.name);
}
};
user.sayHi(); // 이름 ← user 객체가 this
📌 3. 생성자 함수에서 this
function Person(name) {
this.name = name;
}
const p = new Person("이름");
console.log(p.name); // 이름
→ new 키워드로 호출되면, this는 새로운 인스턴스를 가리키게 된다.
📌 4. 화살표 함수에서 this
const user = {
name: "이름",
sayHi: () => {
console.log(this.name);
}
};
user.sayHi(); // undefined (전역 this)
→ 화살표 함수는 자신의 this를 만들지 않는다. 상위 스코프의 this를 “그대로” 사용
📌 5. call, apply, bind로 명시적 지정
function greet() {
console.log(this.name);
}
const person = { name: "지훈" };
greet.call(person); // 지훈
greet.apply(person); // 지훈
const bound = greet.bind(person);
bound(); // 지훈
✅ this가 헷갈리는 상황
이벤트 핸들러 안에서의 this
const btn = document.querySelector("button");
btn.addEventListener("click", function () {
console.log(this); // 클릭된 버튼 요소
});
→ 이때 this는 이벤트를 바인딩한 DOM 요소이다.
⚙️ 요약 정리
호출 방식 | this |
일반 함수 | 전역 객체 (또는 undefined) |
메서드 | 호출한 객체 |
생성자 | 새로 생성된 객체 |
화살표 함수 | 상위 스코프의 this |
call/apply/bind | 명시적으로 지정한 객체 |
🔷 16. 비동기 프로그래밍 (콜백, Promise, async/await)
✅ 비동기란?
어떤 작업이 지연되는 동안에도 자바스크립트가 다른 작업을 계속 실행할 수 있는 방식
⏳ 예: 서버에서 데이터 불러오기
console.log("1");
setTimeout(() => console.log("2"), 1000);
console.log("3");
출력:
1
3
2 ← 1초 뒤에 실행됨
→ 자바스크립트는 싱글 스레드지만, 비동기 처리로 지연된 작업을 Event Loop로 분리 실행한다.
✅ 콜백 함수 (Callback)
비동기 작업이 끝난 후 실행할 함수를 인자로 전달
function fetchData(callback) {
setTimeout(() => {
callback("데이터");
}, 1000);
}
fetchData(data => {
console.log(data);
});
→ 콜백은 동시에 가독성 떨어지고, 에러 처리가 복잡하다는 문제점을 가지고 있다.
✅ Promise
비동기 작업의 성공/실패 결과를 나중에 전달하는 객체
기본 구조
const promise = new Promise((resolve, reject) => {
// 비동기 작업
resolve("성공"); // 또는 reject("실패");
});
사용 방법
promise
.then(result => console.log(result))
.catch(error => console.error(error));
- then() → 성공 시 실행
- catch() → 실패 시 실행
✅ async / await
Promise 기반 비동기 코드를 더 간단하게 작성하는 문법
기본 구조
async function getData() {
try {
const res = await fetch("https://api.com/user");
const data = await res.json();
console.log(data);
} catch (e) {
console.error("에러 발생!", e);
}
}
장점
- try...catch로 에러 처리 가능
- 동기 코드처럼 읽기 쉬움
⚙️ 요약 정리
구분 | 특징 |
콜백 함수 | 가장 기본적인 비동기 처리 방식 (중첩 문제 존재) |
Promise | 비동기 작업을 객체로 감싸서 처리 흐름 분리 |
async/await | Promise를 더 쉽게, 가독성 좋게 사용 |
🔷 17. 예외 처리 (Error Handling)
어떤 코드에서 예상치 못한 오류가 발생해도
전체 프로그램이 멈추지 않도록 “안전하게 대처”해야 하기에 필수적으로 필요하다.
예를 들어 서버에서 값을 못 받아왔거나, 사용자 입력이 잘못됐을 때 → 멈추지 말고 의도된 대처 로직을 실행해야 한다.
✅ 기본 구조 – try / catch
try {
// 에러가 날 가능성이 있는 코드
} catch (err) {
// 에러 발생 시 실행되는 코드
}
- try: 위험한 코드
- catch: 에러가 발생했을 때 실행될 안전 로직
✅ 예시
try {
JSON.parse('잘못된 JSON');
} catch (e) {
console.log('에러 발생!', e.message);
}
✅ finally
에러 발생 여부와 관계없이 무조건 실행되는 블록
try {
console.log("시도");
} catch (e) {
console.log("실패");
} finally {
console.log("항상 실행됨");
}
→ 리소스 정리(예: 파일 닫기, 네트워크 종료)에 자주 사용되는 것을 볼 수 있다.
✅ throw
의도적으로 에러를 “던질” 수 있음
function divide(a, b) {
if (b === 0) throw new Error("0으로 나눌 수 없습니다!");
return a / b;
}
- throw는 에러 객체나 메시지를 강제로 발생
- → try/catch로 잡아야 한다.
⚙️ 요약 정리
키워드 | 설명 |
try | 위험한 코드 실행 |
catch | 에러 발생 시 대처 로직 |
finally | 항상 실행되는 정리 코드 |
throw | 에러 강제 발생 |
⁉️ Q. Symbol / Iterator & Generator / Closure 부분의 추가 학습 내용
프로퍼티,,, 분명 앞에서 공부하고 넘어온 개념이고, 가장 기본적인 개념이라고 할 수 있다. 하지만 정확한 정의가 기억나지 않아서 정리하며 다시 학습하기로 했다.
먼저 프로퍼티에 대해 간단하게 정리하고 넘어가면, 프로퍼티(property)란 자바스크립트 객체의 속성을 말한다. 즉, 키-값 쌍 하나하나를 프로퍼티라고 한다.
다음으로는 Iterator와 Generator에 관련된 의문이다. 분명 이 개념들에 대해서는 이해했고, 어떻게 사용되는지도 알았는데 왜 필요하고, 어디에 사용되는지를 모르니 답답한 느낌이 있었다.
왜 필요하고, 어디에 사용되는지 알아보면, 먼저 Iterator는 데이터를 하나씩 반복해서 꺼낼 수 있는 표준 인터페이스가 필요하고, for...of 문, 스프레드 문법 등이 작동하기 위한 기반이 필요하기에 Iterator가 필요하다고 한다. 배열/문자열 순회, Set/Map을 순회할 때 주로 사용된다고 한다.
다음은 Generator이다. 이터레이터를 더 쉽게 만들고, 중간에 함수 실행을 멈췄다가 다시 시작할 수 있는 특별한 함수가 필요했기 때문에 필요하다고 한다. 주로 지연 평가, 무한 시퀀스 생성, 비동기 흐름 제어 등에 사용된다고 한다.
⁉️ Q. This 부분의 추가 학습 내용
This를 공부할 때, This가 어디를 가리키고 있는 것인지를 확인하는 것이 굉장히 헷갈렸다. 그래서 다시 예시 코드를 통해 이해해보려 한다.
// 1. 생성자 함수 정의
function Person(name, age) {
// 여기서 this는 곧 만들어질 인스턴스를 의미함
this.name = name;
this.age = age;
// 메서드도 this를 통해 정의 가능
this.introduce = function() {
console.log(`안녕하세요, 저는 ${this.name}이고, ${this.age}살입니다.`);
};
}
// 2. new 키워드를 통해 인스턴스 객체 생성
const user1 = new Person("누구", 24);
const user2 = new Person("지훈", 25);
// 3. 각각의 인스턴스에서 this는 각각 user1, user2를 의미함
user1.introduce(); // "안녕하세요, 저는 누구이고, 24살입니다."
user2.introduce(); // "안녕하세요, 저는 지훈이고, 25살입니다."
코드에 주석을 달아서 각 this가 어떤것을 의미하는지 알아보았다. 아직은 계속해서 헷갈릴 것 같기에 코드를 많이 보면서 익숙해져야 할 것 같다.
렉시컬 스코프(Lexical Scope)에 대해서도 헷갈리는 내용이 있었지만 위에서 정리해보며 조금은 정리된 것 같아 넘어가려 한다.
짧은 시간동안 JavaScript를 공부하고 이해해보며 언어 하나를 익히는데 엄청난 시간이 들겠구나 라고 느꼈다. 그래도 Python을 다룰 수 있었기에 이해하기에 조금이나마 수월한 부분이 있었을 것이라고 생각하며, 실습을 진행하고 지금 하고 있는 토스 페이지 클론 코딩을 직접 작성하며 이해하는 부분이 클 것 같다.
본 후기는 [카카오엔터프라이즈x스나이퍼팩토리] 카카오클라우드로 배우는 AIaaS 마스터 클래스 (B-log) 리뷰로 작성 되었습니다.