Please enable JavaScript to view the comments powered by Disqus.

프론트엔드 면접 핸드북 - 자바스크립트(3)

이 인터뷰 핸드북은 front-end-interview-handbook/javascript-questions을 기반으로 정리한 것입니다.


41. ES6의 클래스와 ES5의 생성자 함수의 차이점

한줄 답변: 둘 다 생성자 역할을 하지만 상속을 구현하는 방법에서 큰 차이가 있다.

간단한 생성자 함수와 클래스

// ES5 Function Constructor
function Person(name) {
  this.name = name;
}

// ES6 Class
class Person {
  constructor(name) {
    this.name = name;
  }
}

아래는 상속을 구현한 코드. ES5 문법이 훨씬 더 길고 복잡하며 클래스 문법을 사용하면 간단하다.

ES5에서는 생성자 함수를 상속받기 위해서 prototype에 새 객체를 복사하는 등 여러가지 작업이 필요하지만 클래스는 그런 과정이 필요없다. extends 키워드로 상속받을 함수를 명시하고, constructor 메소드에서 super(this) 만 추가하면 된다.

// ES5 Function Constructor
function Student(name, studentId) {
  // Call constructor of superclass to initialize superclass-derived members.
  Person.call(this, name);

  // Initialize subclass's own members.
  this.studentId = studentId;
}

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

// ES6 Class
class Student extends Person {
  constructor(name, studentId) {
    super(name);
    this.studentId = studentId;
  }
}

42. 화살표 함수를 어떻게 사용하면 좋은지, 기존 함수와의 차이점은 무엇인지

한줄 답변: 간단한 문법, 함수 안의 this가 선언된 scope의 this를 가리키는 lexical scoping

우선 function 키워드가 필요없어서 함수 선언 문법이 훨씬 단순하다는 명백한 장점이 있다. 그리고 화살표 함수 안에서 this 는 화살표 함수를 포함한 스쿠프의 this를 가리킨다. function 으로 선언된 함수 안에서 this는 그 함수를 호출한 객체에 의해 결정된다.

이러한 lexical-scope(문법적인 스쿠프)는 더 직관적이며 React 컴포넌트에서 콜백을 호출할 때 특히 유용하다. (컴포넌트 메소드 안에서 this가 메소드가 선언된 컴포넌트 인스턴스가 아니라 다른 객체 인스턴스를 가리킬 수 있기 때문이다. 컴포넌트 메소드를 화살표 함수로 선언하면 저런 문제가 사라진다.)


43. 생성자 함수에서 메소드 선언에 화살표 함수를 선언하는 것의 장점은?

한줄 답변: this가 바뀌지 않는다. 항상 같은 객체를 가리킨다.

생성자 안에서 메소드를 화살표 함수로 선언하는 것의 가장 큰 장점은 this가 가리키는 값이 함수 생성 시점에 결정된 후에 바뀌지 않는다는 것이다. 그러므로 생성자 함수가 새 객체를 만들면 this 는 언제나 그 객체를 가리킨다.

아래의 예제에서 첫번째 메소드는 function 키워드를 사용해서, 두번째 메소드는 화살표 함수로 선언되어 있고, 둘 다 함 수 안에서 this 키워드를 참조한다.

const Person = function (firstName) {
  this.firstName = firstName;

  // 일반 함수
  this.sayName1 = function () {
    console.log(this.firstName);
  };

  // 화살표 함수
  this.sayName2 = () => {
    console.log(this.firstName);
  };
};

const john = new Person("John");
const dave = new Person("Dave");

john.sayName1(); // John

// 일반 함수의 this는 바뀔 수 있다. 하지만 화살표 함수에서는 바뀌지 않는다.
john.sayName1.call(dave); // Dave (call로 인해 this는 이제 dave 객체다)
john.sayName2.call(dave); // John (call을 사용했지만 this는 여전히 john 객체다)

// apply도 call을 사용한 예제와 마찬가지로 바뀌지 않는다
john.sayName1.apply(dave); // Dave
john.sayName2.apply(dave); // John

// bind를 사용해도 마찬가지로 바뀌지 않는다
john.sayName1.bind(dave)(); // Dave
john.sayName2.bind(dave)(); // John

var sayNameFromWindow1 = john.sayName1;
sayNameFromWindow1(); // undefined
// sayNameFromWindow1 변수는 전역 스쿠프에 속하며, 전역 스쿠프에서 this는 window 객체다.

var sayNameFromWindow2 = john.sayName2;
sayNameFromWindow2(); // John. this는 변하지 않는다.

이처럼 화살표 함수 안에서 this는 바뀌지 않는다. 그러므로 화살표 함수를 어플리케이션의 어디에 가져다 둬도 함수 안에서 컨텍스트가 바뀔 걱정을 하지 않아도 된다.

