면접질문 준비

김기훈·2023년 4월 25일
0

자바스크립트

목록 보기
17/17

Promise와 Callback 차이

  • callback을 사용하면 비동기 로직의 결과값을 처리하기 위해 callback 안에서만 처리를 해야하고, 밖에서는 비동기에서 온 값을 알 수가 없다.
  • promise를 사용하면 비동기에서 온 값이 promise 객체에 저장되기 때문에 .then 메소드를 통해서 저장되어 있는 값을 원하는 때에 사용할 수 있다.

콜백 지옥(Callback hell)을 해결하는 방법

콜백(callback) : 다른함수의 전달인자로 넘겨주는 함수

document.querySelector('#link').addEventListener('click', (e) => {
  console.log('clicked!');
});

1) 콜백지옥 😈

콜백 함수를 익명 함수로 전달하는 과정에서 또 다시 콜백 안에 함수 호출이 반복되어 코드의 들여쓰기 순준이 감당하기 힘들 정도로 깊어지는 현상

step1(function (value1) {
    step2(function (value2) {
        step3(function (value3) {
            step4(function (value4) {
                step5(function (value5) {
                     // Do something with value5
                });
            });
        });
    });
});

2) 콜백 지옥 해결 방법

1) Promise

  • Promise는 자바스크립트에서 비동기 처리에 사용되는 객체로 내용은 실행 되었지만 결과를 아직 반환하지 않은 객체라고 이해해도 좋다.
  • 3가지 상태가 있는데 비동기 처리가 완료 되지 않았다면 Pending, 완료 되었다면 Fulfilled, 실패하거나 오류가 발생하였다면 Rejected 상태를 갖는다.
  • Promise는 호출될 때 바로 실행되지만 그 안의 콜백은 resolve, reject 둘 중 하나가 호출 되기 전에 then 또는 catch로 넘어가지 않는다.
  • then으로 작업을 이어가기 위해서는 resolve() 함수를 호출한다.
  • 작업을 중단 혹은 error 처리를 위해서는 reject() 함수를 호출한다.

2) async/await

  • 가장 최근의 나온 비동기 처리 문법으로 기존의 callback 이나 Promise 의 단점을 해소하고자 만들어졌다.
  • 비동기 작업을 수행하고자하는 함수 앞에 async를 표기하고, 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await을 표기하면 해당 내용이 resolve된 이후에야 다음으로 진행된다.
  • async를 표기한 함수는 return을 하지 않아도 자동으로 Promise를 return 한다.
  • await을 붙인 Promise는 resolve에 인자로 전달한 데이터를 리턴한다.
async function fn() {
  let text = '하나';
  
  //"둘"을 리턴
  text = text + await new Promise((resolve, reject) => {
    setTimeout(() => { resolve('둘') }, 0);
  }); 
  
  text += '셋';
  console.log(text + '넷');
}

Promise와 async, await를 사용한 비동기 통신의 차이

1) 에러 핸들링

  • Promise 를 활용할 시에는 .catch() 문을 통해 에러 핸들링이 가능하지만, async/await 은 try-catch() 문을 활용해야 한다.
  • Promise를 연속으로 호출 할 때 어느 지점에서 에러가 발생하면 어떤 then에서 에러가 발생했는지 찾기가 어렵지만 async를 사용하게 되면, 어떤 지점에서 에러가 발생했는지 쉽게 찾을 수 있다.

function samplePromise() {
  return sampleFunc()
    .then(data => return data)
    .then(data2 => return data2)
    .then(data3 => return data3)
    .catch(err => console.log(err))  // 결과적으로 문제가 발생했다
}

async function sampleAsync() {
  const data1 = await sampleFunc();      // 문제 발생시 data1값이 유효치 않음
  const data2 = await sampleFunc2(data1);
  return data2;
}

2) 코드 가독성

  • Promise는 .then() 지옥의 가능성이 있다.
  • async/await 은 비동기 코드가 동기 코드처럼 읽히게 해준다. 코드 흐름을 이해 하기 쉽다.

스코프 (Scope)

모든 식별자 (변수 이름, 함수 이름, 클래스 이름)는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효범위가 결정된다. 즉, 스코프는 식별자가 유효한 범위를 뜻한다.

MDN에 따르면 스코프란 현재 실행되는 컨텍스트를 말한다. (여기서 컨텍스트는 값과 표현식이 표현되거나 참조될 수 있음을 의미) 스코프는 또한 계층적인 구조를 가지고 있어 하위 스코프는 상위 스코프에 접근할 수 있지만 반대는 불가능하다.


