Developer.

[멋사 백엔드 19기] TIL 28일차 JavaScript 기초 문법

📂 목차


📚 본문

스프레드 연산자

자바스크립트에서 스프레드 연산자(…) 은 배열이나 객체 같은 이터러블 자료 구조를 펼쳐서 요소를 나열하거나, 얕은 복사 및 병합 등에 자주 쓰이게 된다.

const arr = [1, 2, 3];
console.log(...arr); // 1 2 3

const arr = [1, 2, 3];
const copy = [...arr];
console.log(copy); // [1, 2, 3]

const arr1 = [1, 2];
const arr2 = [3, 4];
const merged = [...arr1, ...arr2];
console.log(merged); // [1, 2, 3, 4]

const obj = { a: 1, b: 2 };
const copy = { ...obj };
console.log(copy); // { a: 1, b: 2 }

const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = { ...obj1, ...obj2 };
console.log(merged); // { a: 1, b: 3, c: 4 } 
// 뒤에 오는 속성이 앞에 속성을 덮어씀

function sum(x, y, z) {
  return x + y + z;
}

const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6

클래스

자바스크립트에서는 클래스보다는 객체를 만드는 함수를 정의하여 그 함수를 객체 생성자로 하여 사용한다.

// 객체 생성자 함수
function Animal(type, name, sound) {
    this.type = type;
    this.name = name;
    this.sound = sound;
    this.say = function() {
        console.log(this.sound);
    };
}

// 인스턴스 생성
const dog = new Animal('', '멍멍이', '멍멍');
const cat = new Animal('고양이', '야옹이', '야옹');

dog.say();  // 멍멍
cat.say();  // 야옹

위는 구버전에서의 클래스 선언이었다.

ES6 클래스 선언

class Animal {
    constructor(type, name, sound) {
        this.type = type;
        this.name = name;
        this.sound = sound;
    }
    say() {
        console.log(this.sound);
    }
}

// 인스턴스 생성
const dog = new Animal('', '멍멍이', '멍멍');
const cat = new Animal('고양이', '야옹이', '야옹');

dog.say();  // 멍멍
cat.say();  // 야옹

이제 overriding 기능을 수행 할 prototype 을 본다.

프로토타입

하지만 여기서 자바스크립트는 클래스 기반 언어와는 달리 프로토타입 기반 언어인데, 모든 객체는 자신을 생성한 함수의 prototype 객체를 참조하게 된다. 이 prototype 객체에 정의된 속성과 메서드는 해당 생성자로 만든 모든 인스턴스가 공유하게 된다.

function Person(name) {
  this.name = name;
}

// prototype에 메서드 정의
Person.prototype.sayHello = function () {
  console.log(`Hi, I'm ${this.name}`);
};

const p1 = new Person("Alice");
const p2 = new Person("Bob");

p1.sayHello(); // Hi, I'm Alice
p2.sayHello(); // Hi, I'm Bob

프로토타입 체인

객체가 속성/메서드를 찾을 때, 다음과 같은 과정을 거친다.

  1. 자기 자신 속성 확인
  2. 없으면 _proto_(부모 프로토타입) 탐색
  3. 계속 올라가다가 최상위 Object.prototype 까지 감
  4. 그래도 없으면 undefined

ES6 상속

class Animal {
    constructor(type, name, sound) {
        this.type = type;
        this.name = name;
        this.sound = sound;
    }

    say() {
        console.log(this.sound);
    }
}

// extends로 상속
class Dog extends Animal {
    constructor(name, sound) {
        super('', name, sound);  // 부모 생성자 호출
    }
}

class Cat extends Animal {
    constructor(name, sound) {
        super('고양이', name, sound);
    }
}

const dog = new Dog('멍멍이', '멍멍');
const cat = new Cat('야옹이', '야옹');
const dog2 = new Dog('왈왈이', '왈왈');
const cat2 = new Cat('냐옹이', '냐옹');

dog.say();   // 멍멍
cat.say();   // 야옹
dog2.say();  // 왈왈
cat2.say();  // 냐옹

에러 처리

java 와 유사하게 throw new Error() 라는 구문을 통해 에러를 던질 수 있다. 마찬가지로 상위 프로시저에서 try-catch 문을 사용해 에러를 잡을 수 있고, finally 도 똑같다. 던져지는 error 객체에는 message 가 있다.

function divide(a, b) {
  if (b === 0) {
    throw new Error("0으로 나눌 수 없습니다.");
  }
  return a / b;
}

