프론트 기술 질문 8. JavaScript 핵심 개념 (1)

기운찬곰·2023년 9월 10일
0

프론트 기술 질문

목록 보기
8/10
post-thumbnail

Overview

프론트엔드 개발자라면 당연히 알아야 될 내용이 있습니다. 바로 JavaScript 입니다. 오늘은 자바스크립트 필수 질문에 대해 알아보고 정리해보는 시간을 갖도록 하겠습니다. 양이 많으니 10개씩 끊어서 작성하도록 하지요.


var, let, const 의 차이점을 설명해 주세요.

뭐... 여러가지가 있겠지만 크게 3가지로 볼 수 있지 않을까요? (var 와 let, const 비교 시. let const는 ES6 문법)

  1. 중복 선언 가능 여부 : var는 중복 선언이 가능합니다. 반면에 let, const는 중복선언이 안됩니다.
  2. 스코프 차이 : var는 함수 레벨 스코프입니다. 오직 함수의 코드 블록만을 지역 스코프로 인정합니다. 반면에 let, const는 블록 레벨 스코프입니다. 함수의 코드 블록 뿐만 아니라 if, for, while, try~catch 문 등 모든 코드 블록을 지역 스코프로 인정합니다.
  3. 변수 호이스팅 : var 키워드로 변수를 선언하면 변수 호이스팅에 의해 변수 선언문이 스코프의 선두로 끌어 올려진 것처럼 동작합니다. 즉, 변수 호이스팅에 의해 var 키워드로 선언한 변수는 변수 선언문 이전에 참조할 수 있습니다. 반면에 let, const는 변수 호이스팅이 발생하지 않는 것처럼 동작합니다. 즉, 변수 호이스팅은 발생합니다. 다만 변수 선언 단계와 초기화 단계가 서로 분리되어 진행됩니다. 따라서 스코프의 시작 지점부터 초기화 단계 시작 시점(변수 선언문)까지 변수를 참조할 수 없습니다. 이 구간을 일시적 사각지대(Temporal Dead Zone, TDZ)이라고 부릅니다.

let과 const의 차이는 단순합니다. let은 변수이고, const는 상수입니다. 다시말해 let은 재할당이 가능하고, const는 불가능합니다. 그 외에는 const는 변수 선언과 초기화를 동시에 해줘야 한다는 점이 있습니다. let은 변수 선언만 해줘도 됩니다.


호이스팅과 발생하는 이유에 대해 설명해주세요.

(변수/함수) 선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 자바스크립트 고유의 특징을 호이스팅이라고 합니다.

자바스크립트 엔진은 소스코드를 한 줄씩 순차적으로 실행하기에 앞서 소스코드의 평가 과정을 거치면서 소스코드를 실행하기 위한 준비를 합니다(=실행 컨텍스트 생성). 이 때 변수 선언을 포함한 모든 선언문(변수 선언문, 함수 선언문 등)을 소스코드에서 찾아내 먼저 실행합니다. 그 이후에 선언문을 제외하고 소스코드를 한 줄씩 순차적으로 실행합니다.

함수 호이스팅과 변수 호이스팅에는 미묘한 차이가 있습니다. 둘 다 런타임 이전에 자바스크립트 엔진에 의해 먼저 실행되어 식별자를 생성한다는 점에서 동일합니다. 하지만 var 키워드로 선언된 변수는 undefined로 초기화되고, 함수 선언문을 통해 암묵적으로 생성된 식별자는 함수 객체로 초기화됩니다.


함수 선언문과 함수 표현식의 차이에 대해 설명해주세요.

함수 선언문과 함수 표현식은 형태의 차이와 호이스팅의 차이가 있습니다.

함수 선언문은 function add(x, y) {}의 형태로 쓰여지며, 완료시 undefined가 출력됩니다. 이는 "표현식이 아닌 문"이 라는 뜻입니다. 함수 표현식은 const add = function(x,y) {}의 형태로 쓸 수 있습니다. 근데 이상하지 않나요? "표현식이 아닌 문"이라고 한다면 변수에 할당할 수 없기 때문에 함수 표현식은 불가능해보입니다. 여기에는 트릭이 있습니다.

