Daib Interview Question 1

jjanmo·2021년 2월 17일
0

Self Interview Questions

목록 보기
1/1

신입이 신입에게

이번에 신입 입사 테스트 문제를 준비할 기회가 생겼다. 나 역시 신입이기에 어떤 문제를 만들어야 할지 고민하였다. 내가 만든 문제가 입사 여부에 관계없이 지원자분이 개발자로서 성장하는 과정에서 좋은 리소스가 되었으면 하는 바람이 있었다. 그렇기에 나름 나만의 기준을 갖고 문제를 만들었다.

기준

첫번째, 기본을 물어보자.

기초가 아니다. 기본을 물어보는 것이다. 기본이라고 하면 쉬운 것 같지만 사실 기본이 가장 중요하고 어렵다. 모두들 안다고 생각하지만 막상 말로 풀어내면 잘 안된다. 설명을 할 수 있다 라는 것은 그 개념에 대해서 어느 정도의 정리가 되어있음을 뜻한다. 면접에서 사용할지 과제 테스트로 주어질지는 결정되지 않았지만, 개념에 대해서 말 혹은 글로 설명할 수 있는 문제를 만들고자 하였다.

두번째, 상황을 만들자.

개발자는 상황(조건, 기준 등)에 맞춰서 개발을 한다. 개인 프로젝트가 아닌 이상 협업을 하게 되고 그에 맞는 환경이 구성된다. 많은 조건들을 어떻게 생각하고 있고 이를 어떻게 대처해나가는지를 알아볼 수 있도록 상황을 주려고 노력하였다. 그냥 단순히 '구현하시오'가 아니라 좀 더 구체적으로 상황과 코드를 설정하였다.

세번째, 재미있는 썸띵!!

나름 인터렉티브함과 코드의 재미는 덤...그러나...과연...😈

실제 문제를 풀어볼 수 있는 링크입니다. 아래는 제가 생각하는 문제의 의도와 솔루션입니다. 부정확한 개념이나 잘못된 부분이 있다면 피드백은 언제나 환영입니다. 🚀

문제 상황

이미지를 보면 알겠지만 위에 5가지 버튼이 있다. 이 5가지 버튼을 누르면 해당하는 색으로 아래의 텍스트가 변경된다. 또 리셋버튼을 누르면 텍스트가 원래 상태로 변경된다.

이 문제를 보면 바로 무엇을 물어볼지 예상하는 사람들도 있을 것이다. 사실 이 문제는 많이 유명한 문제(여기서 첫번째 문제)를 각색 및 재구성한 것이다. 이 문제를 먼저 보면 쉽게 해결할 수 있을 것이다. 하지만 이 문제를 만든 근본적인 이유는 문제를 해결하는 것보다 왜 이렇게 해결되는지의 과정에 초점을 맞추는 문제 라고 볼 수 있다.

문제 설명

문제1. 최초 실행을 하면 아마 어떠한 에러를 마주할 것이다. 이 에러는 왜 발생하는 것인지 설명해보자.
문제2. 이미지와 같이 동작하기 위해서 어떻게 수정을 해야할까?
문제3. 리셋버튼 기능을 구현해보자.

문제는 이렇게 3문제로 구성되었다. 여기에 베이스 코드가 주어진다. 각각의 문제는 의도가 있다. 의도에 대해선 아래 솔루션을 적어나가는 과정에서 이야기할 예정이다.

문제1

문제의도 : 개념에 대한 이해

문제를 해결하기 위해서, (최초실행)버튼을 클릭하면 아래와 같은 에러가 발생한다.

그러면 이제 에러가 왜 나타나는지에 대해서 베이스 코드를 보면서 분석할 것이다.

const $buttons = document.querySelectorAll(".button");
const $daib = document.querySelector(".daib");

for (var i = 0; i < $buttons.length; i++) {
  $buttons[i].addEventListener("click", function handleClick() {
    const colorName = $buttons[i].textContent; //--- *
    $daib.className = `daib bg-${colorName}`;
  });
}

베이스 코드가 크게 어렵거나 복잡하지 않다. 간단히 설명하면, 모든 버튼 엘리먼트를 가져와서 각각에 클릭이벤트를 달아준다. 핸들러에는 버튼을 클릭했을 때 발생하는 특정 로직(여기선 클릭에 따라서 색이 바뀌는 로직) 들어가 있다. 에러를 읽어보면 별표(*)에서 undefined라는 에러가 발생하였다.