이는 React 클래스 컴포넌트에서 특히 유용하다. 만약 클릭 이벤트 핸들러로 사용할 컴포넌트 메소드를 일반 함수로 선언하고, 그 메소드를 자식 컴포넌트의 props로 전달해서 사용한다고 가정하자. 자식 컴포넌트에서 그 메소드가 실행될 때 this는 자식 컴포넌트 인스턴스 객체를 가리키게 된다. 만약 메소드 안에서 this를 사용하려 한다면 의도한 것과는 다른 결과가 발생할 것이다(Uncaught TypeError: undefined is not a function 같은 오류가 흔히 발생한다.)

이를 방지하기 위해서는 상위 컴포넌트에서 메소드에 this를 bind해줘야 한다.

render() {
   return(
     <ChildComponent onChange={ this.handleChange.bind(this) } />
   )
}

하지만 화살표 함수를 사용하면 저럴 필요가 없다. 메소드 안에서 this는 항상 그 메소드가 선언되어 있는 컴포넌트 객체를 가리킬 것이기 때문이다.


44. 고차 함수(higher-order function)란?

한줄 답변: 파라미터로 함수를 받거나, 함수를 리턴하는 함수

고차 함수는 함수를 파라미터로 받거나 함수를 리턴하는 함수를 말한다. 반복적으로 실행되는 어떤 작업을 추상화시키는 수단으로 사용한다.

고차 함수의 고전적인 예제는 Array.prototype.map 함수다. 배열을 기반으로 새로운 배열을 만들 때 사용하는데, 이런저런 과정을 생략하고 맵핑 로직만 전달하면 되기 때문에 코드가 무척 간결해진다.

배열에 문자열로 구성되어 있고, 모든 요소에 toUpperCase를 적용해서 새 배열을 만들어야 한다고 하자. map 없이 구현하려면 아래와 같지만

const names = ["irish", "daisy", "anna"];

const transformNamesToUppercase = function (names) {
  const results = [];
  for (let i = 0; i < names.length; i++) {
    results.push(names[i].toUpperCase());
  }
  return results;
};
transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']

.map(transformerFn) 함수를 사용한다면 무척 간단해진다.

const transformNamesToUppercase = function (names) {
  return names.map((name) => name.toUpperCase());
};
transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']

Array의 forEach, filter, reduce 등도 모두 고차 함수다.

관련 자료


45. 구조 분해 할당(destructuring) 예제를 보여줄 수 있는가?

한줄 답변: ES6에서 도입된 객체, 배열의 멤버를 간단하게 추출할 수 있는 문법

구조 분해 할당은 ES6에 도입된 문법으로 객체와 배열의 값을 추출해서 변수에 바로 할당할 수 있는 간결하고 편리한 방법이다.

배열 destructuring

// 예제 배열 선언
const foo = ["one", "two", "three"];

const [one, two, three] = foo; // 배열 내부 순서대로 변수에 할당된다.
console.log(one); // "one"
console.log(two); // "two"
console.log(three); // "three"

// 변수 교환
let a = 1;
let b = 3;

[a, b] = [b, a];
console.log(a); // 3
console.log(b); // 1

객체 destructuring

// 예제 변수 선언
const o = { p: 42, q: true };
const { p, q } = o; // 객체의 필드 이름을 그대로 사용해야 한다

console.log(p); // 42
console.log(q); // true

const { p: pValue, q: qValue } = o; // 새로운 변수명을 제공할 수도 있다.
console.log(pValue); // 42
console.log(qValue); // true

관련 자료


46. 문자열 생성에 있어 큰 유연성을 제공하는 템플릿 리터럴의 예제를 보여달라

한줄 답변: result is ${data}

템플릿 리터럴은 문자열 삽입, 문자열 안에 변수 넣기 등을 간단하게 할 수 있도록 한다. ES6 이전에는 아래와 같이 해야 했다.

var person = { name: "Tyler", age: 28 };
console.log(
  "Hi, my name is " + person.name + " and I am " + person.age + " years old!"
);
// 'Hi, my name is Tyler and I am 28 years old!'

하지만 템플릿 리터럴이 있으면 훨씬 간단해진다.

const person = { name: "Tyler", age: 28 };
console.log(`Hi, my name is ${person.name} and I am ${person.age} years old!`);
// 'Hi, my name is Tyler and I am 28 years old!'

템플릿 리터럴임을 명시하고 ${} 같은 표현식을 사용하기 위해서는 홑따옴표(quote)가 아니라 backtick을 사용해야 한다.

두번째로 유용한 점은 여러 줄로 구성된 문자열을 표현하기 더 쉽다는 것이다. ES6 전에는 개행 문자를 사용해서 표현해야 했다.

