뒤로가기
프론트엔드 기술 면접에서 자주 나오는 질문과 진짜 답변법

November 11, 2021

interviewfrontendcareer

2년 전, 이직을 준비하면서 처음으로 기술 면접이란 걸 봤다. 면접관이 물었다. "클로저가 뭔가요?" 나는 속으로 쾌재를 불렀다. 이건 준비했던 거니까. "클로저는 함수와 그 함수가 선언된 렉시컬 환경의 조합으로, 외부 함수의 실행이 끝난 후에도 내부 함수가 외부 함수의 변수에 접근할 수 있는..." 면접관의 표정이 미세하게 변하는 걸 느꼈다. 그 다음 질문은 "그래서 실무에서 클로저를 어디에 쓰셨어요?"였는데, 나는 말문이 막혔다.

탈락했다. 면접 후기를 노션에 정리하면서 뭐가 잘못됐는지 곱씹어봤다. 내가 한 건 MDN 문서를 읊은 것뿐이었다. 면접관 입장에서 보면, "이 사람이 진짜 이해하고 있는 건가, 아니면 외운 건가"를 판별하는 가장 쉬운 방법이 후속 질문이다. 교과서 정의만 갖고 있으면 후속 질문에서 바로 무너진다.

그 뒤로 면접을 몇 번 더 보고, 반대로 면접관으로도 몇 번 들어가보면서 깨달은 게 있다. 면접관이 듣고 싶은 건 "정답"이 아니다. 어떤 경로를 거쳐서 그 답에 도달했는지, 그 사고 과정을 보고 싶어한다.

"React에서 state가 바뀌면 어떤 일이 일어나나요?"#

이 질문을 받으면 대부분 이렇게 시작한다. "state가 바뀌면 리렌더링이 일어납니다." 틀린 말은 아닌데, 여기서 멈추면 아무 인상도 남기지 못한다. 면접관은 이미 알고 있는 사실을 확인하려고 이 질문을 하는 게 아니다.

붙는 답변은 흐름을 따라간다. setState가 호출되면 React는 즉시 렌더링을 하지 않는다. 업데이트를 큐에 넣고 batching 한다. React 18부터는 이벤트 핸들러 바깥의 setTimeout이나 fetch 콜백 안에서도 automatic batching이 된다는 점까지 말할 수 있으면 좋다.

jsx
// React 18 이전: setTimeout 안의 setState는 각각 리렌더링을 트리거
setTimeout(() => {
  setCount(1);   // 리렌더링
  setFlag(true); // 리렌더링
}, 1000);

// React 18 이후: 자동으로 batching되어 한 번만 리렌더링
setTimeout(() => {
  setCount(1);   // batched
  setFlag(true); // batched → 리렌더링 1회
}, 1000);

그 다음은 reconciliation이다. React가 render phase에서 새로운 virtual DOM 트리를 만들고, 이전 트리와 비교(diffing)해서 실제로 바뀐 부분만 찾는다. 이때 fiber 아키텍처 덕분에 이 작업을 잘게 쪼개서 할 수 있고, 우선순위가 높은 업데이트를 먼저 처리할 수 있다는 점까지 이어가면 면접관의 눈빛이 달라진다.

핵심은 단계를 구분해서 설명하는 것이다. "state 변경 → batching → render phase(virtual DOM 생성) → diffing → commit phase(실제 DOM 업데이트)" 이 흐름을 자기 말로 풀어낼 수 있으면, 면접관은 "아 이 사람은 내부 동작을 이해하고 있구나"라고 판단한다.

한 가지 팁. 여기서 "저는 이 부분을 이런 상황에서 체감했는데요"라고 실무 경험을 연결하면 완전히 다른 차원의 답변이 된다. 예를 들어 "리스트를 렌더링하는데 key를 index로 줬다가 순서가 바뀔 때 input 값이 꼬이는 버그를 겪었어요. diffing에서 key를 기준으로 비교하니까 당연한 결과였죠"라는 식으로.

클로저, 이번에는 제대로#

내가 망했던 그 질문. 지금이라면 이렇게 답할 것 같다.

정의부터 시작하되 한 문장으로 끝내고, 바로 실무 맥락으로 넘어간다. "함수가 자신이 생성될 때의 스코프를 기억하는 것"이면 충분하다. 면접관은 여기서 더 긴 정의를 원하지 않는다.

그 다음 debounce를 예로 드는 게 효과적이다.

js
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

이 코드에서 반환된 함수는 timerfn에 계속 접근할 수 있다. debounce의 실행은 이미 끝났는데도. 이게 클로저다. 여기까지 하면 "아 이 사람 실무에서 써봤구나"라는 인상을 준다.