나는 이 부분을 이해하기 위해선 2가지에 대해서 알아야한다고 생각했다.

첫번째$buttons$buttons[i]가 무엇을 나타내는지를 알아야한다. $buttonsquerySelectorAll()로 가져온 배열이다. 그 안에는 각각의 버튼 엘리먼트 객체가 담겨져 있다.

참고 사실, 배열은 아니다. 정확하게 Nodelist 이다. 이 부분과 비교하여, 클래스 선택자로서 getElementsByClassName() 로 엘리먼트를 가져올 수 있는데, 그 결과값 역시 배열이라고 착각할 수 있다. 이 부분도 정확하게 말하면 HTMLCollection 이다. API가 다르기 때문에 사용할 때 참고하여 사용하면 될 것이다.

버튼 엘리먼트 객체 안에는 여러가지 속성이 존재한다. 그 중에서 textContent 라는 속성에 접근하려고 하는 것이 위의 코드이다. 도식화해보면, 아래와 같다.

buttons = [ button1, button2, button3, button4, button5 ]

button1 = {
    name : ....,
    value : ....,
    ....
    textContent : ....
}

buttons[0].textContent

button1의 textContent에 접근하려고 한다.

그럼 두번째로 알아야 하는 것은 undefined가 나오는 경우가 언제인가 이다. 아래 코드를 보고 1,2,3에서 무엇이 출력되는지 생각해보자.

const person = {
 	name : 'jjanmo',
}
console.log(person.name) 	// 1
console.log(person.age) 	// 2
console.log(person.date.birth)  // 3
정답
1.  jjanmo
2.  undefined
3.  Uncaught TypeError: Cannot read property 'birth' of undefined

2번 출력 결과를 보면 자바스크립트의 특징을 알 수 있다. 자바스크립트는 객체의 없는 속성에 접근하려고 하면 에러가 나는게 아니라 undefined를 발생시킨다. 그렇기 때문에 이런 식으로 잘못된 접근을 시도해도 직접적인 에러를 출력하지 않기 때문에 찾기 힘든 경우가 발생할 수 있다.

참고 배열에서도 이와 같은 맥락의 undefined가 존재한다. 배열에서 없는 인덱스로 접근하려고하면 에러가 아니라 undefined가 뜬다. 이것은 일반적인 정적타이핑 언어(자바, C 등)에서 쉽게 접할 수 있는 에러인 Array index out of bounds에서 해방(?)될 수 있는 특징이지만 한편으로 이러한 부분에서 오류가 발생하면 (명시적으로 오류가 발생하지 않기 때문에) 이를 해결하는 것이 쉽지 않을 수 있다.

3번 출력 결과가 우리가 봤던 에러와 비슷하다(거의 같다). 해석해보면 이렇다.

undefined의 프로퍼티 birth(속성 birth)를 읽을 수 없습니다.

이 말은 undefined에는 birth 속성이 없기 때문에 읽을 수 없다는 말이다. 그러면 date 라는 값이 undefined라는 말이 되고 person이라는 객체 안에는 date라는 속성이 없다는 결론을 내릴수 있다.

2번과 3번 결과를 종합해보면 이제 왜 이런 에러가 나오는지 이해가 할 수 있을 것이다.

 const colorName = $buttons[i].textContent;

 //error message : Cannot read property 'birth' of undefined

결국 $buttons[i]undefined 이기 때문에 textContent라는 속성을 갖고 있지 않다라는 말이된다.

그럼 또 다시 의문이 든다. $buttons[i]가 undefined라는 말은 버튼 엘리먼트를 제대로 가져오지 못했다는 말이 된다. 여러가지 의문이 들 수 있다. 결론적으로 말하면 이 부분은 스코프콜백함수에 대한 개념을 물어보는 것이다. (개인적으로 스코프에 대한 개념이 좀 더 필수적이라고 생각한다.) 여기까지 생각이 들었다면 이제 코드를 수정할 수 있을 것이다. 스코프와 콜백함수에 대한 개념은 문제2를 풀어가는 과정 속에서 설명하겠다. 스코프와 콜백함수에 대해서 자세하게 설명하면 너무 길어지기에 자세한 내용은 기회가 되면 정리해보도록 하겠다. 😅