console.log("This is line one.\nThis is line two.");
// This is line one.
// This is line two.

그리고 화면을 넘어가는 긴 문자열을 에디터에서 보기 좋게 작성하려면 아래와 같이 문자열을 분리할 필요도 있었다.

console.log("This is line one.\n" + "This is line two.");
// This is line one.
// This is line two.

하지만 템플릿 리터럴을 사용하면 개행 문자를 사용하지 않더라도 여러 줄로 문자열을 표현할 수 있다.

console.log(`This is line one.
    This is line two.`);
// This is line one.
// This is line two.

그리고 다른 사용 방법은 문자열에 간단한 변수를 삽입해서 템플릿 라이브러리(ex. React)를 대체하는 것이다.

const person = { name: "Tyler", age: 28 };
document.body.innerHTML = `
      <div>
        <p>Name: ${person.name}</p>
        <p>Name: ${person.age}</p>
      </div>
    `;

innerHTML을 사용할 때 주의할 점은 XSS(cross site scripting 을 사용한 해킹)에 노출될 수 있다는 사실이다. 사용자로부터 제공된 데이터(ex. 자기소개, 제품 후기 등)라면 사용하기 전에 항상 문자열을 sanitize해야 한다.

관련 자료


47. curry 함수의 예제, 그리고 curry 함수가 제공하는 장점은?

한줄 답변: 함수 파라미터가 n개라면, 총 n번 호출해야 실제로 실행되는 함수를 만든다. 함수형 프로그래밍에 유용하다

커링(curryin)은 파라미터가 1개 이상은 함수를 여러개로 분리하고, 연속으로 호출하면서 파라미터를 누적시킨 후 필요한 파라미터가 모두 제공되면 실제 함수를 호출하는 패턴이다.

이 방법은 함수형으로 작성된 코드를 더 읽기 쉽고 조합하기 쉽도록 하는데 유용하다. 함수를 커링할 때 처음에 1개의 함수를 제공해야 하고, 파라미터를 1개씩 제공받는 함수로 쪼개진다. 예를 들어 어떤 함수의 파라미터가 3개라면, 커링된 함수는 총 3번 호출해야 처음에 제공된 함수가 호출된다.

function curry(fn) {
  // 커링에는 재귀 패턴을 사용한다.
  if (fn.length === 0) {
    // Function.prototype.length는 파라미터의 수
    return fn;
  }

  // 커링 로직 재귀 함수
  // depth = 분리할 갯수(최대 재귀 호출 횟수), args = 현재까지 누적된 파라미터 배열
  function _curried(depth, args) {
    return function (newArgument) {
      if (depth - 1 === 0) {
        // 필요한 파라미터가 모두 쌓였다면 처음 제공된 함수 호출
        return fn(...args, newArgument);
      }

      // 파라미터가 전부 쌓이지 않았다면, 커링 로직을 다시 호출한다.
      return _curried(depth - 1, [...args, newArgument]);
    };
  }

  return _curried(fn.length, []); //
}

function add(a, b) {
  // add는 2개의 파라미터가 필요하다.
  return a + b;
}

var curriedAdd = curry(add); // add 함수를 커링시킨다.
var addFive = curriedAdd(5); // 첫번째 파라미터에 5가 들어간 새로운 함수를 만든다.

var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]

관련 자료


48. spread 문법과 장점과 rest 문법과의 차이점은?

한줄 답변: spread ⇒ 데이터를 풀어놓는다, rest ⇒ 전달받은 데이터를 배열, 객체 안에 채워넣는다

ES6의 전개(spread) 문법은 함수형 패러다임으로 코드를 작성할 때 매우 유용하다. 배열이나 객체의 복제본을 Object.createArray.prototype.slice , 또는 라이브러리 함수를 사용하지 않고도 간단하게 만들 수 있기 때문이다. 이 문법은 Redux나 RxJS 를 사용하는 프로젝트에서 자주 사용된다.

function putDookieInAnyArray(arr) {
  return [...arr, "dookie"];
}

const result = putDookieInAnyArray(["I", "really", "don't", "like"]); // ["I", "really", "don't", "like", "dookie"]

const person = {
  name: "Todd",
  age: 29,
};

const copyOfTodd = { ...person };

ES6의 rest 문법은 함수에 전달된 파라미터를 배열로 만들 수 있는 간단한 문법이다. 이것은 마치 spread 문법을 반대로 해놓은 것 같다. spread가 배열 안에 있는 데이터를 펼쳐놓는다면, rest 문법은 데이터를 받아서 배열에 채워넣기 때문이다.