try {
  console.log(divide(10, 2)); // 5
  console.log(divide(10, 0)); // 에러 발생
} catch (err) {
  console.error("에러 발생:", err.message);
} finally {
  console.log("연산 완료");
}

ES2026 isError

어떤 객체가 에러 객체인지 판단할 수 있는 static 한 메서드가 있다. 거의 모든 브라우저에 지원하며, instanceof Error 보다 훨씬 낫다. 이 구문의 문제점은 다음과 같다:

  • 프레임워크/환경 간 호환 문제: 브라우저, Node.js, iframe, Web Worker 등 서로 다른 실행 컨텍스트에 대해 Error 클래스가 달리 취급된다는 것
  • 서브클래스 처리 문제: 커스텀 에러에 대한 처리가 제대로 동작하지 않음

이 때문에 Error.isError() 함수로 위 한계를 피할 수 있게 되었다.

console.log(Error.isError(e)); // true

동기 vs 비동기

원래 동기의 사전적 의미는 같은 시간에 맞춰 함께 움직이는 것을 의미한다. 즉, 코드가 순서대로 시간에 맞춰서 차근차근 작업이 되어야 함을 의미한다. 비동기라는 것은 이것을 부수고, 코드의 순서가 바뀌어진다는 것이다.

지금까지는 자바스크립트는 싱글스레드로, 하나하나 순서대로 실행했다. 하지만 이는 굉장히 많은 데이터를 서버에 요청하였을때, 방대한 양의 데이터는 로딩하는데 조금 걸리게 된다. 이때 사용자는 멈춰있는 화면만 보게 된다면 처리에 대한 피드백이 되어지는지를 알 수 없다. 이를 처리하기 위해 비동기가 나왔고, 비동기를 구현하는 다양한 방법을 살펴보자.

setTimeout()

console.log("A");

setTimeout(() => {
  console.log("B");
}, 1000); // 1000ms = 1초 후 실행

console.log("C");

출럭은 A C B 순서로 된다. setTimeout 내부 함수는 타이머가 끝날 때까지 기다렸다가 실행되긴 하지만, 메인 스레드는 멈추지 않고 다음 코드(C) 를 바로 실행할 것이다. 타이머가 끝나면 이벤트 큐(Event Queue) 에 callback이 들어가고, 스택이 비워진 후 실행된다.

여기서 의문을 가져야 할 것은 Call Stack 에 작업이 어떻게 쌓일지이다. 즉 코드가 길거나 무거워서 콜 스택이 꽉찬 상태에서 비동기 콜백이 어떻게 실행되는지 궁금할 수 있다.

Call Stack

자바스크립트는 기본적으로 싱글 스레드라고 했다. 한 번에 하나씩 작업을 수행하는데, 이러한 작업들은 Call Stack 이라고 불리우는 곳에 쌓이고 하나씩 처리된다. 여기서 setTimeout 과도 같은 비동기 콜백은 타이머가 끝난 후에 Event Queue 라는 곳에 들어가는데, 이 이벤트 큐라는 것은 콜 스택이 바쁘면 이벤트 큐에서 기다리는, 잠시 대기하는 대기실 용도이다.

결과적으로 동기 작업이 길어진다면, setTimeout 도 지연이 된다. 또한 이 delay 뿐 아니라, setTimeout 은 약 4ms 정도 딜레이를 기본적으로 주어지게 했다고 한다(곧바로 실행시키게 되면 컴퓨터 자원을 혼자 자원을 너무 많이 차지해버리기 때문이라고 한다). 그렇다면 곧바로 실행되게 하려면 어떻게 해야할까?

Window Scheduler

윈도우 스케쥴러에는 postTask 라는 최신 브라우저 환경에서 제공하는 API 가 있다. 이는 즉시 이벤트 큐에 태스크를 등록하여 콜스택이 비워진 직후에 어떤 지연도 없이 즉시 실행에 가깝게 처리된다.

console.log("A");

// postTask로 바로 실행
if ('scheduler' in window) {
  window.scheduler.postTask(() => {
    console.log("B (postTask)");
  });
} else {
  console.log("postTask를 지원하지 않는 브라우저입니다.");
}

console.log("C");

Promise

비동기 작업을 행하는데 있어서 위처럼 쓰게 되면 여러 개가 겹칠 수 있는 상황이 발생한다. 이를 콜백 지옥이라고 하는데, 이를 막고자 자바스크립트에서는 Promise 라는 객체를 만들게 된다.

