해당 아티클은 아래 미디엄 블로그글의 번역입니다.
True Stroy — JavaScript 면접을 통과하기 위해서, JavaScript를 그렇게까지 잘 알 필요는 없습니다.
다른 문장의 형태를 띄고 있겠지만 면접관들은 동일한 주제로 테스트를 하기 때문에, 면접에서 나오는 질문들은 거의 대부분 비슷합니다.
하지만, 뒤에 나오는 질문들처럼 이러한 질문에 대한 올바른 답변 통계는 정답률이 상당히 낮습니다.
어떻게 이런 상황을 바꿀 수 있을까요? 간단합니다 - 이러한 주제를 최대한 많이 연습하고, 무엇보다도 그 과정과 결과를 이해하는 것 입니다.
아래에서 주제별로 정리한 전형적인 면접 질문과 텔레그램 채널의 통해 더 많은 통찰력은 얻도록 노력해봅시다.
01 이벤트 루프.
이벤트 루프를 언급하지 않는 JavaScript 면접을 상상하기 어려울 정도로 이 주제는 중요합니다.
헛된 노력이 아니며 이 주제는 정말로 근본적이고, 매일 React, Angular, Vue 개발자들이 사용하는 주제입니다.
퀴즈 №1. 올바른 답변률 18%은 입니다.
setTimeout(() => console.log(1), 0);
console.log(2);
new Promise(res => {
console.log(3)
res();
}).then(() => console.log(4));
console.log(5);
해설.
이 예시에서는 setTimeout, Promise 및 일부 동기 코드를 볼 수 있습니다.
이 퀴즈에 정확한 답변을 한 개발자의 생각의 흐름은 다음과 같을 것입니다.
- setTimeout에 전달한 함수는 지연 시간이 0이지만, 이 함수는 동기적으로 아니면 비동기적으로 호출될까요?
- setTimeout 함수가 지연 시간이 0이더라도, 콜백 함수는 비동기적으로 호출됩니다. 엔진은 콜백 함수를 콜백 큐 (매크로태스크 큐)에 넣고, 콜 스택이 비어있을 때 콜 스택으로 옮길 것입니다. 따라서 숫자 1은 건너뛰고 콘솔에 먼저 숫자 2가 표시됩니다.
- Promise 생성자에 전달한 함수는 동기적으로 아니면 비동기적으로 호출될까요?
- Promise 생성자가 가져온 함수는 동기적으로 실행됩니다. 따라서 콘솔에 다음으로 표시되는 숫자는 3입니다.
- 프로미스의 then 핸들러에 전달한 함수는 지연 시간이 0이더라도 동기적으로 아니면 비동기적으로 호출될까요?
- then 메서드의 콜백은 비동기적으로 실행됩니다. 프로미스가 지연 없이 해결되더라도, 차이점은 엔진이 프로미스 콜백을 다른 큐인 작업 큐 (마이크로태스크 큐)에 넣어서 실행 차례를 기다린다는 것입니다. 따라서 다음으로 콘솔에 들어오는 숫자는 5입니다. 마이크로태스크 큐와 매크로태스크 큐, 다시 말해 프로미스와 setTimeout 중 어떤 것이 우선순위가 높을까요? 마이크로태스크(프로미스)가 매크로태스크(setTimeout)보다 우선순위가 높습니다. 따라서 콘솔에서 다음으로 표시되는 숫자는 4이고 마지막으로 1이 됩니다.
응답을 분석하면, 응답자 중 대다수가 Promise 생성자에 전달한 실행자 함수가 비동기적으로 호출된다는 가정이 틀렸음을 알 수 있습니다.
02 문맥 (Context).
문맥에 관한 질문은 경험이 풍부한 개발자들조차도 혼란스러울 수 있습니다.
예를 들어, 이 간단한 작업을 해결한 개발자는 전체 중 29%에 불과합니다.
'use strict';
function foo() {
console.log(this);
}
function callFoo(fn) {
fn();
}
let obj = { foo };
callFoo(obj.foo);
해설.
this의 값은 함수가 호출될 때 설정됩니다.
예를 들어, obj.foo 함수는 다른 callFoo 함수의 인수로 전달되며, 이 함수는 컨텍스트 없이 호출합니다.
일반 모드에서 실행 컨텍스트가 없고 코드가 브라우저 환경에서 실행되는 경우 this는 window 객체를 참조하며, 엄격 모드에서는 undefined입니다.
그렇기 때문에 올바른 답은 undefined입니다.
퀴즈 №2. 올바른 답변률 28%으고, 또 다른 흔한 면접 질문은 화살표 함수 내부의 this 값입니다.
'use strict';
var x = 5;
var y = 5;
function Operations(op1 = x, op2 = y) {
this.x = op1;
this.y = op2;
};
Operations.prototype.sum = () => this.x + this.y;
const op = new Operations(10, 20);
console.log(op.sum());
해설.
화살표 함수는 자체적인 this를 갖지 않습니다.
대신 화살표 함수의 본문 내부의 this는 해당 화살표 함수가 정의된 스코프의 this 값을 가리킵니다.
해당 함수는 전역 스코프에서 정의되었고, 전역 스코프에서의 this는 전역 객체를 가리킵니다 (엄격 모드에서도). 따라서 정답은 10입니다.
03 화살표 함수.
경험 있는 면접관(a.k.a 악마)은 화살표 함수에 관해 다음과 같은 질문도 할 수 있습니다.
function Operations(coef) {
return {
sum: (...args) => arguments[0] + coef
}
}
const ops = Operations(0.1);
console.log(ops.sum(1, 2, 3));
해설.
화살표 함수는 자체적인 arguments 객체를 갖지 않습니다.
대신, arguments는 감싸고 있는 스코프의 arguments를 참조하므로, arguments[0]은 coef 인자를 가리키면서 이 퀴즈의 결과는 0.2가 됩니다.
화살표 함수에 관한 또 다른 질문은 다음과 같습니다.
const Num = () => {
this.getNum = () => 10;
}
Num.prototype.getNum = () => 20;
const num = new Num();
console.log(num.getNum());
해설.
화살표 함수는 생성자로 사용할 수 없으며, new로 호출할 경우 오류가 발생합니다.
또한 화살표 함수는 prototype 속성을 가지고 있지 않습니다: TypeError: Cannot set properties of undefined (setting ‘getNum’)
이와 같은 질문은 드물지만, 이에 대비할 준비를 해야 합니다. 화살표 함수에 대한 더 많은 정보는 MDN에서 확인할 수 있습니다.
05 변수의 범위 (Variable scope).
이 주제는 면접에서 뿐만 아니라 변수의 범위를 잘 이해한다면 코드 디버깅하는 데 많은 시간을 절약할 수 있기 때문에 탐구해볼 가치가 있습니다.
일반적인 몇 가지 예제를 살펴보겠습니다.
'use strict';
console.log(foo());
let bar = 'bar';
function foo() {
return bar;
}
bar = 'baz';
해설.
let / const 변수의 정의 전 스코프 내의 위치를 임시 사각 지대라고 부릅니다.
정의되기 전에 let / const 변수에 접근하려고 하면 참조 오류가 발생합니다. 이러한 동작은 const 변수 때문에 선택되었습니다.
변수가 정의되기 전에 var 변수에 접근하면 undefined가 반환됩니다.
하지만 const 변수에 대해서는 동일하게 작동할 수 없습니다. 그렇게 되면 상수가 아니게 되기 때문입니다.
let 변수에 대한 동작도 유사한 방식으로 처리되어서 두 유형의 변수 간에 쉽게 전환할 수 있도록 설계되었습니다.
우리 예시로 돌아가서 함수 호출이 bar 변수의 정의보다 위에 위치하기 때문에, 해당 변수는 임시 사각 지대에 있습니다.
그래서 코드는 다음과 같은 오류를 발생시킵니다: ReferenceError: Cannot access ‘bar’ before initialization
let func = function foo() {
return 'hello';
}
console.log(typeof foo);
해설.
이름 있는 함수 표현식에서 이름은 함수 본문 내에서만 지역적으로 사용되며 외부에서는 사용할 수 없습니다.
따라서 foo는 전역 스코프에 존재하지 않습니다.
typeof 연산자는 정의되지 않은 변수에 대해 undefined를 반환합니다.
다음 예시는 실제로 사용하기에는 권장되지 않지만, 적어도 이 코드가 어떻게 작동하는지 알아야 합니다.
function foo(bar, getBar = () => bar) {
var bar = 10;
console.log(getBar());
}
foo(5);
해설.
복잡한 매개변수(구조 분해, 기본값)를 가진 함수의 경우, 매개변수 목록은 고유한 스코프 내에서 둘러싸입니다.
따라서 함수 본문에서 bar 변수를 생성해도 매개변수 목록에 있는 동일한 이름의 변수에 영향을 미치지 않으며,
getBar() 함수는 클로저를 통해 매개변수에서 bar를 가져옵니다.
일반적으로 ES6가 7년이 넘게 발표되었음에도 불구하고, 그 기능들은 개발자들에게 여전히 잘 이해되지 않는 것으로 보입니다.
물론 모두가 이 버전의 기능의 구문을 알고 있지만, 그 중에서 심층적으로 이해하는 사람은 소수뿐입니다.
ES6 기능에 관한 다음 기사를 놓치지 마세요.
06 ES6 모듈.
면접관이 어떤 이유로든 지원자를 좋아하지 않는다면, 모듈에 대한 질문은 누구든 떨어뜨리는 데 도움이 될 것입니다.
이 주제의 가장 쉬운 작업 중 하나를 선택했지만 보통 ES6 모듈은 훨씬 더 복잡합니다.
index.js
console.log('index.js');
import { sum } from './helper.js';
console.log(sum(1, 2));
helper.js
console.log('helper.js');
export const sum = (x, y) => x + y;
해설.
Imports 또한 호이스팅의 대상이 됩니다.
호이스팅은 JavaScript에서 변수와 함수 선언이 코드가 실행되기 전에 해당 스코프의 맨 위로 이동되는 메커니즘입니다.
모든 종속성은 코드가 실행되기 전에 로드됩니다.
따라서 정답은: helper.js index.js 3입니다.
07 호이스팅 (Hoisting).
다른 인기 있는 면접 주제로는 호이스팅이 있습니다.
선택한 퀴즈가 실제와 무관하더라도, 호이스팅 메커니즘을 완벽하게 설명합니다.
이 코드가 어떻게 작동하는지 이해한다면, 거의 모든 다른 호이스팅 질문에 문제가 없을 것입니다.
'use strict';
var num = 8;
function num() {
return 10;
}
console.log(num);
해설.
함수와 변수 선언은 해당 스코프의 맨 위에 배치되며, 변수의 초기화는 스크립트 실행 시간에 발생합니다.
동일한 이름을 가진 변수의 반복 선언은 건너뛰게 됩니다.
함수는 항상 먼저 호이스팅됩니다.
코드에서 함수와 동일한 이름을 가진 변수의 선언이 어떤 순서로든 나타나더라도 함수가 우선권을 가지며, 이는 함수가 더 위로 끌어올려지기 때문입니다.
예시 1.
function num() {}
var num;
console.log(typeof num); // function
예시 2.
var num;
function num() {}
console.log(typeof num); // function
변수는 항상 맨 마지막에 초기화됩니다.
var num = 8;
function num() {}
다음과 같이 변환됩니다:
function num() {}
var num; // 반복 선언은 무시됨
num = 8;
결과적으로 num = 8이 됩니다.
우리가 모듈이 어렵다고 말했던 것 기억하시나요? 모듈과 호이스팅을 합치면 머릿속을 좀 더 복잡해집니다.
index.mjs
import foo from './module.mjs';
console.log(typeof foo);
module.mjs
foo = 25;
export default function foo() {}
해설.
export default function foo() {}는 다음과 같습니다: function foo() {} export { foo as default }
그리고 함수는 호이스팅되고 변수 초기화는 항상 함수/변수 선언 이후에 발생한다는 사실을 상기해야 합니다.
엔진이 모듈 코드를 처리한 후, 다음과 같이 상상해 볼 수 있습니다: function foo() {} foo = 25; export { foo as default }
따라서 올바른 답은 number입니다.
프로미스 (Promises)
놀랍게도, 프로그래머들은 프로미스를 자신이 생각하는 것보다 더 잘 알고 있습니다.
이 주제에 대한 면접 질문은 보통 가장 기본적이며 대부분의 사람들이 그에 대처할 수 있습니다.
Promise.resolve(1)
.then(x => { throw x })
.then(x => console.log(`then ${x}`))
.catch(err => console.log(`error ${err}`))
.then(() => Promise.resolve(2))
.catch(err => console.log(`error ${err}`))
.then(x => console.log(`then ${x}`));
해설.
이 코드가 어떻게 단계별로 실행되는지 살펴보겠습니다.
- 첫 번째 then 핸들러는 오류를 발생시킵니다 (즉, 거부된 프로미스를 반환함).
- 다음 then 핸들러는 실행되지 않습니다. 오류가 발생했기 때문에 실행이 다음 catch로 이동합니다.
- catch 핸들러는 오류를 출력하고 빈 프로미스를 반환합니다. catch 핸들러도 then 핸들러와 마찬가지로 항상 프로미스를 반환합니다.
- catch 핸들러가 프로미스를 반환했기 때문에 다음 then 핸들러가 호출되며 값이 2인 프로미스를 반환합니다.
- 마지막 then 핸들러가 호출되어 2를 출력합니다.
결론으로,
언제나 우리는 매일 작성하는 언어를 계속해서 배우는 것을 권장하며, IT 인더스트리를 더욱 더 좋게 만들어 봅시다!
'Front End' 카테고리의 다른 글
[프론트엔드] 자바스크립트의 불변성 (Immutability) (0) | 2023.08.09 |
---|---|
[Vue.js] 간단한 Vue 기본 상식 시리즈 -2 Vue 라우터 Vue Router (0) | 2023.01.31 |
[Vue.js] 간단한 Vue 기본 상식 시리즈 -4 Vue 3 컴포지션 API (0) | 2023.01.15 |
[Vue.js] 간단한 Vue 기본 상식 시리즈 -3 Vue 3 프로젝트 라우팅 설정 (0) | 2023.01.13 |
[Vue.js] 간단한 Vue 기본 상식 시리즈 -2 Vue 3 프로젝트 시작하기 (0) | 2023.01.12 |