그런데 진짜 차별화되는 건 클로저가 문제를 일으키는 경우를 말할 수 있을 때다. React에서 흔히 겪는 stale closure 문제.

jsx
function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // 항상 0 — stale closure
      setCount(count + 1); // 항상 1로만 설정됨
    }, 1000);
    return () => clearInterval(id);
  }, []); // 빈 의존성 배열이 원인

  return <div>{count}</div>;
}

의존성 배열이 비어 있으면 effect 안의 함수는 첫 렌더링 시점의 count를 캡처한 채로 계속 실행된다. 이게 클로저 때문에 생기는 버그라는 걸 설명하고, 해결법(setCount(prev => prev + 1) 같은 함수형 업데이트)까지 이어가면 된다.

면접에서 이런 식으로 답했을 때 면접관이 "정확해요"라고 하면서 자기 경험담을 꺼낸 적이 있다. 질문이 대화로 바뀌는 순간이 오면, 그 면접은 거의 성공이다.

"브라우저에 URL을 입력하면 어떤 일이 일어나나요?"#

이 질문은 범위가 넓다. DNS 조회, TCP 연결, TLS 핸드셰이크, HTTP 요청, 서버 응답, HTML 파싱, CSSOM 구성, 렌더 트리, 레이아웃, 페인트... 전부 나열할 수 있으면 대단하긴 한데, 면접관이 진짜 보는 건 어디에 깊이를 두느냐다.

떨어지는 답변은 위키피디아식으로 전 과정을 얇게 훑는다. 붙는 답변은 전체 흐름을 간략히 잡은 뒤, 자기가 잘 아는 영역에서 깊이 들어간다.

프론트엔드 개발자라면 브라우저 렌더링 파이프라인에서 깊이를 보여주는 게 자연스럽다. HTML을 파싱해서 DOM을 만드는 과정에서 <script> 태그를 만나면 파싱이 멈춘다는 것, 그래서 deferasync가 존재한다는 것. CSSOM이 완성되기 전에는 렌더링이 block된다는 것. 이런 지점에서 구체적인 경험을 덧붙이면 된다.

나는 이 질문에 답할 때 critical rendering path를 중심으로 이야기하고, "그래서 저는 프로젝트에서 CSS를 <head>에, 스크립트를 <body> 하단에 배치하거나 defer를 쓰는 이유가 여기에 있다고 생각합니다"로 연결한다. 이론과 실무가 하나로 이어지는 느낌을 주는 거다.

면접관으로 들어갔을 때, 한 후보자가 이 질문에 "전체 흐름은 이런데요, 저는 특히 HTTP 캐싱 쪽을 깊이 봤습니다"라고 하면서 Cache-Control, ETag, stale-while-revalidate 전략을 자기 프로젝트 경험과 엮어서 설명한 적이 있다. 프론트엔드 면접인데 네트워크 캐싱을 깊게 파고든 거다. 솔직히 그 답이 가장 기억에 남았다. 남들과 다른 지점에서 깊이를 보여주면 그 자체가 차별점이 된다.

성능 최적화 — 숫자 없이 말하면 안 믿는다#

"성능 최적화 경험 있으세요?"

이 질문에 "React.memo 써봤습니다", "이미지를 lazy loading 했습니다" 정도로 끝나면 아쉽다. 면접관은 속으로 이렇게 생각한다. "그래서 얼마나 좋아졌는데?"

작년에 회사에서 랜딩 페이지 Lighthouse Performance 점수가 42점이었던 적이 있다. LCP가 3.2초, hero 이미지가 1.8MB짜리 PNG로 CSS background-image로 박혀 있었다. 이걸 next/image로 교체하고 WebP 변환 + priority 속성을 넣은 것만으로 LCP가 1.4초로 떨어졌다. 그 다음 번들 분석해서 lodash 전체 import를 tree-shaking 가능한 개별 import로 바꾸고, 사용하지 않는 moment.js를 day.js로 교체했다. 최종적으로 LCP 0.9초, Performance 점수 91점.

면접에서는 이 흐름 그대로 말한다. 측정 → 원인 분석 → 조치 → 결과 측정. 이 사이클을 돌렸다는 걸 보여주는 게 핵심이다.

text
Before: Lighthouse Performance 42, LCP 3.2s, Bundle 1.2MB
After:  Lighthouse Performance 91, LCP 0.9s, Bundle 480KB

구체적인 수치가 있으면 면접관이 더 깊이 물어본다. "번들 분석은 어떤 도구로 했어요?" "CLS는 어떻게 잡았어요?" 이런 후속 질문이 오면 오히려 기회다. 그 질문 하나하나가 자신의 경험을 더 보여줄 수 있는 창구니까.