사실 자바스크립트 엔진은 함수 선언문을 해석해 함수 객체를 생성하는데, 이 때 함수 이름은 함수 몸체 내부에서만 유효한 식별자이므로 함수 이름과는 별도로 생성된 함수 객체를 가리키는 식별자가 필요합니다. 따라서 자바스크립트 엔진은 생성된 함수를 호출하기 위해 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고, 거기에 함수 객체를 할당합니다. 결국 아래 2개는 사실 같은 의미가 됩니다.

function add(x, y) {
	return x + y
}

var add = function add(x, y) { // 결국 이런 녀석이 된다. 
	return x + y;
}

정리해보면 함수는 함수 이름으로 호출하는 것이 아니라 함수 객체를 가리키는 식별자로 호출하는 것입니다. (너무 디테일하긴 하네..)

이번에는 호이스팅의 차이에 대해 알아보겠습니다. 함수 선언문으로 정의한 함수는 함수 선언문 이전에 호출할 수 있습니다. 그러나 함수 표현식으로 정의한 함수는 함수 표현식 이전에 호출할 수 없습니다. 이는 함수 선언문으로 정의한 함수와 함수 표현식으로 정의한 함수의 생성 시점이 다르기 때문입니다.

함수 선언문으로 함수를 정의하면 런타임 이전에 함수 객체가 먼저 생성됩니다. 그리고 자바스크립트 엔진은 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 생성된 함수 객체를 할당합니다. 그렇기 때문에 함수 선언문은 함수 선언문 이전에 호출할 수 있는 것입니다. 반면, 함수 표현식으로 함수를 정의하면 함수 호이스팅이 발생하는 것이 아닌 변수 호이스팅이 발생합니다. 변수 할당문으로 인식하기 때문에 그렇습니다. 그래서 undefined로 평가됩니다.


콜백 함수에 대해 설명해주세요.

함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수를 콜백 함수라고 하며, 매개변수를 통해 함수의 외부에서 콜백함수를 전달받은 함수를 고차 함수(Higher-Order Function, HOF)라고 합니다. 고차함수는 콜백 함수를 자신의 일부분으로 합성하여 자유롭게 교체할 수 있다는 장점이 있습니다.

아래는 이해를 돕기 위한 예시입니다.

// 외부에서 전달받은 f를 n만큼 반복 호출한다. - 고차함수(HOC)
function repeat(n, f) {
    for (var i = 0; i < n; i++) {
        f(i);
    }
}

// 출력하는 일을 하는 함수 - 콜백함수
var log = function (i) {
    console.log(i)
}

// 홀수 일때만 출력을 하는 함수 - 콜백함수
var logOdds = function (i) {
    if (i % 2) console.log(i)
}

repeat(5, log)
repeat(5, logOdds)

고차 함수는 매개변수를 통해 전달받은 콜백 함수의 호출 시점을 결정해 호출합니다. 다시 말해, 콜백 함수는 고차 함수에 의해 호출(제어권 역전)되며, 이 때 고차함수는 필요에 따라 콜백 함수에 인수를 전달 할 수 있습니다.

콜백 함수는 함수형 프로그래밍 패러다임 뿐만 아니라 비동기 처리(이벤트 처리, Ajax, 타이머 함수 등)에 활용되는 중요한 패턴입니다.


동기와 비동기에 대해 설명해주세요.

현재 실행 중인 태스크가 종료할 때까지 다음에 실행될 태스크가 대기하는 방식을 동기 처리라고 합니다. 동기 처리 방식은 태스크를 순서대로 처리하기 때문에 실행 순서가 보장된다는 장점이 있지만, 앞선 태스크가 종료할 때까지 블로킹되는 단점이 있습니다. 기본적으로 자바스크립트 엔진은 한 번에 하나의 태스크만 실행할 수 있는 싱글 스레드 방식으로 동작합니다. 따라서 처리에 시간이 걸리는 태스크를 실행하는 경우 블로킹이 발생합니다.

반면에 현재 실행 중인 태스크가 종료되지 않은 상태라 해도 다음 태스크가 곧바로 실행되는 방식을 비동기 처리라고 합니다. 비동기 방식은 블로킹이 발생하지 않는다는 장점이 있지만, 태스크의 실행 순서를 보장하지 않는다는 단점이 있습니다. 타이머 함수인 setTimeout과 setInterval, HTTP 요청, 이벤트 핸들러는 비동기 처리 방식으로 동작합니다. 비동기 처리는 이벤트 루프와 태스크 큐와 깊은 관계가 있습니다.


이벤트 루프와 태스크 큐에 대해 설명해주세요.

