JavaScript 기본기
1. 클로저(Closure)에 대해 설명해주세요.
답변: 클로저는 함수가 선언될 당시의 렉시컬 환경을 기억하여, 함수가 생성된 스코프 외부에서 실행되더라도 그 스코프의 변수에 접근할 수 있는 것을 말합니다. 쉽게 말해 외부 함수의 변수를 내부 함수가 참조하고 있을 때, 외부 함수가 종료된 후에도 내부 함수가 외부 함수의 변수에 접근할 수 있는 것입니다.
실무에서는 데이터 은닉과 캡슐화에 주로 활용합니다. 예를 들어 카운터를 만들 때, count 변수를 외부에서 직접 접근하지 못하게 하고 increase, decrease 같은 메서드로만 접근하게 할 수 있습니다. 또한 이벤트 핸들러에서 특정 상태를 유지해야 할 때도 클로저를 활용합니다.
2. this 바인딩에 대해 설명해주세요.
답변: JavaScript에서 this는 함수가 호출되는 방식에 따라 동적으로 결정됩니다. 네 가지 바인딩 규칙이 있습니다.
첫 번째는 기본 바인딩으로, 일반 함수 호출 시 this는 전역 객체를 가리킵니다. strict mode에서는 undefined가 됩니다.
두 번째는 암시적 바인딩으로, 객체의 메서드로 호출되면 this는 그 객체를 가리킵니다. 하지만 메서드를 변수에 할당하면 참조를 잃어버려 기본 바인딩이 적용됩니다.
세 번째는 명시적 바인딩으로, call, apply, bind 메서드로 this를 직접 지정할 수 있습니다.
네 번째는 new 바인딩으로, 생성자 함수를 new와 함께 호출하면 this는 새로 생성된 객체를 가리킵니다.
화살표 함수는 자체적인 this를 가지지 않고, 상위 스코프의 this를 그대로 사용한다는 점도 중요합니다. 우선순위는 new 바인딩, 명시적 바인딩, 암시적 바인딩, 기본 바인딩 순입니다.
3. 프로토타입 체인에 대해 설명해주세요.
답변: JavaScript는 프로토타입 기반 언어로, 모든 객체는 자신의 프로토타입을 참조하는 내부 링크를 가지고 있습니다. 객체에서 특정 속성이나 메서드를 찾을 때, 해당 객체에 없으면 프로토타입을 따라 올라가면서 찾습니다. 이렇게 프로토타입이 체인처럼 연결된 것을 프로토타입 체인이라고 합니다.
예를 들어 배열에서 push 메서드를 호출하면, 배열 객체 자체에는 없지만 Array.prototype에 있는 push를 찾아서 실행합니다. Array.prototype에도 없으면 Object.prototype까지 올라가고, 여기에도 없으면 undefined를 반환합니다.
ES6 클래스 문법도 내부적으로는 프로토타입을 사용합니다. extends로 상속받으면 자식 클래스의 프로토타입이 부모 클래스의 프로토타입을 가리키게 됩니다.
4. 실행 컨텍스트와 호이스팅을 설명해주세요.
답변: 실행 컨텍스트는 코드가 실행되는 환경을 추상화한 개념입니다. 변수 환경, 렉시컬 환경, this 바인딩 정보를 포함하고 있습니다. 함수가 호출되면 새로운 실행 컨텍스트가 생성되고 콜 스택에 쌓입니다.
호이스팅은 변수와 함수 선언이 스코프 최상단으로 끌어올려지는 것처럼 동작하는 현상입니다. 실제로 코드가 이동하는 것은 아니고, 실행 컨텍스트 생성 단계에서 변수와 함수 선언을 메모리에 먼저 등록하기 때문에 발생합니다.
var로 선언한 변수는 호이스팅 시 undefined로 초기화되어 선언 전에 접근해도 에러가 발생하지 않습니다. 반면 let과 const는 TDZ에 들어가서 선언 전에 접근하면 ReferenceError가 발생합니다. 함수 선언문은 전체가 호이스팅되지만, 함수 표현식은 변수 호이스팅 규칙을 따릅니다.
실무에서는 for문에서 var를 사용하면 클로저 문제가 발생할 수 있어서 let을 사용해야 합니다.
5. 이벤트 루프와 태스크 큐에 대해 설명해주세요.
답변: JavaScript는 싱글 스레드 언어지만 비동기 처리가 가능한데, 이것이 이벤트 루프 덕분입니다.
실행 구조는 콜 스택, 태스크 큐, 마이크로태스크 큐로 나뉩니다. 콜 스택에서 동기 코드가 모두 실행되면, 이벤트 루프가 마이크로태스크 큐를 먼저 확인해서 모두 비웁니다. 그 다음 태스크 큐에서 하나만 가져와 실행하고, 다시 마이크로태스크 큐를 확인하는 과정을 반복합니다.
마이크로태스크 큐에는 Promise의 then, catch, finally 콜백이나 MutationObserver가 들어갑니다. 태스크 큐에는 setTimeout, setInterval, I/O 작업이 들어갑니다.
따라서 setTimeout의 콜백보다 Promise의 then이 항상 먼저 실행됩니다. 동기 코드, 마이크로태스크, 태스크 순서로 실행된다고 기억하면 됩니다.
6. Promise와 async/await의 차이점을 설명해주세요.
답변: Promise는 비동기 작업의 완료 또는 실패를 나타내는 객체입니다. then 체이닝으로 비동기 작업을 연결할 수 있고, catch로 에러 처리를 할 수 있습니다.
async/await는 Promise를 더 동기 코드처럼 작성할 수 있게 해주는 문법적 설탕입니다. async 함수는 항상 Promise를 반환하고, await는 Promise가 처리될 때까지 기다립니다.
차이점은 가독성입니다. Promise 체이닝은 then이 여러 개 연결되면 복잡해지지만, async/await는 동기 코드처럼 읽기 쉽습니다. 에러 처리도 try-catch를 사용할 수 있어 더 직관적입니다.
성능 측면에서 중요한 것은 병렬 처리입니다. await를 순차적으로 사용하면 느리므로, Promise.all을 사용해 병렬로 처리하는 것이 좋습니다. Promise.allSettled는 일부가 실패해도 모든 결과를 받을 수 있고, Promise.race는 가장 빠른 하나만 받을 수 있습니다.
7. 얕은 복사와 깊은 복사의 차이를 설명해주세요.
답변: 얕은 복사는 객체의 최상위 속성만 복사하고, 중첩된 객체는 참조만 복사합니다. 스프레드 연산자나 Object.assign이 얕은 복사를 수행합니다. 따라서 중첩된 객체를 수정하면 원본도 영향을 받습니다.
깊은 복사는 중첩된 모든 객체를 새롭게 생성해서 완전히 독립적인 복사본을 만듭니다. 가장 간단한 방법은 JSON.stringify와 JSON.parse를 사용하는 것인데, 함수나 undefined, Symbol 같은 값은 손실됩니다.
최신 브라우저에서는 structuredClone을 사용할 수 있고, lodash의 cloneDeep도 많이 사용됩니다. 또는 재귀 함수로 직접 구현할 수도 있습니다.
실무에서는 불변성 유지가 중요한데, 특히 React에서 상태를 업데이트할 때 새 객체를 만들어야 React가 변경을 감지할 수 있습니다. immer 같은 라이브러리를 사용하면 불변성 관리가 더 쉬워집니다.
React 핵심
8. Virtual DOM과 Reconciliation 과정을 설명해주세요.
답변: Virtual DOM은 실제 DOM의 가벼운 복사본으로, JavaScript 객체로 표현됩니다. React는 상태가 변경되면 새로운 Virtual DOM을 만들고, 이전 Virtual DOM과 비교해서 변경된 부분만 실제 DOM에 반영합니다. 이 과정을 Reconciliation이라고 합니다.
핵심은 Diffing 알고리즘입니다. 같은 위치의 요소를 비교할 때, 타입이 다르면 전체를 새로 만들고, 타입이 같으면 속성만 업데이트합니다. 리스트를 렌더링할 때는 key를 통해 각 요소를 추적합니다.
React 16부터 도입된 Fiber 아키텍처는 이 과정을 더 효율적으로 만들었습니다. 작업을 작은 단위로 나누어 우선순위를 정하고, 필요하면 작업을 중단했다가 재개할 수 있습니다. 이를 통해 사용자 인터랙션 같은 긴급한 작업을 먼저 처리할 수 있습니다.
key를 올바르게 사용하는 것이 중요한데, index를 key로 사용하면 리스트 순서가 바뀔 때 문제가 발생할 수 있습니다. 고유한 id를 사용하는 것이 좋습니다.
9. useState의 동작 원리와 배치 업데이트를 설명해주세요.
답변: useState는 클로저를 활용해서 구현되어 있습니다. React는 각 컴포넌트의 Fiber 노드에 hooks 배열을 저장하고, useState 호출 순서대로 상태를 관리합니다. 그래서 조건문 안에서 hook을 사용하면 안 됩니다.
상태를 업데이트할 때 중요한 점은 React가 여러 상태 업데이트를 모아서 한 번에 처리한다는 것입니다. 이를 배치 업데이트라고 합니다. React 18부터는 이벤트 핸들러뿐만 아니라 setTimeout이나 Promise 안에서도 자동으로 배치됩니다.
함수형 업데이트를 사용하면 이전 상태를 기반으로 업데이트할 수 있습니다. 같은 이벤트 핸들러에서 여러 번 상태를 업데이트할 때, 일반적인 방법은 같은 값으로 설정되지만, 함수형 업데이트를 사용하면 순차적으로 적용됩니다.
객체나 배열 상태를 업데이트할 때는 불변성을 지켜야 합니다. 직접 수정하면 React가 변경을 감지하지 못하므로, 스프레드 연산자로 새 객체나 배열을 만들어야 합니다.
10. useEffect의 의존성 배열과 cleanup 함수를 설명해주세요.
답변: useEffect는 컴포넌트가 렌더링된 후에 실행되는 side effect를 처리합니다. 의존성 배열에 따라 실행 시점이 달라지는데, 배열을 생략하면 매 렌더링마다 실행되고, 빈 배열이면 마운트 시에만 실행되며, 특정 값을 넣으면 그 값이 변경될 때 실행됩니다.
cleanup 함수는 useEffect가 반환하는 함수로, 컴포넌트가 언마운트되거나 다음 effect가 실행되기 전에 호출됩니다. 타이머를 정리하거나 이벤트 리스너를 제거하는 등 메모리 누수를 방지하는 데 사용합니다.
실무에서 API 호출 시 주의할 점이 있습니다. 컴포넌트가 언마운트되었는데 응답이 와서 상태를 업데이트하려고 하면 에러가 발생할 수 있습니다. 이를 방지하려면 cleanup 함수에서 플래그를 설정하거나, AbortController를 사용해서 요청을 취소해야 합니다.
의존성 배열에 모든 외부 값을 포함해야 하는데, 함수나 객체를 의존성으로 넣으면 매번 새로 생성되어 불필요한 실행이 발생할 수 있습니다. 이럴 때는 useCallback이나 useMemo를 사용합니다.
11. useMemo와 useCallback의 차이와 사용 시기를 설명해주세요.
답변: useMemo는 계산 비용이 큰 값을 메모이제이션하고, useCallback은 함수 자체를 메모이제이션합니다.
useMemo는 의존성이 변경되지 않으면 이전에 계산한 값을 재사용합니다. 복잡한 연산이나 대량의 데이터 필터링, 정렬 같은 작업에 사용합니다.
useCallback은 함수를 메모이제이션해서 같은 참조를 유지합니다. 주로 자식 컴포넌트에 props로 전달하는 함수에 사용하는데, 자식이 React.memo로 최적화되어 있을 때 효과적입니다.
하지만 과도하게 사용하면 오히려 성능이 떨어질 수 있습니다. 메모이제이션 자체도 비용이 들고, 의존성 배열을 비교하는 비용도 있기 때문입니다. 실제로 성능 문제가 발생했을 때, 프로파일링을 통해 확인한 후 최적화하는 것이 좋습니다.
간단한 계산이나 원시 타입 값은 메모이제이션할 필요가 없습니다. 컴포넌트가 자주 리렌더링되고, 계산이 무겁거나 자식 컴포넌트 리렌더링을 방지해야 할 때 사용합니다.
12. React.memo와 컴포넌트 리렌더링 최적화를 설명해주세요.
답변: React.memo는 함수형 컴포넌트를 메모이제이션해서, props가 변경되지 않으면 리렌더링을 건너뛰게 합니다. 기본적으로 얕은 비교를 하고, 두 번째 인자로 비교 함수를 전달해서 커스텀 비교 로직을 구현할 수도 있습니다.
클래스 컴포넌트에서는 PureComponent나 shouldComponentUpdate를 사용합니다.
하지만 React.memo를 사용해도 props로 전달되는 함수나 객체가 매번 새로 생성되면 효과가 없습니다. 인라인 함수나 객체 리터럴을 props로 전달하면 매번 새로운 참조가 생성되기 때문입니다. 이럴 때 useCallback과 useMemo를 함께 사용해야 합니다.
리렌더링이 발생하는 경우는 세 가지입니다. 첫째, 자신의 state가 변경되었을 때, 둘째, 부모 컴포넌트가 리렌더링될 때, 셋째, Context 값이 변경되었을 때입니다.
최적화 전략으로는 상태를 최대한 가까운 곳에 두고, 상태를 분리해서 불필요한 리렌더링을 방지하며, 무거운 컴포넌트는 React.memo로 감싸고, Context는 적절히 분리해서 사용하는 것이 좋습니다.
13. Custom Hook 작성 패턴을 설명해주세요.
답변: Custom Hook은 반복되는 로직을 재사용 가능한 함수로 추출한 것입니다. 이름은 use로 시작해야 하고, 내부에서 다른 Hook을 사용할 수 있습니다.
자주 만드는 패턴으로는 로컬 스토리지 동기화 Hook이 있습니다. 초기값을 로컬 스토리지에서 가져오고, 값이 변경될 때마다 로컬 스토리지에 저장합니다.
API 호출을 추상화한 Hook도 유용합니다. loading, data, error 상태를 관리하고, AbortController로 cleanup을 처리합니다. URL이 변경되면 자동으로 재요청하게 할 수도 있습니다.
디바운싱 Hook은 검색 기능에서 많이 사용합니다. 입력이 멈춘 후 일정 시간이 지나야 값을 업데이트해서, API 호출 횟수를 줄일 수 있습니다.
무한 스크롤을 위한 Intersection Observer Hook도 자주 만듭니다. ref를 반환해서 요소에 연결하면, 해당 요소가 화면에 보일 때를 감지할 수 있습니다.
Custom Hook의 장점은 로직을 UI에서 분리할 수 있고, 테스트하기 쉬우며, 여러 컴포넌트에서 재사용할 수 있다는 것입니다.
14. React 18의 주요 변경사항을 설명해주세요.
답변: React 18의 가장 큰 변화는 Concurrent Rendering입니다. 렌더링을 중단하고 재개할 수 있어서, 긴급한 업데이트와 긴급하지 않은 업데이트를 구분할 수 있습니다.
Automatic Batching은 모든 상황에서 상태 업데이트를 자동으로 배치합니다. 이전에는 이벤트 핸들러 안에서만 배치되었는데, 이제는 setTimeout이나 Promise 안에서도 배치됩니다.
Transitions API는 startTransition 함수로 긴급하지 않은 업데이트를 표시할 수 있습니다. 예를 들어 검색창에 입력은 즉시 반영하고, 검색 결과는 나중에 업데이트할 수 있습니다. isPending 상태로 로딩 UI도 보여줄 수 있습니다.
useDeferredValue는 Transition의 대안으로, 특정 값의 업데이트를 지연시킵니다. 긴급한 업데이트가 끝난 후에 지연된 값이 업데이트됩니다.
Suspense가 개선되어 데이터 페칭과 함께 사용할 수 있게 되었고, Streaming SSR도 지원합니다.
useId Hook은 서버와 클라이언트에서 동일한 ID를 생성해서, 하이드레이션 에러를 방지합니다.
useSyncExternalStore는 외부 상태 관리 라이브러리가 React 18과 호환되도록 만드는 Hook입니다.
Next.js 핵심
15. Next.js의 렌더링 전략들을 설명하고 각각 언제 사용해야 하는지 말씀해주세요.
답변: Next.js는 네 가지 렌더링 전략을 제공합니다.
CSR은 클라이언트 사이드 렌더링으로, 브라우저에서 JavaScript로 렌더링합니다. 초기 로딩은 느리지만 페이지 전환이 빠르고, SEO에는 불리합니다. 대시보드나 어드민 페이지처럼 SEO가 필요 없는 경우에 사용합니다.
SSG는 빌드 시에 HTML을 미리 생성합니다. 가장 빠르고 CDN 캐싱이 가능해서, 마케팅 페이지나 블로그, 문서 사이트에 적합합니다. getStaticProps로 데이터를 가져오고, 동적 라우트는 getStaticPaths로 경로를 생성합니다.
ISR은 SSG에 주기적인 재생성을 추가한 것입니다. revalidate 옵션으로 재생성 주기를 설정하면, 오래된 페이지를 백그라운드에서 새로 생성합니다. 전자상거래 상품 페이지처럼 자주 변하지만 실시간까지는 필요 없는 경우에 사용합니다.
SSR은 매 요청마다 서버에서 HTML을 생성합니다. getServerSideProps로 데이터를 가져오고, 개인화된 데이터나 실시간 데이터가 필요한 경우에 사용합니다. 단점은 서버 부하가 크고 TTFB가 길다는 것입니다.
선택 기준은 데이터의 변경 빈도와 개인화 필요성입니다. 정적이면 SSG, 가끔 변하면 ISR, 자주 변하거나 사용자별로 다르면 SSR을 사용합니다.
16. Next.js App Router의 주요 개념을 설명해주세요.
답변: Next.js 13부터 도입된 App Router는 기존 Pages Router와 완전히 다른 구조입니다.
가장 큰 변화는 Server Components가 기본이라는 것입니다. 컴포넌트가 서버에서 렌더링되고, JavaScript 번들에 포함되지 않아서 성능이 향상됩니다. 클라이언트 컴포넌트가 필요하면 use client 지시어를 추가합니다.
파일 기반 라우팅 규칙도 바뀌었습니다. page.tsx가 실제 페이지가 되고, layout.tsx는 공통 레이아웃을 정의합니다. loading.tsx는 자동으로 Suspense 경계를 만들고, error.tsx는 에러 바운더리를 만듭니다.
라우트 그룹을 사용하면 URL 구조에 영향을 주지 않고 폴더를 구조화할 수 있습니다. 괄호로 감싸면 됩니다.
Streaming과 Suspense가 통합되어, 페이지의 일부가 준비되는 대로 점진적으로 보여줄 수 있습니다. 전체 페이지가 준비될 때까지 기다릴 필요가 없습니다.
Parallel Routes로 같은 레이아웃에 여러 페이지를 동시에 렌더링할 수 있고, Intercepting Routes로 모달 같은 UI를 구현할 수 있습니다.
API Routes도 Route Handlers로 바뀌었습니다. GET, POST 같은 HTTP 메서드를 함수로 export하면 됩니다.
17. Server Components와 Client Components의 차이를 설명해주세요.
답변: Server Components는 서버에서만 렌더링되는 컴포넌트입니다. 데이터베이스에 직접 접근하거나 파일 시스템을 읽을 수 있고, 민감한 정보를 안전하게 다룰 수 있습니다. JavaScript 번들에 포함되지 않아서 번들 크기가 줄어듭니다.
Client Components는 브라우저에서 실행되는 기존 React 컴포넌트와 같습니다. useState, useEffect 같은 Hook을 사용할 수 있고, 이벤트 핸들러를 등록할 수 있습니다. use client 지시어로 명시합니다.
중요한 규칙이 있습니다. Server Component는 Client Component를 import할 수 있지만, Client Component는 Server Component를 직접 import할 수 없습니다. 대신 children으로 전달받을 수는 있습니다.
데이터 페칭은 Server Component에서 하는 것이 좋습니다. async/await를 직접 사용할 수 있고, 워터폴을 피하기 위해 Promise.all을 쓸 수 있습니다.
선택 기준은 간단합니다. 상태나 이벤트, 브라우저 API가 필요하면 Client Component를 사용하고, 그 외에는 Server Component를 사용합니다. 기본은 Server Component이고, 필요할 때만 Client Component로 전환합니다.
18. Next.js의 데이터 캐싱 전략을 설명해주세요.
답변: Next.js는 여러 레벨에서 캐싱을 수행합니다.
첫째, Request Memoization은 같은 요청을 자동으로 중복 제거합니다. 한 렌더링 주기 동안 같은 URL과 옵션으로 fetch를 여러 번 호출해도 한 번만 실행됩니다.
둘째, Data Cache는 fetch 요청 결과를 서버에 영구 저장합니다. 재배포해도 유지되고, revalidate나 revalidatePath로 무효화할 수 있습니다. cache 옵션으로 캐시를 끄거나 설정할 수 있습니다.
셋째, Full Route Cache는 빌드 시에 정적 렌더링된 라우트를 캐시합니다. SSG와 비슷하지만 자동으로 처리됩니다.
넷째, Router Cache는 클라이언트 사이드에서 방문한 라우트를 메모리에 캐시합니다. 뒤로가기가 빠른 이유입니다.
revalidate 옵션으로 캐시 유지 시간을 설정할 수 있고, revalidateTag로 특정 태그의 캐시를 무효화할 수 있습니다. revalidatePath는 특정 경로의 캐시를 무효화합니다.
동적 함수를 사용하면 자동으로 동적 렌더링이 됩니다. cookies, headers, searchParams 같은 함수를 사용하면 요청 시마다 렌더링됩니다.
19. Next.js Middleware의 활용 사례를 설명해주세요.
답변: Middleware는 요청이 완료되기 전에 실행되는 코드로, 루트에 middleware.ts 파일을 만들면 됩니다.
가장 흔한 사용 사례는 인증 체크입니다. 쿠키에서 토큰을 확인하고, 없으면 로그인 페이지로 리다이렉트합니다. 반대로 로그인된 사용자가 로그인 페이지에 접근하면 대시보드로 보낼 수도 있습니다.
국제화도 Middleware에서 처리합니다. Accept-Language 헤더나 쿠키를 확인해서 적절한 locale로 리다이렉트할 수 있습니다.
A/B 테스팅도 구현할 수 있습니다. 랜덤이나 특정 조건으로 버킷을 나누고, 쿠키에 저장해서 일관된 경험을 제공합니다.
Rate Limiting으로 API 남용을 방지할 수도 있습니다. IP 주소별로 요청 횟수를 제한하고, 초과하면 429 에러를 반환합니다.
보안 헤더를 추가하는 것도 좋은 사용 사례입니다. X-Frame-Options, CSP 같은 헤더를 모든 응답에 추가할 수 있습니다.
봇 차단이나 특정 국가 차단도 Middleware에서 처리합니다. User-Agent나 지역 정보를 확인해서 접근을 제어합니다.
matcher 설정으로 특정 경로에만 Middleware를 적용할 수 있습니다. API routes나 static 파일은 제외하는 것이 일반적입니다.
20. Next.js의 이미지 최적화에 대해 설명해주세요.
답변: Next.js의 Image 컴포넌트는 자동으로 이미지를 최적화합니다.
첫째, 크기 최적화를 합니다. 디바이스 크기에 맞는 이미지를 자동으로 제공하고, srcset을 생성해서 반응형 이미지를 만듭니다. WebP나 AVIF 같은 최신 포맷으로 자동 변환합니다.
둘째, 레이아웃 시프트를 방지합니다. width와 height를 지정하면 공간을 미리 확보하고, placeholder로 블러 이미지를 보여줄 수 있습니다. fill 모드는 부모 요소에 맞춰서 크기를 조절합니다.
셋째, Lazy Loading이 기본입니다. 뷰포트에 들어올 때까지 로딩을 지연시켜서 초기 로딩 속도를 높입니다. priority 속성으로 중요한 이미지는 즉시 로드할 수 있습니다. LCP 이미지에는 priority를 사용해야 합니다.
넷째, 캐싱을 최적화합니다. 한 번 생성된 이미지는 캐시에 저장되어 재사용됩니다.
설정 파일에서 허용할 도메인을 지정해야 외부 이미지를 사용할 수 있습니다. remotePatterns로 패턴 매칭도 가능합니다.
Font 최적화도 중요한데, next/font를 사용하면 구글 폰트를 빌드 시에 다운로드해서 자체 호스팅합니다. FOUT를 방지하고, 프라이버시도 향상됩니다. display swap으로 폰트 로딩 전략을 설정할 수 있습니다.
면접에서는 이렇게 말로 설명하되, 필요하면 간단한 예시를 화이트보드에 그리거나 손으로 표현하면서 설명하면 좋습니다. 본인의 실제 프로젝트 경험을 덧붙이면 훨씬 더 설득력 있는 답변이 됩니다!