우선 생명 주기에서 Promise 는 3가지 상태가 있다.

  • 상태(State)
    • Pending: 대기 상태
    • Fulfilled: 작업 성공
    • Rejected: 작업 실패

위 상태를 가지고 Promise 가 어떻게 실행되는지 보자.

Promise 생성

const promise = new Promise((resolve, reject) => {
  const success = true;

  if (success) {
    resolve("작업 성공!");   // 작업 완료 시 호출
  } else {
    reject("작업 실패!");   // 실패 시 호출
  }
});

new Promise 생성자 안에는 executor 함수가 들어간다. executor 함수는 두 인자를 받는데 두 인자 또한 함수이다.

  • resolve(value) -> Fulfilled 상태로 변환
  • reject(error) -> Rejected 상태로 변환

만약 그럼 성공했다면(fulfilled 라면) 그 다음 실행하고 싶은 함수는 promise 객체 다음 chaining methodthen 을 쓰면 된다(then 다음 또 then 을 쓸 수 있다). 만약 실패했다면 catch 를 써서 reject 상태를 처리할 수 있다.

new Promise((resolve) => resolve(1))
  .then(result => {
    console.log(result); // 1
    return result + 1;
  })
  .then(result => {
    console.log(result); // 2
    return result + 1;
  })
  .then(result => {
    console.log(result); // 3
  })
  .catch(error => {
    console.error("실패: ", error);
  })
  .finally(() => {
    console.log("작업 끝")
  });
Promise.all

모든 Promise 가 완료되면 Fulfilled 상태가 됨

Promise.all([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
]).then(results => console.log(results)); // [1, 2, 3]
Promise.race

어떤 Promise 라도 완료되면 해당 Promise 완료되는 값을 가져와 사용

Promise.race([
  new Promise(res => setTimeout(() => res("첫 번째"), 1000)),
  new Promise(res => setTimeout(() => res("두 번째"), 500))
]).then(result => console.log(result)); // "두 번째"

async/await

Promiseasyncawait 랑 결합을 하여 사용할 수 있다. async 는 말 그대로 비동기로 실행하라는 소리이며, 이는 콜백이 다 끝나고 나서 실행하라는 소리이다.

await 는 의미 자체로 기다리라는 의미이다. Promisefulfilled 될 때까지 기다리고, 프로미스는 1초 후 “데이터” 를 넘겨주는 약속을 하고 있다. 여기서 스레드는 그 다음 코드를 실행하지 않고 이를 기다리게 된다.

async function fetchData() {
  try {
    const result = await new Promise(resolve => setTimeout(() => resolve("데이터"), 1000));
    console.log(result); // 데이터
  } catch (err) {
    console.error(err);
  }
}

fetchData();

그럼 실행의 시간적인 순서로 따진다면

  • 동기 코드(Callback 이든 일반 코드든) 먼저 실행
  • 기다리고 있는 비동기 fetchData() 를 실행 -> async 함수 내부 실행 시작
  • 실행할 때 await 를 만나 함수의 나머지 실행만 일시 중단 Promise 객체가 fulfilled 될 때까지 기다리게 됨
    • 이때 주의점은 전체 스레드를 멈추는게 아니기 때문에 해당 스레드만 멈추고 다른 이벤트 루프는 게속 진행된다.
    • 브라우저의 API 내부에 컨텍스트가 생성되어 그 결과를 넘겨주는 것이기 때문
  • 1초 후에 “데이터” 를 넘겨 받고, 다음 명령문을 실행하게 된다.
async function example() {
  console.log("A");

  const result = await new Promise(resolve => 
    setTimeout(() => resolve("B"), 1000)
  );

  console.log(result); // B
  console.log("C");
}

example();
console.log("D");

✒️ 용어

callback

다른 함수의 인자로 전달되어서 특정 시점에 호출되는 함수를 콜백 함수라고 하는데 주로 비동기 처리에서 작업이 끝난 뒤에 후속 작업이나 에러 처리 작업을 수행하도록 할 때 일컫는 모든 함수들을 말한다.

동기 콜백

function greet(name, callback) {
  console.log(`안녕하세요, ${name}!`);
  callback();
}

greet("Alice", () => console.log("인사 완료!"));
// 출력:
// 안녕하세요, Alice!
// 인사 완료!

비동기 콜백

setTimeout(() => {
  console.log("1초 후 실행되는 콜백");
}, 1000);