자바스크립트는 싱글 스레드로 동작한다고 했습니다. 하지만 브라우저가 동작하는 것을 살펴보면 그렇지 않아보입니다. 이처럼 자바스크립트의 동시성을 지원하는 것이 바로 이벤트 루프입니다. 이벤트 루프는 브라우저에 내장되어 있는 기능 중 하나입니다.

구글의 V8 자바스크립트 엔진을 비롯한 대부분의 자바스크립트 엔진은 크게 2개의 영역으로 구분할 수 있습니다.

  • 콜 스택 : 실행 컨텍스트 스택이 바로 콜 스택입니다.
  • 힙 : 객체가 저장되는 메모리 공간입니다. 콜 스택의 요소인 실행 컨텍스트는 힙에 저장된 객체를 참조합니다.

비동기 처리에서 소스코드의 평가와 실행을 제외한 모든 처리는 자바스크립트 엔진을 구동하는 환경인 브라우저(Web API) 또는 Node.js가 담당합니다. 예를 들어, 비동기 방식으로 동작하는 setTimeout의 콜백 함수의 평가와 실행은 자바스크립트 엔진이 담당하지만 호출 스케줄링을 위한 타이머 설정과 콜백 함수의 등록은 브라우저(Web API) 또는 Node.js가 담당합니다.

이를 위해 브라우저 환경은 태스크 큐와 이벤트 루프를 제공합니다.

  • 태스크 큐 : setTimeout이나 setInterval과 같은 비동기 함수의 콜백 함수 또는 이벤트 핸들러가 일시적으로 보관되는 영역입니다. 태스크 큐와는 별도로 프로미스의 후속 처리 메서드의 콜백 함수가 일시적으로 보관되는 마이크로태스크 큐도 존재합니다.
  • 이벤트 루프 : 콜 스택에 현재 실행 중인 실행 컨텍스트가 있는지, 태스크 큐에 대기 중인 함수(콜백 함수, 이벤트 핸들러 등)가 있는지 반복해서 확인합니다. 만약 콜 스택이 비어 있고 태스크 큐에 대기 중인 함수가 있다면 이벤트 루프는 순차적으로 태스크 큐에 대기 중인 함수를 콜 스택으로 이동시킵니다. 이때 콜 스택으로 이동한 함수는 실행됩니다. 즉, 태스크 큐에 일시 보관된 함수들은 비동기 처리 방식으로 동작하게 됩니다.

이처럼 비동기 함수인 setTimeout의 콜백 함수는 태스크 큐에 푸시되어 대기하다가 콜 스택이 비게 되면, 다시 말해 전역 코드 및 명시적으로 호출된 함수가 모두 종료하면 비로소 콜 스택에 푸시되어 실행됩니다.

다음은 setTimeout이 타이머 만료 직후 즉시 실행되지 않는 예제입니다. (이해 되시죠?)

const seconds = new Date().getTime() / 1000;

setTimeout(function () {
  // "2"를 출력, 즉 500밀리초가 지난 후 즉시 실행된 것이 아니라는 것
  console.log(`${new Date().getTime() / 1000 - seconds}초 후 실행됩니다.`);
}, 500);

while (true) {
  if (new Date().getTime() / 1000 - seconds >= 2) {
    console.log("좋아요, 2초간 반복했습니다.");
    break;
  }
}

💡 자바스크립트는 싱글 스레드 방식으로 동작한다는 것은 브라우저가 아닌 브라우저에 내장된 자바스크립트 엔진이라는 것에 주의하기 바랍니다. 만약 모든 자바스크립트 코드가 자바스크립트 엔진에서 싱글 스레드 방식으로 동작한다면 자바스크립트는 비동기로 동작할 수 없습니다. 즉, 자바스크립트 엔진은 싱글 스레드로 동작하지만 브라우저는 멀티 스레드로 동작합니다.

태스크 큐와 마이크로 태스트 큐

마이크로 태스크 큐는 일반 태스크 큐(Macro Task Queue)보다 우선 순위가 높습니다. 즉, 이벤트 루프는 콜 스택이 비면 먼저 마이크로 태스크 큐에 대기하고 있는 함수를 가져와 실행하며, 이후 마이크로 태스크 큐가 비면 그제서야 태스크 큐에서 대기하고 있는 함수를 가져와 실행합니다.

  • Macro Task Queue : 타이머 함수(setTimeout, setInterval, setImmediate), DOM 이벤트, Ajax 호출 등
  • Micro Task Queue : process.nextTick, Promises 등

