JavaScript

JavaScript - 자바스크립트란 무엇인가? (심화편-3 객체, 프로토타입 등 )

인어공쭈 2024. 9. 22. 14:56

자바스크립트는 객체지향적 성격과 함수형 프로그래밍의 요소를 모두 담고 있는 언어다. 이러한 특징을 이해하려면 자바스크립트의 객체, 프로토타입 기반 상속, 클래스, 그리고 비동기 처리의 작동 방식을 잘 알아야 한다. 이 포스트에서는 객체, 프로토타입, 클래스 기반 객체지향 프로그래밍, 비동기 처리, 이터러블, 이벤트의 핵심 개념들을 통합적으로 설명해보겠다.

 

객체와 인스턴스: 객체지향의 출발점

객체는 자바스크립트에서 데이터와 함수의 집합으로, 프로퍼티(속성)와 메서드(동작)를 포함하는 독립적인 엔티티다. 객체는 단순한 키-값 쌍으로 표현될 수 있으며, 함수처럼 더 복잡한 구조를 가질 수도 있다. 객체가 생성자 함수클래스를 통해 만들어진 경우, 그 결과로 생성된 개별 객체를 인스턴스라고 부른다.

 

 

 

프로토타입: 객체지향의 핵심 개념

자바스크립트는 전통적인 클래스 기반 객체지향 언어와는 달리 프로토타입 기반 객체지향 프로그래밍 방식을 사용한다. 모든 자바스크립트 객체는 다른 객체로부터 상속받으며, 그 상속 구조를 프로토타입 체인이라고 한다. **프로토타입(Prototype)**은 객체가 자신을 만들 때 참조하는 원형 객체다. 각 객체는 내부적으로 **__proto__**라는 숨겨진 프로퍼티를 통해 자신의 프로토타입을 참조한다.

프로토타입 상속과 프로토타입 체이닝

프로토타입 상속은 객체가 다른 객체의 속성이나 메서드를 상속받을 수 있는 메커니즘이다. 객체가 특정 프로퍼티나 메서드를 찾지 못하면, 프로토타입 체인을 따라 상위 객체에서 이를 찾게 된다. 이는 클래스 기반 언어에서의 상속 개념과 유사하지만, 더 동적이고 유연하다.

prototype과 __proto__

  • prototype: 생성자 함수의 속성으로, 이 생성자로 만들어진 모든 객체는 이 프로토타입 객체를 참조한다.
  • __proto__: 객체의 내부 프로퍼티로, 그 객체가 참조하는 프로토타입을 가리킨다. 이를 통해 상위 객체와의 연결이 이루어진다.