var, let, const 차이

1) 변수 선언 방식

  • Var : 중복 선언 가능
  • let : 중복 선언 불가능, 재할당 가능
  • const : 중복 선언 불가능, 재할당 불가능

2) 스코프 (Scope)

  • Var : 함수 레벨 스코프
  • let,const : 블록 레벨 스코프

3) 호이스팅 (Hoisting)

  • Var : 호이스팅이 발생
console.log(a); // undefined

var a = 5;
console.log(a); // 5
  • let, const : 호이스팅이 발생하지만, 다른 방식으로 작동
console.log(a); // ReferenceError: a is not defined

let a = 5;
console.log(a); // 5
  • let 또는 const 로 변수를 선언하는 경우, 변수 선언만 해둘뿐 초기화는 코드 실행 과정에서 변수 선언문을 만났을 때 수행한다. 때문에 호이스팅이 발생하지만 변수의 선언과 초기화 사이에 일시적으로 변수 값을 참조할 수 없는 구간인 TDZ(Temporal Dead Zone)에 빠졌기 때문에 값을 참조할 수 없다.

함수 선언식과 함수 표현식의 차이

  • 함수 선언식은 함수 전체를 호이스팅해 함수 선언 전에 함수를 사용할 수 있다.

  • 함수 표현식은 별도의 변수에 할당하게 되는데, 변수는 선언부와 할당부를 나누어 호이스팅하며 선언부만 호이스팅한다.

//함수 선언식

foo()  //"foo"

function foo() {
	console.log("foo");
}


foo2()  //foo2 is not defined

//함수 표현식
const foo2 = function() {
	console.log("foo2");
}

3) 함수 표현식의 장점

  • 클로저로 사용 : 함수가 종료돼도, 렉시컬 스코프의 index와 같은 정보를 유지한다.

  • 콜백으로 사용 (다른 함수의 인자로 넘길 수 있음)


클로저(Closure)

클로저는 함수와 함수가 선언된 어휘적 환경의 조합으로 중첩된 함수는 외부 범위(scope)에서 선언한 변수에도 접근할 수 있다.

클로저의 활용

1) 데이터를 보존하는 함수

  • 클로저는 함수 실행이 끝나고 나면 함수 내부의 변수를 사용할 수 없는 일반적인 함수와는 달리, 외부 함수의 실행이 끝나더라도 외부 함수 내 변수가 메모리 상에 저장된다.

2) 캡슐화와 정보 은닉

  • 클로저를 통해 불필요한 전역 변수 사용을 줄이고, 스코프를 이용해 값을 보다 안전하게 다룰 수 있다.

3) 모듈화(모듈 패턴)

  • 모듈화란 함수 재사용성을 극대화하여, 함수 하나를 완전히 독립적인 부품 형태로 분리하는 것을 말한다.
    클로저를 통해 데이터와 메서드를 같이 묶어서 다룰 수 있다.

렉시컬 환경(Lexical Environment)

변수,함수가 어디에서 사용 가능한지 알기 위해 그 변수,함수가 소스코드 내 어디에서 선언되었는지 고려하는 것을 의미한다. 중첩된 함수는 외부 scope에서 선언한 변수에도 접근할 수 있다. 스크립트 전체, 실행중인 함수, 코드블록 등은 자신만의 렉시컬 환경을 갖는다.

렉시컬 환경은 환경레코드, 외부렉시컬 환경으로 구성된다.

환경레코드

렉시컬 환경에서 모든 지역변수를 프로퍼티로 저장하고 있는 객체이다. this, 함수일 경우 매개변수도 포함된다.

외부 렉시컬 환경

현재 렉시컬 환경보다 더 상위의 렉시컬 환경이다. 스크립트는 최상위 렉시컬 환경이며 스크립트 내에 호출된 함수나 코드블록은 외부렉시컬 환경으로 스크립트 렉시컬 환경을 참조한다.


실행 컨텍스트

자바스크립트 코드가 실행되고 연산되는 범위를 나타내는 개념으로 실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다.

자바스크립트는 실행 컨텍스트가 활성화되는 시점에 다음과 같은 현상이 발생한다.

  • 호이스팅이 발생한다(선언된 변수를 위로 끌어올린다)
  • 외부 환경 정보를 구성한다
  • this 값을 설정한다.