다음 코드를 보고 결과를 예상해봅시다.

console.log('script start'); // A

setTimeout(function () { // B
  console.log('setTimeout');
}, 0);

Promise.resolve() 
  .then(function () { // C
    console.log('promise1');
  })
  .then(function () { // D
    console.log('promise2');
  });

console.log('script end'); // E

결과는 다음과 같습니다. 말 안해도 아시겠죠?

script start
script end
promise1
promise2
setTimeout

콜백 헬이 발생하는 이유와 문제점에 대해 알려주세요.

예를 들어 XMLHttpRequest로 GET 요청을 위한 함수를 작성해봅시다.

// GET 요청을 위한 비동기 함수
const = get = (url, callback) => {
	const xhr = new XMLHttpRequest();
	xhr.open('GET', url);
	xhr.send();

	xhr.onload = () => {
		if (xhr.status === 200) {
			callback(JSON.parse(xhr.response));
		} else {
			console.error(`${xhr.status} ${xhr.statusText}`)
		}
	}
}

get은 비동기 함수입니다. 비동기 함수를 호출하면 함수 내부의 비동기로 동작하는 코드가 완료되지 않았다 해도 기다리지 않고 즉시 종료됩니다. 즉, 비동기 함수가 종료된 이후에 완료됩니다. 따라서 비동기 함수 내부의 비동기로 동작하는 코드에서 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당하면 기대한 대로 동작하지 않습니다.

이처럼 비동기 함수는 비동기 처리 결과를 외부에 반환할 수도 없고, 상위 스코프의 변수에 할당할 수도 없습니다. 따라서 비동기 함수의 처리 결과(서버의 응답 등)에 대한 후속 처리는 비동기 함수 내부에서 수행해야 합니다. 이 때 비동기 함수를 범용적으로 사용하기 위해 비동기 함수 처리 결과에 대한 후속 처리를 수행하는 콜백 함수를 전달하는 것이 일반적입니다.

// id가 1인 post의 userId를 취득
get(`/${url}/posts/1`, ({ userId }) => {
	console.log(userId); // 1
	// post의 userId를 이용해 user 정보 취득
	get(`/${url}/users/${userId}`, userInfo => {
		console.log(userInfo); // {id: 1, name: "~~", username: "~", ...}
	});
});

결국 콜백 함수를 통해 비동기 처리 결과에 대한 후속 처리를 수행하는 비동기 함수가 비동기 처리 결과를 가지고 또다시 비동기 함수를 호출해야 한다면 콜백 함수 호출이 중첩되어 복잡도가 높아지는 현상이 발생하는데, 이를 콜백 헬이라 합니다.

콜백 헬은 가독성을 나쁘게 하며 실수를 유발하는 원인이 됩니다. (읽는 순서와 실행 순서가 다르기 때문에)

또한, 콜백 패턴의 가장 큰 문제는 에러 처리가 곤란하다는 것입니다. 아래 예시를 보겠습니다.

try {
	setTimeout(() => { throw new Error('Error'); }, 1000);
} catch (e) {
	// 에러를 캐치 못한다
	console.error(e);
}

setTimeout 함수의 콜백 함수가 실행될 때 setTimeout 함수는 이미 콜 스택에서 제거된 상태입니다. 이것은 setTimeout의 콜백 함수를 호출한 것이 setTimeout 함수가 아니라는 것을 의미합니다. 에러는 호출자(caller) 방향으로 전파됩니다. 하지만 앞서 설명한대로 setTimeout의 콜백 함수를 호출한 것이 setTimeout 함수가 아니므로 콜백 함수가 발생시킨 에러는 catch 블록에서 캐치되지 않습니다.


Promise에 대해 설명해주세요.

ES6에서 도입된 Promise는 ECMAScript 사양에 정의된 표준 빌트인 객체입니다. 앞서 살펴본 get을 프로미스로 다시 구현해보면 다음과 같습니다.

const = promiseGet = (url) => {
	return new Promise((resolve, reject) => {
		const xhr = new XMLHttpRequest();
		xhr.open('GET', url);
		xhr.send();
	
		xhr.onload = () => {
			if (xhr.status === 200) {
				resolve(JSON.parse(xhr.response));
			} else {
				reject(new Error(xhr.status));
			}
		}
	});
};