function Person(name) {
  this.name = name;
}

Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}`);
};

const john = new Person('John');
john.greet();  // 'Hello, my name is John'

 

이 예시에서, Person의 인스턴스인 john은 자신의 프로토타입 객체에서 greet 메서드를 상속받아 사용할 수 있다.

 

 

 

클래스와 생성자 함수

ES6 이후 자바스크립트는 클래스 문법을 도입하여, 더 직관적이고 전통적인 객체지향 프로그래밍 방식을 지원한다.

클래스의 특징

  • 클래스 문법은 생성자 함수보다 읽기 쉽고 명확하다.
  • **constructor**는 클래스를 초기화하는 역할을 한다.
  • static 메서드는 클래스 자체에서 호출되며, 인스턴스에 직접 속하지 않는다.

클래스와 프로토타입 기반 상속의 차이

  • 클래스는 명시적이고, 전통적인 객체지향 구조를 제공한다.
  • 프로토타입 기반 객체지향은 더 동적이며, 런타임에 객체 간 상속을 쉽게 조작할 수 있다.

클래스와 함수의 주요 차이점

1. 클래스는 new 없이 호출할 수 없다:

  • 클래스는 항상 new 키워드를 사용해 인스턴스를 생성해야 한다. new 없이 호출하려고 하면 에러가 발생한다.
class Person {}
const john = Person();  // TypeError: Class constructor Person cannot be invoked without 'new'

 

2. 클래스는 함수 호이스팅이 되지 않는다:

  • 함수 선언은 호이스팅되지만, 클래스 선언은 호이스팅되지 않으므로 선언 전에 호출할 수 없다.
console.log(Person);  // ReferenceError: Cannot access 'Person' before initialization
class Person {}

 

3.클래스는 엄격 모드(strict mode)를 기본 적용:

  • 클래스는 자동으로 엄격 모드에서 실행되기 때문에 더 엄격한 규칙을 따르게 된다. 예를 들어, 암시적인 전역 변수 생성을 방지한다.

 

 

Object.create() 메서드란?

Object.create() 메서드는 자바스크립트에서 새로운 객체를 생성하는 데 사용되며, 생성된 객체의 프로토타입을 명시적으로 지정할 수 있다. 이를 통해 특정 객체를 프로토타입으로 삼는 새로운 객체를 만들 수 있다.

const person = {
  greet() {
    console.log('Hello!');
  }
};

const student = Object.create(person);
student.greet();  // 'Hello!', student 객체는 person 객체를 프로토타입으로 상속받음

 

이 예시에서 student 객체는 person 객체를 자신의 프로토타입으로 설정하여 greet 메서드를 상속받았다. 이 방식은 자바스크립트의 프로토타입 상속을 보다 명확하게 설정할 수 있게 해준다.

클래스와의 관련성

Object.create()는 자바스크립트에서 클래스 없이도 객체 상속을 구현할 수 있는 방법이다. 하지만 ES6 이후로 클래스 문법이 도입되면서, 클래스 내부에서 상속을 쉽게 구현할 수 있게 되었다. Object.create()는 클래스 문법이 생기기 전 객체 기반 상속을 구현하는 데 자주 사용되었으며, 여전히 특정 상황에서 유용하게 쓰인다.

 

 

super 키워드란?

super 키워드는 자바스크립트에서 상속받은 부모 클래스의 생성자나 메서드를 호출할 때 사용된다. 클래스 상속에서 자식 클래스가 부모 클래스의 메서드나 속성을 사용할 때 유용하다.

class Person {
  constructor(name) {
    this.name = name;
  }
  
  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

class Student extends Person {
  constructor(name, grade) {
    super(name);  // 부모 클래스의 생성자 호출
    this.grade = grade;
  }

  greet() {
    super.greet();  // 부모 클래스의 메서드 호출
    console.log(`I am in grade ${this.grade}`);
  }
}

const student = new Student('Alice', 10);
student.greet();
// Hello, my name is Alice
// I am in grade 10

 

이 예시에서 super 키워드는 자식 클래스 Student가 부모 클래스 Person의 생성자와 greet 메서드를 호출하는 데 사용되었다. 이를 통해 부모 클래스의 기능을 재사용하면서 추가적인 로직을 구현할 수 있다.

 

 

instanceof는 어떤 상황에서 사용하는 걸까?

 

instanceof 연산자는 객체가 특정 생성자 함수나 클래스에서 생성되었는지 확인하는 데 사용된다. 이를 통해 객체가 특정 클래스의 인스턴스인지 확인할 수 있다.

class Person {}

const john = new Person();

console.log(john instanceof Person);  // true
console.log(john instanceof Object);  // true, 모든 객체는 Object의 인스턴스

 

instanceof는 객체가 어느 클래스나 생성자 함수의 인스턴스인지를 확인하는 데 사용된다. 이를 통해 클래스 상속 관계를 확인하거나, 특정 클래스의 인스턴스인지 검증할 수 있다.

 

 

비동기 처리: Promise와 async/await

 

비동기 처리는 자바스크립트의 중요한 부분이다. 자바스크립트는 단일 스레드 기반 언어이기 때문에, 비동기 처리가 필요한 작업을 효율적으로 관리할 수 있어야 한다. 이를 위해 Promiseasync/await가 사용된다.

Promise란?

Promise는 자바스크립트에서 비동기 작업의 결과를 처리하는 객체로, 작업의 완료 여부를 나타내는 3가지 상태를 갖는다.

  1. 대기 중(pending): 비동기 작업이 아직 완료되지 않은 상태.
  2. 이행됨(fulfilled): 비동기 작업이 성공적으로 완료된 상태.
  3. 거부됨(rejected): 비동기 작업이 실패한 상태.
더보기

fetch 함수의 반환된 Promise는 어떤 조건에서 rejected 상태가 될까요?

fetch 함수는 네트워크 요청을 보내며, 다음과 같은 경우에 Promise가 rejected 상태가 된다:

  1. 네트워크 오류(Network Failure): 인터넷 연결이 끊기거나 서버가 응답하지 않는 경우.
  2. CORS 오류: CORS 정책에 의해 브라우저가 요청을 차단할 때.
  3. DNS 실패: 요청한 도메인을 찾을 수 없는 경우.
  4. 서버에 도달할 수 없거나 요청이 타임아웃된 경우.

 

추가적으로 thenable이라는 개념과 Promise.all, Promise.race와 같은 메서드는 비동기 프로그래밍에서 자주 사용되는 기능들이다. 이 글에서는 thenable의 개념과 Promise의 유용한 메서드들인 Promise.allPromise.race에 대해 설명하겠다.

thenable 의 개념은 Promise 객체가 then 메서드를 사용할 수 있다는 것을 의미한다. 즉, Promise 객체는 비동기 작업의 결과를 처리하기 위해 then 메서드를 호출할 수 있는 객체라는 뜻이다.

thenable이 될 수 있는 조건

  1. then 메서드를 가진 객체: 객체가 then 메서드를 가지고 있다면, 그 객체는 thenable이라고 부를 수 있다.
  2. then 메서드는 두 개의 인자를 받아야 한다: 첫 번째 인자는 비동기 작업이 성공했을 때 실행되는 onFulfilled 함수, 두 번째 인자는 작업이 실패했을 때 실행되는 onRejected 함수이다.
const thenableObj = {
  then: function(onFulfilled, onRejected) {
    onFulfilled('Success!');
  }
};

Promise.resolve(thenableObj).then(result => {
  console.log(result);  // 'Success!'
});

 

위 코드에서 thenableObj는 then 메서드를 가지고 있기 때문에 thenable한 객체로 간주된다. Promise.resolve()는 이러한 thenable 객체를 감싸서 Promise처럼 사용할 수 있게 해준다.

Promise.all과 Promise.race

Promise.allPromise.race는 여러 개의 Promise를 동시에 처리할 때 유용한 메서드이다. 두 메서드는 Promise 배열을 입력으로 받고, 그 배열의 Promise가 어떻게 완료되는지에 따라 다른 방식으로 결과를 반환한다.

Promise.all

**Promise.all**은 전달된 모든 Promise가 성공해야지만 결과를 반환하는 메서드다. 모든 Promise가 이행되면, 그 결과를 배열로 반환한다. 만약 하나의 Promise라도 거부된다면, 즉시 Promise.all은 거부된 상태로 끝나며, 나머지 Promise는 무시된다.

const promise1 = Promise.resolve(10);
const promise2 = Promise.resolve(20);
const promise3 = Promise.resolve(30);

Promise.all([promise1, promise2, promise3]).then(results => {
  console.log(results);  // [10, 20, 30], 모든 Promise가 완료되면 배열로 반환
}).catch(error => {
  console.error(error);  // 하나라도 실패하면 오류 처리
});

 

위 예시에서 세 개의 Promise가 모두 성공적으로 완료되면, 결과 배열 [10, 20, 30]이 출력된다. 하나라도 실패하면 전체 결과는 실패로 간주된다.

Promise.race

**Promise.race**는 전달된 Promise 중 가장 먼저 완료되는 Promise의 결과를 반환한다. 즉, 배열 안의 Promise 중 하나라도 가장 빨리 이행되거나 거부되면, 그 즉시 해당 Promise의 결과를 반환하고 나머지는 무시된다.

const promise1 = new Promise((resolve) => setTimeout(resolve, 100, 'First'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 50, 'Second'));

Promise.race([promise1, promise2]).then(result => {
  console.log(result);  // 'Second', 더 빨리 완료된 Promise가 반환됨
});

 

위 예시에서는 promise2가 50ms 후에 완료되므로, Promise.race는 가장 빠른 promise2의 결과인 'Second'를 반환한다.

Promise.all과 Promise.race의 차이점

  • Promise.all모든 Promise가 성공적으로 완료되어야 결과를 반환하며, 중간에 하나라도 실패하면 전체가 실패로 간주된다.
  • Promise.race가장 먼저 완료된 Promise의 결과를 반환하며, 나머지 Promise의 상태는 무시된다

 

Promise와 콜백 함수의 차이

콜백 함수는 함수 내부에서 비동기 작업이 완료될 때 호출되는 방식이다. 하지만 콜백 함수는 중첩되기 쉬워 콜백 지옥을 초래할 수 있다. 반면 Promise는 체이닝을 통해 이러한 문제를 해결하고, 비동기 작업을 더 쉽게 관리할 수 있게 해준다.

async/await의 역할

async/await 구문은 Promise를 더 직관적으로 사용할 수 있게 한다. 비동기 작업을 마치 동기 작업처럼 작성할 수 있어 코드 가독성이 크게 향상된다.

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

 

 

fetch 함수

fetch 함수JavaScript에서 네트워크 요청을 보내기 위한 최신 비동기 함수로, HTTP 요청을 보내고 응답을 처리하는데 사용된다. Promise 기반으로 동작하며, 비동기적으로 서버와 데이터를 주고받을 수 있다.

fetch 함수의 주요 특징:

  • Promise 기반: fetch는 네트워크 요청이 완료되었을 때 Promise를 반환하며, 성공하면 fulfilled 상태, 실패하면 rejected 상태가 된다.
  • 요청 옵션: 기본적으로 GET 메서드로 요청하지만, 옵션을 통해 POST, PUT, DELETE 등의 다른 HTTP 메서드를 사용할 수 있다.
  • 응답 처리: 서버로부터 받은 응답은 Promise 객체로 반환되며, .then() 메서드로 응답을 처리할 수 있다.
fetch('https://api.example.com/data')
  .then(response => response.json()) // 응답을 JSON으로 파싱
  .then(data => console.log(data))   // 파싱된 데이터를 출력
  .catch(error => console.error('Error:', error)); // 에러 처리

 

fetch 함수의 HTTP 요청은 기본적으로 어떤 메서드로 보내질까요?

 

fetch 함수는 기본적으로 GET 메서드로 요청을 보낸다. 만약 POST나 다른 메서드를 사용하려면 옵션을 명시적으로 설정해야 한다.

//기본스타일
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data));

// post 요청
fetch('https://api.example.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'John', age: 30 })
})
  .then(response => response.json())
  .then(data => console.log(data));

 

 

이터러블과 제너레이터

 

이터러블(Iterable) 객체는 반복 가능한 객체를 의미하며, 자바스크립트의 for...of 문에서 사용될 수 있다. 이터레이터(Iterator)는 이터러블 객체를 순차적으로 처리할 수 있게 해주는 메커니즘이다.

제너레이터(Generator)

제너레이터는 이터레이터를 쉽게 구현할 수 있게 해주는 함수로, yield 키워드를 사용해 중간 값을 반환할 수 있다. 제너레이터는 중단되었다가 다시 실행되는 특성을 가지고 있어, 큰 데이터를 처리하거나 복잡한 반복 작업을 관리하는 데 유용하다.

function* numberGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

const generator = numberGenerator();
console.log(generator.next().value);  // 1
console.log(generator.next().value);  // 2

 

 

 

이벤트 처리와 CustomEvent

자바스크립트에서는 다양한 이벤트를 처리할 수 있다. 이벤트 버블링이벤트 캡처링은 이벤트가 발생할 때 DOM 트리에서 어떻게 전파되는지를 설명하는 개념이다. 이벤트 버블링은 이벤트가 자식 요소에서 부모 요소로 전파되는 방식이고, 이벤트 캡처링은 반대로 부모에서 자식으로 전파된다.

CustomEvent

CustomEvent는 개발자가 사용자 정의 이벤트를 생성하고, 그 이벤트를 트리거할 때 사용한다. 이를 통해 특정 상황에 맞는 커스텀 이벤트를 정의하여 더 유연한 상호작용을 구현할 수 있다.

const event = new CustomEvent('custom', { detail: { data: 123 } });
document.dispatchEvent(event);

 

 

 

이벤트 위임

 

이벤트 위임(Event Delegation)은 하위 요소에 개별적으로 이벤트 리스너를 붙이는 대신, 공통된 상위 요소에 하나의 이벤트 리스너를 등록하여, 이벤트가 상위 요소에서 처리되도록 하는 기법이다. 이 방식은 하위 요소가 많거나 동적으로 추가될 때, 성능을 최적화하고 코드의 유지보수성을 높이는 데 유용하다.

동작 원리

이벤트 위임은 이벤트 버블링(Event Bubbling) 메커니즘을 활용한다. 이벤트 버블링이란, 특정 요소에서 발생한 이벤트가 상위 요소로 전파되는 것을 의미한다. 따라서 하위 요소에서 발생한 이벤트를 상위 요소에서 캡처하여 처리할 수 있다.

예를 들어, 동적으로 생성되는 여러 버튼에 각각 이벤트 리스너를 붙이는 대신, 이 버튼들의 상위 요소에 하나의 리스너만 등록하여 버튼이 클릭되었을 때 그 이벤트를 처리할 수 있다.

<div id="buttonContainer">
  <button class="dynamic-btn">Button 1</button>
  <button class="dynamic-btn">Button 2</button>
</div>

<script>
  // 상위 요소에 이벤트 리스너 등록
  document.getElementById('buttonContainer').addEventListener('click', function(event) {
    // 클릭한 요소가 특정 클래스인 경우에만 이벤트 처리
    if (event.target.classList.contains('dynamic-btn')) {
      console.log(event.target.textContent + ' clicked');
    }
  });
</script>

이벤트 위임의 장점:

  1. 성능 최적화: 많은 하위 요소에 각각 이벤트 리스너를 등록하는 대신, 상위 요소에 하나의 리스너만 등록함으로써 메모리 사용량을 줄일 수 있다.
  2. 유지보수성 향상: 동적으로 추가되거나 제거되는 하위 요소에도 별도의 코드 변경 없이 상위 리스너로 이벤트 처리가 가능하다.
  3. 코드 간결화: 하위 요소가 많을 때도 코드가 간결해지며, 관리가 쉬워진다.

이벤트 위임을 사용할 수 있는 경우:

  • 동적으로 추가되는 요소에 대한 이벤트 처리가 필요한 경우.
  • 다수의 유사한 하위 요소에 이벤트를 적용해야 하는 경우.
반응형