이 문법은 함수 파라미터, 배열와 객체 destructuring에도 사용할 수 있다. destructuring에서 배열 데이터는 새로운 배열로, 객체 데이터는 새로운 객체로 만들어진다.

function addFiveToABunchOfNumbers(...numbers) {
  return numbers.map((x) => x + 5);
}

const result = addFiveToABunchOfNumbers(4, 5, 6, 7, 8, 9, 10); // [9, 10, 11, 12, 13, 14, 15]

const [a, b, ...rest] = [1, 2, 3, 4]; // a: 1, b: 2, rest: [3, 4]

const { e, f, ...others } = {
  e: 1,
  f: 2,
  g: 3,
  h: 4,
}; // e: 1, f: 2, others: { g: 3, h: 4 }

관련 자료


49. 파일 사이에서 코드를 어떻게 공유하는가?

한줄 답변: CommonJS, AMD, ES modules 를 사용한다

자바스크립트 환경에 따라 다르다

클라이언트(브라우저 환경)에서는 변수, 함수가 global 스쿠프에 선언되기 때문에 모든 스크립트에서 서로 참조할 수 있다. 또는 RequireJS로 AMD(Asynchronous Module Definition)를 사용해서 모듈화시킬 수도 있다.

서버(node.js)에서는 CommonJS를 사용한다. 각각의 파일은 모듈로 취급되며 module.exports 문법을 사용해서 변수, 함수를 내보낼 수 있다.

ES2015는 AMD와 CommonJS를 대체할 수 있는 모듈 문법을 정의했다. 이것은 브라우저와 Node 환경에서 모두 사용할 수 있다. (import, export)

관련 자료


50. static 클래스 멤버를 사용하는 이유는?

한줄 답변: 객체 인스턴스의 영향 없음. 설정 값, 유틸리티 함수로 활용

static 클래스 멤버(속성, 메소드)는 특정한 클래스 인스턴스에 구속되지 않으며 어떤 인스턴스에서 참조하더라도 같은 값을 가진다. static 속성은 주로 설정값으로 사용하며, 메소드는 인스턴스의 상태값에 영향을 받지 않는 순수한 유틸리티 함수로 사용한다.

관련 자료


51. let, var, const 를 사용해 선언된 변수의 차이점

한줄 답변: var는 함수 레벨 스쿠프. let, const는 블럭 레벨 스쿠프.

var 키워드로 선언한 변수는 그 변수를 포함한 함수 스쿠프에 속한다. 만약 변수를 선언한 곳이 어떤 함수 안도 아니라면 전역 스쿠프에 속한다.

let, const 는 함수 스쿠프가 아닌 블럭 스쿠프(함수, if~else, for 루프 등)에 속한다. 이는 가장 가까운 중괄호({} )로 둘러싸인 영역 안에서만 접근 가능하다는 의미다.

function foo() {
  // 모든 변수는 이 함수 안에서만 접근 가능하다.
  var bar = "bar";
  let baz = "baz";
  const qux = "qux";

  console.log(bar); // bar
  console.log(baz); // baz
  console.log(qux); // qux
}

console.log(bar); // ReferenceError: bar is not defined
console.log(baz); // ReferenceError: baz is not defined
console.log(qux); // ReferenceError: qux is not defined

if (true) {
  var bar = "bar";
  let baz = "baz";
  const qux = "qux";
}

// var 키워드로 선언된 변수는 함수 스쿠프 어디에서든 접근 가능하다.
console.log(bar); // bar

// let, const 키워드로 선언된 변수를 중괄호 바깥에서는 접근 불가능하다.
console.log(baz); // ReferenceError: baz is not defined
console.log(qux); // ReferenceError: qux is not defined

호이스팅

var는 호이스팅(hoisting)되지만 let, const는 그렇지 않다. let, const로 선언된 변수를 선언되기 전에 참조하려 한다면 오류를 발생시킬 것이다. 대신 호이스팅된 변수는 undefined로 나온다.

console.log(foo); // undefined
var foo = "foo";

console.log(baz); // ReferenceError: can't access lexical declaration 'baz' before initialization
let baz = "baz";

console.log(bar); // ReferenceError: can't access lexical declaration 'bar' before initialization
const bar = "bar";

재선언

var 키워드로 같은 이름의 변수를 다시 선언하는 것은 가능하지만, let, const는 에러를 발생시킨다.

var foo = "foo";
var foo = "bar";
console.log(foo); // "bar"

let baz = "baz";
let baz = "qux"; // Uncaught SyntaxError: Identifier 'baz' has already been declared

재할당

let은 변수에 값을 재할당하는 것이 가능하지만, const는 불가능하다.

let foo = "foo";
foo = "bar";

// 아래 코드는 오류를 발생시킨다.
const baz = "baz";
baz = "qux";

관련 자료