자바스크립트 엔진

자바스크립트 엔진은 자바스크립트 코드를 해석하고 실행하는 소프트웨어로 다음과 같은 두 가지 주요 구성 요소로 이루어져 있다.

1) Memory Heap — 메모리 할당이 일어나는 곳

2) Call Stack - 코드 실행에 따라 스택 프레임이 쌓이는 곳

  • 현재 어떤 함수가 동작하고 있는지, 그 함수 내에서 어떤 함수가 동작하는지, 다음에 어떤 함수가 호출되어야 하는지 등을 제어한다.

  • 함수 호출을 기록하고 실행 컨텍스트를 스택에 쌓고(pop) 제거하는 역할을 한다.

  • LIFO : 후입 선출 방식

  • 자바스크립트는 단일 스레드 프로그래밍 언어이므로, 단일 호출 스택이 있다. 따라서 한 번에 하나의 일(Task)만 처리할 수 있다.


자바스크립트에서 일어나는 데이터 형 변환

1) 암시적 변환 (Implicit Conversion)

자바스크립트에서는 연산이나 비교 시에 자동으로 데이터 형을 변환하는 경우가 있는데 이를 암시적 변환(묵시적 변환, implicit conversion)이라고 한다. 예를 들어, 문자열과 숫자를 더할 때 숫자를 문자열로 변환하여 연산을 수행한다.

2) 명시적 변환 (Explicit Conversion)

개발자가 명시적으로 데이터 형을 변환하는 경우로 자바스크립트에서 제공하는 다양한 내장 함수들을 사용하여 수행할 수 있다. 예를 들어, 문자열을 숫자로 변환하고자 할 때는 parseInt()나 parseFloat() 함수를 사용할 수 있다.


자바스크립트가 유동적인 언어인 이유

자바스크립트는 변수의 타입을 런타임에서 결정하는 동적 타입 언어(dynamic typing language)이다.

자바스크립트에서는 변수를 선언할 때 타입을 지정하지 않고, 변수에 저장되는 값에 따라 타입이 동적으로 결정된다. 예를 들어, 숫자형 변수에 문자열 값을 할당하면, 자바스크립트는 해당 변수를 문자열 타입으로 인식하게 된다.

이러한 유동성은 개발자가 데이터 형 변환과 같은 작업을 수동으로 처리할 필요가 없으므로, 코드 작성 시간이 단축되고, 코드의 유연성이 증가 하지만, 동적 타입 언어는 컴파일 시간에 타입 오류를 검출할 수 없다는 단점이 있다.


프로토타입

객체 지향 프로그래밍에서 상속을 구현하기 위한 중요한 개념으로 프로토타입 객체는 일반 객체와 마찬가지로 속성과 메소드를 가질 수 있다.

객체는 프로토타입 객체를 가리키는 숨겨진 링크인 [[Prototype]]을 가지는데 이 링크를 통해 객체가 가지고 있는 속성과 메소드를 찾아 사용할 수 있다. 만약 객체에서 속성이나 메소드를 찾을 수 없다면, 자바스크립트 엔진은 해당 객체의 프로토타입 객체에서 찾는다. 이러한 과정을 프로토타입 체인(prototype chain)이라고 한다.

프로토타입 체인을 이용하여 객체가 가지고 있는 속성과 메소드를 상속받을 수 있으며, 이를 통해 객체 간의 관계를 유지하면서 재사용성을 높일 수 있다.


깊은 복사와 얕은 복사

자바스크립트의 참조 타입(객체나 배열)의 이터를 복사하는 방법

1) 얕은 복사

  • 타입 데이터가 저장한 '메모리 주소 값'을 복사한 것을 의미한다. 이 경우 새로운 변수를 변경하면 원본 객체나 배열도 변경된다.
  • 안에 객체가 있을 경우에 한 개의 객체라도 원본 객체를 참조한다면 얕은 복사라고 볼 수 있다.

2) 깊은 복사

  • 새로운 메모리 공간을 확보해 완전히 복사하는 것을 의미한다. 따라서 새로운 변수를 변경해도 원본 객체나 배열은 변경되지 않는다. 하지만 깊은 복사는 원본 객체나 배열이 복잡하고 중첩된 객체나 배열을 가지고 있을 경우, 복사하는 데 시간과 메모리를 많이 소모할 수 있다.

3) Object.assign()