// promiseGet 함수는 프로미스를 반환한다.
promiseGet('https://jsonplaceholder.typicode.com/posts/1')
	.then(res => console.log(res))
	.catch(res => console.error(res))
	.finally(() => console.log('Bye!));

프로미스는 현재 비동기 처리가 어떻게 진행되고 있는지 상태 정보를 갖습니다.

  • pending : 비동기 처리가 아직 수행되지 않은 상태
  • fulfilled : 비동기 처리가 수행(완료)된 상태 - resolve 함수 호출
  • rejected : 비동기 처리가 수행(실패)된 상태 - reject 함수 호출

프로미스는 비동기 처리 상태와 더불어 비동기 처리 결과도 상태로 갖습니다. 즉, 프로미스는 비동기 처리 상태와 처리 결과를 관리하는 객체입니다. 프로미스의 비동기 처리 상태가 변화하면 이에 따른 후속 처리를 하게 됩니다. 이를 위해 프로미스는 후속 메서드 then, catch, finally를 제공합니다.

  • then 메서드는 두개의 콜백 함수를 인수로 전달받는다.
  • catch 메서드의 콜백 함수는 프로미스가 rejected 인 상태만 호출된다.
  • finally 메서드의 콜백 함수는 프로미스의 성공 또는 실패와 관련없이 무조건 한 번 호출된다.

Async, Await에 대해 설명해주세요.

ES8부터는 간단하고 가독성 좋게 비동기 처리를 동기 처리처럼 동작하도록 구현할 수 있는 async/await이 도입되었습니다. async/await은 promise를 기반으로 동작합니다. promise의 then, catch, finally 후속 처리 메서드에 콜백 함수를 전달해 처리할 필요 없이 마치 동기 처리처럼 promise가 처리 결과를 반환하도록 구현할 수 있습니다.

  • async 함수는 언제나 프로미스를 반환한다.
  • await 키워드는 반드시 async 함수 내부에서 사용해야 한다.
  • await 키워드는 프로미스가 settled 상태(비동기 처리가 수행된 상태)가 될때까지 대기하다가 settled 상태가 되면 프로미스가 resolve한 처리 결과를 반환한다. await 키워드는 반드시 프로미스 앞에서 사용해야 한다.
  • async 함수 내에서 catch 문을 사용해 에러를 처리할 수 있다. 만약 처리를 하지 않으면 async 함수는 발생한 에러를 reject 하는 프로미스를 반환한다.
const getGithubUserName = async id => {
	try {
		const res = await fetch(`https://api.github.com/users/${id}`);
		const { name } = await res.json();
		console.log(name);
	} catch (err) {
		console.log(err);
	}
}

getGithubUserName('ungmo2');

AJAX에 대해 설명해주세요.

Ajax(Asynchronous JavaScript and XML)란 자바스크립트를 사용하여 브라우저가 서버에게 비동기 방식으로 데이터를 요청하고, 서버가 응답한 데이터를 수신하여 웹페이지를 동적으로 갱신하는 프로그래밍 방식을 말합니다. Ajax는 브라우저에서 제공하는 Web API인 XMLHttpRequest 객체를 기반으로 동작합니다.

이전의 웹페이지는 html 태그로 시작해서 html 태그로 끝나는 완전한 HTML을 서버로부터 전송받아 웹페이지 전체를 처음부터 다시 렌더링하는 방식으로 동작했습니다. Ajax의 등장은 이전의 전통적인 패러다임을 획기적으로 전환했습니다다. 즉, 서버로부터 웹페이지의 변경에 필요한 데이터만 비동기 방식으로 전송받아 웹페이지를 변경할 필요가 없는 부분을 다시 렌더링하지 않고, 변경할 필요가 있는 부분만 한정적으로 렌더링하는 방식이 가능해진 것입니다.


마치면서

면접보기 전에 꼭 숙지하도록 바랍니다. (제 자신에게 하는 얘기...)

저는 외우는 것보다 이해하는 것이 중요하다고 생각해서 단순 답변 스크립트가 아니라 좀 더 상세한 설명과 예시까지 작성했습니다. 사실 저는 외우는 거 잘못하거든요. 😂. 그때그때 생각나는대로 답변하는게 가장 좋다고 생각합니다.


참고 자료

profile
velog ckstn0777 부계정 블로그 입니다. 프론트 개발 이외의 공부 내용을 기록합니다. 취업준비 공부 내용 정리도 합니다.

0개의 댓글

관련 채용 정보