측정 없는 최적화는 최적화가 아니다. "느린 것 같아서 memo를 넣었다"는 면접에서 감점 요소에 가깝다. React DevTools Profiler를 켜서 어떤 컴포넌트가 불필요하게 리렌더링되는지 확인하고, 그 다음에 useMemoReact.memo를 적용했다는 식으로 말해야 한다. 도구를 알고, 그 도구로 문제를 진단한 뒤에 해결책을 적용했다는 과정이 중요하다.

"최근에 공부한 것 있으세요?"#

이 질문이 제일 가벼워 보이는데, 의외로 당락을 가르는 경우가 많다.

"TypeScript 공부하고 있습니다"나 "Next.js 문서 읽고 있어요"는 나쁘지 않지만 인상적이지도 않다. 거의 모든 프론트엔드 개발자가 하는 말이니까.

내가 면접관일 때 가장 좋았던 답변은 이거였다. 한 후보자가 "개인 블로그를 Next.js App Router로 만들면서 RSC(React Server Components)를 써봤는데, 클라이언트 컴포넌트랑 서버 컴포넌트의 경계를 어디서 나눠야 하는지가 생각보다 어려웠어요"라고 했다. 그러면서 자기가 겪은 구체적인 문제를 설명했다. 서버 컴포넌트에서 useState를 써서 에러가 나서 한참 헤맨 이야기, 결국 'use client' 지시어의 의미를 제대로 이해하게 된 과정.

이게 왜 좋은 답변이냐면, 세 가지를 동시에 증명하기 때문이다. 첫째, 실제로 코드를 작성했다. 둘째, 막혔을 때 스스로 해결하는 과정을 경험했다. 셋째, 그 경험에서 무엇을 배웠는지 정리되어 있다.

블로그도 같은 맥락이다. "블로그에 기술 글을 쓰고 있습니다"는 강력한 답변인데, 한 가지 조건이 있다. 진짜 글이 있어야 한다는 거다. 면접 끝나고 블로그 URL을 확인하는 면접관은 생각보다 많다. 글이 3개 있는데 전부 "Hello World" 수준이면 오히려 역효과다. 하나라도 제대로 깊이 파고든 글이 있으면 그게 면접 답변 10분보다 더 강한 증거가 된다.

면접을 대화로 바꾸는 기술#

여기까지 읽으면 패턴이 보일 거다. 잘 답변하는 사람들의 공통점은 "정의 → 실무 경험 → 문제 상황 → 배운 점"으로 이어지는 흐름을 갖고 있다는 것. 물론 매번 이 순서를 기계적으로 따르라는 건 아니다. 질문에 따라 유연하게 변형하되, 핵심은 **"나는 이걸 단순히 외운 게 아니라 실제로 부딪혀봤다"**는 인상을 주는 거다.

그리고 모르는 질문이 나왔을 때의 대처가 사실 가장 중요하다. 모르는 건 모른다고 하면 된다. 다만 "잘 모르겠습니다"로 끝내지 말고, "정확히는 모르겠는데, 제가 아는 범위에서 추론해보면..."이라고 사고 과정을 보여주면 전혀 다른 평가를 받는다. 면접관으로 앉아봐서 아는데, 모르는 걸 인정하면서도 논리적으로 접근하는 후보자에게는 오히려 가산점을 주고 싶어진다.

내가 두 번째 이직 면접에서 합격한 건, 모든 질문에 완벽하게 대답해서가 아니었다. 웹 접근성 관련 질문에서 ARIA 속성을 정확히 몰라서 버벅거렸는데, "이 부분은 깊이 다뤄본 적이 없어서 정확하지 않은데, 시맨틱 HTML을 우선 쓰고 부족한 부분을 ARIA로 보완한다는 원칙 정도만 알고 있습니다"라고 했다. 면접이 끝나고 합격 통보를 받을 때, 면접관이었던 분이 "모르는 것과 아는 것의 경계를 명확히 구분하시는 게 좋았어요"라고 피드백을 줬다.

기술 면접은 시험이 아니다. 내가 어떤 개발자인지를 보여주는 대화다. 정의를 많이 외우는 것보다, 하나를 깊이 경험하고 그걸 자기 말로 풀어내는 연습이 훨씬 효과적이다. 지금 면접을 준비하고 있다면, 가장 먼저 할 일은 질문 리스트를 100개 모으는 게 아니라 자기가 작업한 코드 중에서 이야기가 되는 것 5개를 골라 정리하는 거다.