Object.assign()을 이용하면 객체 자체는 깊은 복사가 수행되지만, 2차원 이상의 객체는 얕은 복사가 수행된다. 아래 예시에서 객체는 서로 다른 주소를 참조하고 있어 깊은 복사가 이루어졌지만 내부의 객체는 같은 주소를 참조하고 있다.

let origin = {
    a: 1,
    b: { c: 2 }
};

let copy = Object.assign({}, origin);
copy.b.c = 3

console.log(origin === copy) // false
console.log(origin.b.c === copy.b.c) // true

4) 스프레드 연산자

스프레드 연산자 역시 배열 안에 객체로 중첩된 상태(2차원 이상의 객체)는 전개 연산자로 중첩된 곳까지 깊은 복사가 적용이 안된다.

const exampleData = [
  {id: 0, name: '둘리', age: 8},
  {id: 1, name: '도우너', age: 8},
  {id: 2, name: '또치', age: 20},
];
const copyData = [...example];

copyData[2].name = '마이콜';

console.log(exampleData[2].name);     // '마이콜'
console.log(copyData[2].name);        // '마이콜'

5) 깊은 복사 수행 방법

  1. JSON.parse()와 JSON.stringify()를 사용하는 방법 (성능이 안좋음)
const exampleData = [
  {id: 0, name: '둘리', age: 8},
  {id: 1, name: '도우너', age: 8},
  {id: 2, name: '또치', age: 20},
];
const copyData = JSON.parse(JSON.stringify(exampleData));

copyData[2].name = '마이콜';

console.log(exampleData[2].name);
// '또치'
console.log(copyData[2].name);
// '마이콜'
  1. Lodash 라이브러리 사용 : JavaScript 라이브러리로 손쉽게 깊은 복사를 할 수 있다.
const exampleData = [
  {id: 0, name: '둘리', age: 8},
  {id: 1, name: '도우너', age: 8},
  {id: 2, name: '또치', age: 20},
];
const copyData = _.cloneDeep(exampleData);

copyData[2].name = '마이콜';

console.log(exampleData[2].name);
// '또치'
console.log(copyData[2].name);
// '마이콜'

불변성을 유지하려면 어떻게 해야할까?

변하지 않는 값을 처리하는 방법은 간단하게는 const 선언자로 상수 선언을 하는 방법이 있다. 개별 값은 이렇게 해도 되지만, 중간에 변경되면 안 되는 중요 설정 값을 담은 객체나, 원본 데이터를 담고 있는 객체 같은 경우, const 선언자로는 불변성을 유지할 수 없다.

그래서 자바스크립트는 객체에 불변성을 유지할 수 있도록 별도의 객체 메서드를 지원하고 있으며, preventExtensions < seal < freeze 순으로 불변성이 강하게 유지된다.

1) freeze

freeze로 불변 속성을 부여한 객체는 속성의 값을 수정할 수도 없고, 속성을 추가하거나 삭제할 수도 없다.

const apiconfig = {
    name: 'API서버',
    server: {
        ip: '192.168.0.3',
        port: '8081',
        userid: 'apost',
        password: 'dpdlvldkdl',
        authkey: 'QK#ETRA1*AA',
    }
}

Object.freeze(apiconfig)

apiconfig.name = 'API테스트서버' // freeze.js:14 Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'

하지만 freeze()는 strict 모드에서 정상적으로 동작하기 때문에 코드 상단에 "use strict"를 선언해야 freeze()가 동작한다. 또한 하위에 중첩된 객체가 있으면 하위 객체의 속성은 freeze가 적용되지 않는다.

따라서 앞서의 객체는 다음과 같이 수정해야 온전히 freeze()가 적용된다.

"use strict";
const apiconfig = {
    name: 'API서버',
    server: {
        ip: '192.168.0.3',
        port: '8081',
        userid: 'apost',
        password: 'dpdlvldkdl',
        authkey: 'QK#ETRA1*AA',
    }
}

Object.freeze(apiconfig)
Object.freeze(apiconfig.server)

2) seal

freeze()와 유사한 기능을 하며, 중첩 객체에 대해서는 하위 객체에도 seal()을 적용해야 하는 점도 동일하나 기존에 있는 속성의 값은 자유롭게 변경할 수 있다.

3) preventExtensions

seal()과 유하하나 속성 삭제가 가능하다.


profile
평생 공부하기

0개의 댓글

관련 채용 정보