참고 스코프유효범위라고 한다. 여기서 무엇에 대한 유효범위인지가 중요하다. 식별자 해결(변수의 값을 찾는 것)하기 위한 범위가 스코프이고 해당 변수의 값을 찾을 때까지 상위의 스코프로 올라가는 과정이 진행되는데, 이를 스코프 체인이라고 한다.
콜백함수말 그대로 나중에 불러지는 함수이다. 일반적으로 개발자가 호출하는게 아닌 필요한 시점에(내부적으로 미리 설정된 시점) 불러지는 것을 콜백함수라고 한다. 위 코드에서 이벤트 핸들러가 이에 해당한다.

문제2

문제의도 : 문제 해결 능력

문제2의 경우, 다양한 방법으로 해결할 수 있다. 많은 해결 방법을 사용하면 좋긴할 것이다. 단, 그 해결 방법에 대해서 왜 그렇게 사용하는지에 대한 이해가 선행되어야 할 것이다. 나는 3가지의 방법을 통해서 해결하였다.

Solution1

1번 풀이가 가장 쉽다.

for (let i = 0; i < $buttons.length; i++) {
  $buttons[i].addEventListener("click", function handleClick() {
    const colorName = $buttons[i].textContent;
    $daib.className = `daib bg-${colorName}`;
  });
}

무엇이 바뀌었는지 찾지 못할 수도 있다. for문의 varlet 으로 바뀌었다.

이렇게만 보면 별거없네 라고 생각하기 쉽지만 왜 이렇게 바꾸면 되는지에 대해서 설명하는 것이 중요한다. 우선 베이스 코드가 왜 제대로 작동안했는지 스코프 관점에서 살펴보자.

var i; //전역공간

for (i = 0; i < $buttons.length; i++) {
  $buttons[i].addEventListener("click", function handleClick() {
    const colorName = $buttons[i].textContent;
    $daib.className = `daib bg-${colorName}`;
  });
}

var 는 기본적으로 function level scope를 갖고 있다. 그렇기 때문에 베이스 코드처럼 사용하게 되면 사실은 위의 코드처럼 사용한 것과 동일한 코드가 된다. 이제 코드가 실행되는 순서를 생각해보자. 최초로 코드가 실행되었을 때의 상태를 코드로 표현해보자. 우선 반복문이 끝났을 때의 상태이다.

var i = 5; //전역공간

$buttons[0].addEventListener("click", function handleClick() {
  	const colorName = $buttons[0].textContent;
	$daib.className = `daib bg-${colorName}`;
});

$buttons[1].addEventListener("click", function handleClick() {
	const colorName = $buttons[1].textContent;
	$daib.className = `daib bg-${colorName}`;
});
// 생략...
            
$buttons[4].addEventListener("click", function handleClick() {
    const colorName = $buttons[4].textContent;
    $daib.className = `daib bg-${colorName}`;
});

이런 식으로 5개의 리스너가 반복문을 통해서 만들어지고 클릭 이벤트를 기다리는 콜백함수가 만들어진다. 맨 위를 보면 반복이 끝나면 전역변수인 i는 5이다. 5번을 반복하였기 때문에 이 부분은 이해할 수 있을 것이다. 이 상태는 아직 콜백함수가 실행되지 않은 상태이다. 그런데 이벤트가 발생하는 시점에 콜백 함수가 실행되면 콜백함수 안에 있는 i의 값을 찾고자 할 것이다. 그런데 i은 값은 함수 안에는 존재하지않기 때문에 스코프 체인을 통해서 한단계 위의(바깥의) 스코프로 올라가서 i의 값을 찾게된다.(여기서는 전역스코프가 된다.) 그러면 i의 값은 이미 반복이 끝나고 5가 되어있기 때문에
$buttons[5].textContent라는 코드를 실행하고자 한다. 그렇기 때문에 에러가 발생하게 된다. 그리고 엄밀하게 이야기해서 위의 코드는 약간(?) 잘못되었다. 다시 적어보자.

var i = 5; //전역공간

$buttons[0].addEventListener("click", function handleClick() {
  	const colorName = $buttons[i].textContent;
	$daib.className = `daib bg-${colorName}`;
});

$buttons[1].addEventListener("click", function handleClick() {
	const colorName = $buttons[i].textContent;
	$daib.className = `daib bg-${colorName}`;
});
// 생략...
            
$buttons[4].addEventListener("click", function handleClick() {
    const colorName = $buttons[i].textContent;
    $daib.className = `daib bg-${colorName}`;
});

콜백함수 내부의 i에는 반복이 끝난 직후에는 값이 할당되지 않는다. 실행할 때 스코프 체인에 의해서 해당 값을 찾기 때문이다.

그러면 이제 왜 에러가 발생하는지에 대해서 알 수 있었다. 이제 왜 let 을 사용하면 되는지 알아보자. let은 var 와 다르게 block level scope 이다. 그렇기 때문에 var를 사용했을 때와는 다르게 코드가 작동한다. 이 역시 반복문이 끝난 직후의 코드를 적어보겠다.

{
  let i = 0;
  $buttons[0].addEventListener("click", function handleClick() {
  	const colorName = $buttons[i].textContent;
	$daib.className = `daib bg-${colorName}`;
  });
}
{
  let i = 1;
  $buttons[1].addEventListener("click", function handleClick() {
	const colorName = $buttons[i].textContent;
	$daib.className = `daib bg-${colorName}`;
  });
}
// 생략...

{
  let i = 4
  $buttons[4].addEventListener("click", function handleClick() {
    const colorName = $buttons[i].textContent;
    $daib.className = `daib bg-${colorName}`;
  }); 
}
            

정확하게 이렇게 실행된다라기 보다 이런 식으로 작동한다는 것을 보여주기 위한 코드이다. 만약에 전역공간에서 i의 값을 출력하려고 하면 Uncaught ReferenceError: i is not defined 이런 오류를 마주하게 될 것이다. 이는 i는 var를 사용했던 것과는 다르게 전역이 아닌 블럭(내부, { }) 공간에만 존재함을 알 수 있다.

위의 코드 처럼 i는 블럭 안에만 존재한다. 그래서 콜백함수가 실행되는 시점에 i의 값을 찾으려고 하면 상위 스코프에 제대로된 i값이 존재하기 때문에 제대로 작동이 되는 것이다.

Solution2

두번째 풀이방법은 IIFE(ImmediatelyInvokedFunctionExpression, 즉시실행함수)를 활용한 방법이다. 위에서 말했듯이 기본적으로 자바스크립트는 function level scope 이다. 즉시실행함수는 말그대로 함수이다. 그렇기 때문에 즉시실행함수를 만들어 인위적으로 스코프를 만들어줌으로서 이를 해결할 수 있다.

for (var i = 0; i < $buttons.length; i++) {
  $buttons[i].addEventListener(
    "click",
    (function (j) { //function 
      return function () {
        const colorName = $buttons[j].textContent;
        $daib.className = `daib bg-${colorName}`;
      };
    })(i)
  );
}

이 코드를 보면 좀 복잡할 수 있다. 위에서 설명한 것을 음미하면서 아래에 도식화해보겠다.

각각의 네모칸은 스코프를 말한다. 화살표는 함수가 실행될 때, 내부 변수의 값을 설정하기 위해서 바라보는 외부 스코프의 방향과 해당 변수를 말한다.

여기서 알아야 할 점은 이벤트 리스너의 인자로는 이벤트 문자열과 콜백함수(이벤트 핸들러)가 들어와야한다. 그렇기 때문에 여기서도 즉시실행함수로 리턴되는 값이 함수여야만 한다. 이 점을 인지하고 도식화된 이미지를 설명하면, 즉시실행함수가 실행될 때(반복문이 돌때)에는 전역스코프의 i를 인자로 받아서 사용한다. 그렇게 되면 즉시실행함수 내부의 스코프에 i값이 할당된다.(매개변수도 변수이기 때문에 i값을 받게되면 변수로서 할당된다.) 즉시실행함수의 실행 결과로 나타난 함수는 이벤트가 발행하면 실행이 된다. 이 때 이 함수에 존재하는 j는 상위스코프인 즉시실행함수의 스코프에 있는 j를 찾아서 그 값을 자신의 j의 값으로 할당하여 사용하게 된다.

이 부분이 이해되지 않는다면 스코프와 스코프체인에 대해서 구글링을 통해서 여러 개의 블로그를 음미해보는 것을 추천한다. 두번째 방법의 포인트는 제대로 된 값을 찾을 수 있도록 즉시실행함수를 통해서 인위적으로 스코프를 만들어 주었다는 점이다.

Solution3

세번째 방법은 내가 좋아하는 방법이다.

const handleClick = (e) => {
  const colorName = e.target.textContent;
  $daib.className = `daib bg-${colorName}`;
};

$buttons.forEach(($button) => $button.addEventListener("click", handleClick));

핸들러를 분리하여 사용하고 (제공되는)매개변수를 사용함으로서 스코프와 관계없이(?) 작동이 되도록 만들수 있다.

참고 e.target 와 e.currentTarget에 대해서 알아보자. 다 설명하면 재미없으니, 직접 찾아보시길 😈

문제3

문제 의도 : 협업, 코드 컨벤션 등

세번째 문제는 비교적 쉬운 문제이다. reset이라는 네이밍처럼 리셋(초기화)버튼의 기능을 구현하는 것이다. 구현하고자 하면 쉽게 구현할 수 있다. 해당 엘리먼트를 가져와서 거기에 클릭이벤트를 걸어주고 리셋로직을 넣어주면 끝이다. 하지만 여기서 보려고 하는 것은 내 생각대로만 구현하는 것이 아니라 주변의 코드를 살펴보면서 전체적인 컨벤션이나 네이밍이나 로직의 일관성을 유지하고 있는지 여부를 알아보려는 의도가 포함되어있다.

이러한 관점에 $를 사용한 것도 있다. $ 표시를 사용한 변수는 일반적으로 엘리먼트라는 것을 나타내는 컨벤션이다. 그렇기 때문에 구현할 때 해당 엘리먼트를 가져올 때 $를 사용하지 않는다는 것은 잘못된 코드는 아니지만 일관성이 없는 코드일 수 있다. 이 말은 기존에 코드를 살펴보려는 노력을 하지 않았다고도 볼 수 있기 때문에 긍정적으로 보긴 어렵지않을까?! 라고 개인적으로 생각한다. 🧐

const $reset = document.querySelector(".reset");

$reset.addEventListener("click", function handleReset() {
  $daib.className = `daib`;
});

핸들러를 분리하지 않은 방법

const $reset = document.querySelector(".reset");

const handleReset = () => {
  $daib.className = `daib`;
};

$reset.addEventListener("click", handleReset);

핸들러를 분리한 방법

위의 코드에서 초기화를 시킬 때 className에 값을 재할당함으로서 초기화를 시키고 있다. 하지만 이 방법 말고도 classList를 사용하여 좀 더 모던하게 구현할 수도 있을 것이다. 이것은 구현의 선택사항이겠지만 개인적으로 코드에서 일관된 로직으로 보여주는 것을 선호한다. 같은 로직인 배경색을 변경하는 로직에서도 className에 할당하는 방식으로 스타일을 컨트롤하였기 때문이다.

짧게 끝날거라는 나의 생각은 오판이였다. 문제를 만들고 이를 설명하는 과정에서 가장 어려웠던 점은 개념을 어디서부터 어디까지 설명해야할지에 대한 부분이였다. 다 설명하자니 너무 길어지고 짧게 설명하지나 정확한 이해가 힘들것 같은 느낌 속에서 중도(적당한 분량의 적절한 이해)를 지키는 것은 참으로 어려운 일이였다. 나름 내가 알고 있는 개념에 대해서 정확하게 표현하고자 노력했지만 여전히 부족하고 막히는 부분들이 존재하였다. 이 부분을 채우면서 정리하는 과정이 있었기에 만족스럽다. 이 문제를 풀게될 누군가도 이를 분석하고 해결해나가는 과정 속에서 성장할 수 있는 계기가 되었으면 좋겠다.

부족한 글 읽어주셔서 감사합니다. 내용에 대한 피드백은 언제나 환영입니다. 앞으로 조금이라도 도움이 될 수 있는 글을 쓰기 위해서 노력하겠습니다.

profile
눈길을 걸어갈 때 어지럽게 걷지 말기를.

0개의 댓글