주요 업무에 WEB/APP 서비스 개발, React/Next.js 프론트엔드, Spring 백엔드, 서비스 도메인 모델링, DB 설계와 성능 최적화, 비즈니스 요구사항 분석, AI가 생성한 코드의 품질·보안·아키텍처 검토가 함께 적혀 있습니다.
따라서 답변은 다음 순서로 연결합니다.
기본 CS 원리 → 프론트엔드 구현 판단 → 서버/API/DB와의 연결 → 프로젝트에서 검증한 경험 → 한계
우선순위는 다음과 같습니다.
핵심 답변
실행 컨텍스트는 JavaScript 코드가 실행될 때 변수, 함수, this, 외부 렉시컬 환경 참조를 관리하는 실행 단위입니다. 전역 코드가 실행되면 전역 실행 컨텍스트가 만들어지고, 함수가 호출되면 함수 실행 컨텍스트가 콜 스택에 추가됩니다.
식별자를 찾을 때는 현재 렉시컬 환경에서 먼저 찾고, 없으면 함수가 선언된 위치의 외부 렉시컬 환경을 따라 올라갑니다. 이 연결이 스코프 체인입니다. JavaScript는 호출 위치가 아니라 선언 위치를 기준으로 상위 스코프가 결정되는 렉시컬 스코프 언어입니다.
꼬리 질문 대비
var는 평가 단계에서 undefined로 초기화되지만, let과 const는 초기화 전까지 TDZ에 있어 접근하면 오류가 발생합니다.핵심 답변
클로저는 함수가 선언될 때의 렉시컬 환경을 기억해, 외부 함수 실행이 끝난 뒤에도 해당 변수에 접근할 수 있는 특성입니다. 상태 은닉, 함수 팩토리, 이벤트 핸들러, 비동기 콜백에서 이전 값을 유지할 때 사용합니다.
Real World의 Refresh Token 대기 큐에서는 토큰 갱신 중 들어온 요청의 resolve, reject를 큐에 저장했습니다. 갱신이 끝난 뒤 저장된 함수를 호출해 대기 중이던 요청을 이어서 처리했습니다. 이때 각 Promise의 처리 함수가 생성 당시 상태와 연결되는 점이 클로저와 관련됩니다.
주의할 점
클로저가 값을 복사한다고 말하지 않습니다. 렉시컬 환경의 바인딩을 참조합니다. 또한 클로저 자체가 메모리 누수는 아니고, 더 이상 필요 없는 큰 객체를 참조하는 콜백이 이벤트 리스너나 타이머에 남아 있을 때 문제가 됩니다.
핵심 답변
JavaScript는 콜 스택에서 동기 코드를 처리합니다. 타이머, 네트워크, 사용자 이벤트 같은 비동기 작업은 브라우저 런타임에서 처리되고, 완료된 콜백은 큐에 들어갑니다.
현재 Task의 동기 코드가 끝나고 콜 스택이 비면, 이벤트 루프는 Microtask Queue를 먼저 비울 때까지 처리합니다. Promise then/catch/finally와 queueMicrotask가 Microtask이고, setTimeout, DOM 이벤트 콜백 등은 Task로 처리됩니다.
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
출력 순서는 start → end → promise → timeout입니다. Promise executor는 생성 즉시 동기로 실행되고, 후속 handler가 Microtask로 실행된다는 점을 구분해야 합니다.
핵심 답변
Promise는 비동기 작업의 대기, 성공, 실패 상태를 표현합니다. then은 새 Promise를 반환하므로 체이닝이 가능하고, 중간에서 throw하거나 rejected Promise를 반환하면 이후 catch로 전파됩니다.
async 함수는 항상 Promise를 반환합니다. await는 전체 스레드를 막는 것이 아니라 해당 async 함수의 나머지 실행을 중단하고, Promise가 처리되면 Microtask에서 이어서 실행합니다.
에러는 try/catch로 처리하되 복구할 수 없는 에러를 무조건 삼키면 안 됩니다. 병렬 요청이 서로 독립적이면 순차 await보다 먼저 Promise를 만들고 Promise.all 등을 사용합니다.
핵심 답변
React 업데이트는 render 단계와 commit 단계로 나눌 수 있습니다. Render 단계에서는 컴포넌트 함수를 실행해 React element를 만들고, 기존 Fiber와 비교해 필요한 변경을 계산합니다. 이 비교 과정이 Reconciliation입니다. Commit 단계에서는 계산된 변경을 실제 DOM에 반영하고 effect를 실행합니다.
컴포넌트 함수 실행, Fiber 비교, 실제 DOM 변경은 서로 다릅니다. UNIBUSK에서는 최적화 후 Profiler에서 일부 컴포넌트가 표시됐지만 console.count로는 함수 실행이 없었습니다. 이는 React가 이전 결과를 재사용한 bail-out으로 해석했습니다.
프로젝트 연결
공연 등록 폼에서 watch() 구독 위치가 상위 컴포넌트에 있어 입력 한 번에 넓은 트리가 영향을 받았습니다. useWatch()와 useFormState({ name })로 구독 범위를 좁혀 렌더 함수 실행 횟수를 75회에서 1회로 줄였습니다.
useEffect는 언제 사용하나요?핵심 답변
useEffect는 컴포넌트를 외부 시스템과 동기화할 때 사용합니다. 외부 시스템은 네트워크 연결, 구독, 타이머, 브라우저 API 같은 React 바깥의 상태를 의미합니다.
사용자 클릭으로 바로 실행되는 로직은 이벤트 핸들러에 두고, props와 state로 계산 가능한 값은 렌더 중 계산합니다. 의존성 배열은 실행하고 싶은 시점을 임의로 고르는 목록이 아니라 effect 내부에서 사용하는 반응형 값의 목록입니다.
프로젝트 연결
Real World에서는 컴포넌트 마운트 이후 useEffect에서 데이터를 패칭하던 구조를 Router loader로 옮겼습니다. 데이터 패칭은 항상 effect의 책임이 아니며, 라우터나 서버 계층에서 더 일찍 시작할 수 있으면 waterfall을 줄일 수 있습니다.
useMemo, useCallback, React.memo는 언제 사용하나요?핵심 답변
useMemo는 계산 결과를, useCallback은 함수 참조를, React.memo는 props가 같을 때 컴포넌트 렌더를 건너뛸 기회를 제공합니다. 비용이 큰 계산이 실제 병목이거나 memoized 자식에게 안정적인 참조를 전달해야 할 때 유용합니다.
하지만 습관적으로 적용하면 비교 비용, 의존성 관리, 코드 복잡성이 늘어납니다. UNIBUSK의 폼 문제는 props 참조가 아니라 상위 컴포넌트의 구독 위치가 원인이었으므로 React.memo()보다 구독 위치를 옮기는 것이 근본 해결책이었습니다.
핵심 답변
제어 컴포넌트는 입력값을 React state가 원본으로 관리하고 value와 onChange로 동기화합니다. 값에 따라 UI를 즉시 제어하기 쉽지만 입력마다 상태 업데이트와 렌더가 발생할 수 있습니다.
비제어 컴포넌트는 DOM이 입력값을 보관하고 필요할 때 ref나 Form API로 읽습니다. React Hook Form은 비제어 입력을 중심으로 효율적으로 동작하지만, watch()나 formState를 높은 위치에서 넓게 구독하면 불필요한 렌더가 생길 수 있습니다.
핵심 답변
서버 상태는 원본이 서버에 있고 비동기 조회, 캐싱, 최신성, 재요청, mutation 이후 동기화가 중요합니다. TanStack Query가 적합합니다.
클라이언트 상태는 모달 열림, 현재 스텝처럼 브라우저 안에서 의미가 있는 상태입니다. 한 컴포넌트에만 필요하면 useState, 여러 영역에서 공유하거나 언마운트 이후 유지해야 하면 Zustand 같은 store를 검토합니다.
폼 상태는 입력값, touched, dirty, field error와 제출 생명주기가 중요합니다. UNIBUSK 멀티 스텝 폼에서는 현재 입력은 RHF, 스텝 간 스냅샷은 Zustand, 서버 데이터는 TanStack Query로 나눴습니다.
핵심 답변
브라우저는 URL을 해석하고 캐시와 보안 정책을 확인한 뒤 DNS로 서버 IP를 찾습니다. TCP 연결을 맺고 HTTPS라면 TLS 핸드셰이크로 서버 인증과 암호화 키를 협상합니다. 이후 HTTP 요청을 보내고 응답을 받습니다.
HTML을 받으면 DOM을 만들고, CSS로 CSSOM을 만듭니다. DOM과 CSSOM을 바탕으로 Render Tree를 구성하고 Layout, Paint, Compositing을 거쳐 화면에 표시합니다. JavaScript는 로딩 방식에 따라 HTML 파싱을 차단하거나 DOM과 스타일 변경으로 렌더링 비용을 만들 수 있습니다.
핵심 답변
CORS는 브라우저가 다른 출처로 요청하는 것을 서버가 허용하는지의 문제입니다. Cookie는 어느 도메인에 저장되고 어떤 요청에 자동으로 포함되는지의 문제입니다.
요청이 CORS 정책상 허용되더라도 쿠키의 Domain, Path, SameSite, Secure, credentials 조건이 맞지 않으면 인증 상태가 이어지지 않을 수 있습니다.
프로젝트 연결
UNIBUSK Kakao OAuth2 문제에서는 단순 CORS 문제가 아니라 브라우저의 도메인별 쿠키 저장 정책이 핵심이었습니다. 백엔드 도메인에 저장된 쿠키를 프론트엔드 도메인에서 자신의 쿠키처럼 사용할 수 없었기 때문에, Proxy와 Authorization Code 교환 방식으로 인증 흐름을 재구성했습니다.
핵심 답변
Access Token은 짧은 수명을 전제로 API 요청에 사용되는 토큰이고, Refresh Token은 더 긴 수명으로 Access Token을 재발급하는 데 사용됩니다. Refresh Token은 탈취 영향이 크므로 JavaScript에서 접근할 수 없는 HttpOnly Cookie에 두는 방식이 일반적으로 더 안전합니다.
Access Token을 메모리에 두면 XSS 노출 위험을 줄일 수 있지만 새로고침 시 유지가 어렵습니다. Local Storage에 두면 유지가 쉽지만 XSS에 취약합니다. 저장 위치는 보안 요구, 사용자 경험, 서버 구조를 함께 보고 판단해야 합니다.
프로젝트 연결
Real World에서는 여러 API가 동시에 401을 받았을 때 Refresh Token 요청이 4회 발생하던 문제를 Promise 대기 큐로 단일화했습니다. 첫 번째 요청만 갱신을 수행하고 나머지는 대기했다가 새 Access Token으로 재시도했습니다.
핵심 답변
XSS(Cross-Site Scripting)는 공격자가 악성 스크립트를 신뢰된 웹 페이지에 주입해, 다른 사용자의 브라우저에서 그 스크립트가 실행되도록 만드는 공격입니다. 게시글, 댓글, 프로필 소개처럼 사용자 입력이 그대로 HTML로 렌더링되는 지점이 주요 통로입니다. 스크립트가 실행되면 쿠키(HttpOnly가 아닌 경우) 탈취, 세션 하이재킹, 페이지 위변조가 가능해집니다.
방어는 입력이 아니라 출력 시점에 집중해야 합니다. 사용자 입력을 HTML로 렌더링할 때 이스케이프 처리를 하고, dangerouslySetInnerHTML이나 innerHTML처럼 원본 HTML을 그대로 꽂는 API는 꼭 필요한 경우에만 sanitize 라이브러리(DOMPurify 등)를 거쳐 사용해야 합니다. React는 JSX로 렌더링하는 값을 기본적으로 이스케이프하기 때문에 {value} 형태로만 출력하면 XSS 위험이 크게 줄어듭니다. 서버 쪽에서는 Content-Security-Policy 헤더로 허용된 출처의 스크립트만 실행되도록 제한하는 것도 보조 방어선이 됩니다.
CSRF(Cross-Site Request Forgery)는 사용자가 이미 로그인된 상태를 이용해, 공격자가 만든 페이지에서 사용자 브라우저가 대신 요청을 보내게 만드는 공격입니다. 브라우저는 쿠키 기반 인증에서 요청이 어디서 시작됐는지와 상관없이 해당 도메인의 쿠키를 자동으로 실어 보내기 때문에, 사용자가 악성 페이지를 열기만 해도 로그인된 서비스에 의도치 않은 요청(비밀번호 변경, 송금, 게시글 삭제 등)이 전송될 수 있습니다.
방어는 요청이 실제로 우리 서비스 페이지에서 시작됐는지를 서버가 검증하는 방식입니다. CSRF 토큰을 서버가 발급해 폼이나 요청 헤더에 담아 보내고, 서버는 세션과 토큰이 일치하는지 확인합니다. 쿠키에 SameSite=Strict 또는 Lax를 설정하면 다른 출처에서 시작된 요청에는 쿠키가 자동으로 실리지 않아 CSRF 위험을 크게 줄일 수 있습니다.
XSS와 CSRF의 핵심 차이
XSS는 공격자의 스크립트가 우리 사이트 안에서(브라우저 컨텍스트 안에서) 직접 실행되는 것이고, CSRF는 스크립트 실행 없이 브라우저가 자동으로 실어 보내는 쿠키를 이용해 사용자 대신 요청만 위조하는 것입니다. 그래서 방어 지점도 다릅니다. XSS는 "우리 페이지에 악성 코드가 실행되지 않게" 막는 것이고, CSRF는 "요청이 진짜 우리 페이지에서 온 것인지" 검증하는 것입니다.
프로젝트 연결
Real World와 UNIBUSK 모두 Refresh Token을 HttpOnly Cookie에 저장했는데, 이는 XSS로 스크립트가 실행되더라도 JavaScript에서 document.cookie로 토큰을 직접 읽어갈 수 없게 만드는 방어이기도 합니다. 다만 HttpOnly Cookie는 CSRF에는 취약할 수 있어, 실제로는 SameSite 옵션(예: Lax)을 함께 설정해 다른 출처에서의 자동 전송을 제한하는 방향으로 보완해야 한다고 이해하고 있습니다. 사용자 입력값(게시글 본문, 프로필 소개)은 별도의 HTML 렌더링 라이브러리를 쓰지 않고 텍스트로만 출력해 XSS 표면을 줄였습니다.
꼬리 질문 대비
SameSite=Strict와 SameSite=Lax의 차이: Strict는 다른 출처에서 시작된 요청에는 어떤 경우에도 쿠키를 보내지 않고, Lax는 사용자가 링크를 클릭해 이동하는 것 같은 최상위 GET 네비게이션에는 쿠키를 허용합니다.핵심 답변
SSR은 서버에서 HTML을 만들어 초기 응답으로 내려주는 방식입니다. 사용자는 초기 콘텐츠를 빨리 볼 수 있지만, 클라이언트 상호작용을 위해서는 hydration이 필요합니다.
RSC는 React Server Components로, 서버에서만 실행되는 컴포넌트를 통해 클라이언트 번들에 포함될 필요 없는 로직을 분리할 수 있습니다. DB 접근이나 서버 전용 로직을 클라이언트로 보내지 않는 장점이 있습니다.
Hydration은 서버에서 만들어진 HTML에 클라이언트 React가 이벤트 핸들러와 상태를 연결해 상호작용 가능하게 만드는 과정입니다. Hydration 비용이 크면 초기 상호작용이 늦어질 수 있습니다.
핵심 답변
Hydration Pattern은 클라이언트에서 발생하던 데이터 대기 시간을 서버의 TTFB 구간으로 옮기는 방식입니다. 요청 비용을 없애는 것이 아니라 요청 시점과 사용자 경험을 바꿉니다.
초기 데이터가 즉시 필요하고 SEO 가치가 있으며 사용자별 민감 데이터가 아닌 페이지에는 유리합니다. 반대로 인증 이후 개인화 데이터는 캐시 정책을 잘못 설정하면 다른 사용자에게 노출될 수 있어 조심해야 합니다.
프로젝트 연결
UNIBUSK 공연 목록과 상세 페이지에는 적용해 클라이언트 API 요청 2건을 0건으로 줄였고, Slow 3G 환경의 2초 이상 대기 시간을 제거했습니다. 인증 페이지에는 적용하지 않았습니다.
핵심 답변
Code Splitting은 필요한 시점에 코드를 나눠 로드하는 전략입니다. 적용 기준은 초기 화면에 필요하지 않은지, 조건부로 렌더링되는지, 해당 컴포넌트에서만 쓰는 무거운 의존성이 있는지, LCP 요소가 아닌지입니다.
검증은 번들 분석과 Network 탭으로 합니다. 초기 로드 시 분리한 청크가 요청되지 않는지, 실제 상호작용 이후 해당 청크가 요청되는지 확인합니다.
프로젝트 연결
UNIBUSK에서 RegisterModal, ProfileEditModal에는 적용했지만 KakaoMapView는 LCP 요소라 제외했습니다. barrel import를 경유한 dynamic import가 청크 분리를 방해해 실제 컴포넌트 파일 경로로 변경했습니다.
핵심 답변
먼저 사용자가 느끼는 증상을 구체화합니다. 초기 진입이 느린지, 입력 반응이 느린지, 화면 이동이 끊기는지에 따라 볼 지표가 다릅니다.
초기 로딩은 Network와 Performance 패널에서 TTFB, 리소스 waterfall, LCP를 확인합니다. 상호작용 지연은 Long Task와 INP 후보를 봅니다. React 내부 렌더링이 의심되면 Profiler로 영향을 받는 컴포넌트와 commit 시간을 확인합니다. 번들 비용은 번들 분석과 Coverage로 확인합니다.
가설을 세운 뒤 한 요소만 바꾸고 같은 빌드·네트워크·CPU 조건에서 전후를 비교합니다.
핵심 답변
REST API는 리소스를 중심으로 URI를 설계하고, HTTP Method로 행위를 표현합니다. GET은 조회, POST는 생성, PUT/PATCH는 수정, DELETE는 삭제에 사용합니다. 상태 코드는 성공, 클라이언트 오류, 서버 오류를 구분해 클라이언트가 적절히 대응할 수 있게 해야 합니다.
프론트엔드 입장에서는 API 계약이 UI 상태와 직접 연결됩니다. 로딩, 빈 상태, 에러, 권한 없음, 유효성 오류를 구분할 수 있도록 응답 구조와 에러 타입이 명확해야 합니다.
핵심 답변
데이터 간 관계가 명확하고 스키마 제약, 조인, 트랜잭션이 중요하면 관계형 데이터베이스가 적합합니다. Real World에서는 사용자, 게시글, 댓글처럼 관계와 일관성이 중요한 데이터가 있어 PostgreSQL과 Prisma를 사용했습니다.
한계도 있습니다. 스키마가 매우 유동적이거나 특정 대규모 분산 접근 패턴에서는 복잡한 조인과 수평 확장 비용이 부담이 될 수 있습니다. DB 선택은 익숙함보다 데이터 모델, 일관성 요구, 조회 패턴, 운영 조건을 함께 봐야 합니다.
핵심 답변
원자성은 묶인 작업이 전부 성공하거나 전부 실패해야 한다는 의미입니다. 일관성은 트랜잭션 전후에 제약 조건이 유지되는 것입니다. 격리성은 동시에 실행되는 트랜잭션이 서로의 중간 상태에 부적절하게 영향을 주지 않도록 하는 성질이고, 지속성은 커밋된 결과가 장애 후에도 유지되는 성질입니다.
프론트엔드도 트랜잭션을 이해해야 합니다. 등록, 결제, 인증처럼 일부만 성공하면 안 되는 요청에서는 중복 제출 방지, 재시도, 실패 복구 UX, 에러 메시지 설계가 서버의 일관성 규칙과 연결되기 때문입니다.
핵심 답변
실무 수준의 대규모 백엔드 도메인 모델링 경험이 있다고 과장하지 않습니다. 대신 Real World에서 NestJS 3계층 구조와 DI를 적용해 Controller, Service, Repository의 책임을 나눈 경험을 말합니다.
프론트엔드에서는 도메인 모델링을 UI 상태와 API 응답 구조에 반영했습니다. Dessert Front Dashboard에서 Query는 여러 기능에서 재사용되는 읽기 데이터로 보고 entities에 두고, Mutation은 특정 사용자 행동과 연결되는 쓰기 흐름으로 보고 features에 배치했습니다. 이처럼 데이터의 성격과 사용 범위를 기준으로 구조를 정했습니다.
핵심 답변
CI는 Continuous Integration, 즉 지속적 통합입니다. 개발자가 push하거나 PR을 만들 때 자동으로 의존성 설치, lint, test, build 같은 검증을 수행해 변경 사항이 기존 코드와 잘 통합되는지 확인합니다.
CD는 Continuous Delivery 또는 Continuous Deployment로 설명할 수 있습니다. Delivery는 배포 가능한 산출물을 만들고 승인 후 배포할 수 있는 상태까지 자동화하는 것이고, Deployment는 검증을 통과한 변경을 실제 운영 환경까지 자동 배포하는 의미로 쓰입니다.
면접에서는 경험 범위를 구분해서 말해야 합니다. BE-realworld에서는 GitHub Actions로 lint, unit test, e2e test, Prisma migration, coverage를 실행하는 CI는 구성했지만, production 환경으로 자동 배포되는 CD까지 운영했다고 말하지는 않겠습니다.
핵심 답변
GitHub Actions는 저장소의 .github/workflows 디렉터리에 있는 YAML 파일로 자동화 흐름을 정의합니다. workflow는 push, pull request, 수동 실행 같은 event에 의해 실행됩니다. workflow 안에는 하나 이상의 job이 있고, 각 job은 runner라는 실행 환경에서 동작합니다. job 안의 step은 shell command를 실행하거나 actions/checkout, actions/setup-node 같은 재사용 가능한 action을 실행합니다.
BE-realworld에서는 ci-mini.yml을 push와 pull request에서 실행되게 만들고, checkout → Node.js 20 설정 → npm ci → npm run lint 순서로 빠른 검증을 수행했습니다. 별도의 ci.yml은 수동 실행으로 두고, PostgreSQL service container, Prisma migration, lint, unit test, e2e test, coverage, Codecov 업로드까지 수행하게 했습니다.
꼬리 질문 대비
workflow: 자동화 전체 단위event: workflow를 실행시키는 조건job: runner에서 실행되는 작업 묶음step: job 안에서 실행되는 명령 또는 actionrunner: job을 실제로 실행하는 가상 머신 또는 서버secrets: DB URL, JWT secret처럼 코드에 노출하면 안 되는 값을 안전하게 주입하는 저장소 설정핵심 답변
첫째, 피드백 속도와 검증 강도의 균형을 봅니다. 모든 PR마다 무거운 E2E 테스트와 coverage까지 돌리면 안정성은 올라가지만 대기 시간이 길어집니다. 그래서 빠른 lint/type/test와 무거운 통합 검증을 나누는 전략을 사용할 수 있습니다.
둘째, 실제 서비스와 가까운 환경에서 검증해야 합니다. 백엔드 E2E 테스트는 DB가 필요하기 때문에 BE-realworld에서는 PostgreSQL service container를 띄우고, Prisma migration을 적용한 뒤 테스트를 실행하도록 구성했습니다.
셋째, 민감 정보 관리를 고려해야 합니다. DATABASE_URL, JWT_ACCESS_SECRET, JWT_REFRESH_SECRET 같은 값은 workflow 파일에 직접 적지 않고 GitHub Secrets로 주입해야 합니다.
마지막으로 CD까지 간다면 배포 대상 환경, rollback 전략, migration 실패 대응, 환경별 secrets, 승인 절차를 함께 설계해야 합니다.
핵심 답변
AI가 만든 코드는 바로 신뢰하지 않고 네 가지를 검토합니다.
AI에게는 반복 작업, 초안 작성, 대안 탐색을 위임할 수 있습니다. 하지만 최종 판단은 개발자가 해야 합니다. 특히 인증, 캐시, DB, 권한, 사용자 데이터가 얽힌 부분은 보안과 아키텍처 관점에서 직접 검토해야 합니다.
핵심 답변
동작 여부만 보지 않습니다. 먼저 문제를 해결하는 위치가 적절한지 봅니다. 예를 들어 React 렌더링 문제에서 AI가 React.memo()를 제안하더라도 원인이 상위 구독이면 해결 위치가 맞지 않습니다.
그다음 타입 안정성, 에러 처리, 테스트 가능성, 기존 코드 스타일, 경계 조건을 확인합니다. 마지막으로 실제 측정이나 테스트로 결과를 검증합니다. 성능 개선이라면 Lighthouse 점수 하나가 아니라 Network, Performance, React Profiler, console.count 등 도구가 검증한 범위를 구분해야 합니다.
핵심 답변
프로세스는 실행 중인 프로그램의 독립적인 실행 단위입니다. 각 프로세스는 코드, 데이터, 힙, 스택 같은 메모리 공간을 독립적으로 가집니다. 그래서 한 프로세스가 죽어도 다른 프로세스에 직접 영향을 덜 주지만, 프로세스 간 통신은 상대적으로 비용이 큽니다.
스레드는 프로세스 안에서 실행되는 더 작은 작업 단위입니다. 같은 프로세스의 스레드들은 메모리 공간을 공유하기 때문에 데이터 공유가 쉽고 생성 비용도 상대적으로 낮습니다. 대신 공유 자원에 동시에 접근하면 race condition이 생길 수 있어 lock, mutex 같은 동기화가 필요합니다.
면접에서는 "프로세스는 격리된 실행 단위, 스레드는 프로세스 내부의 실행 흐름이며 메모리를 공유한다"고 먼저 말하고, 안정성과 통신 비용의 trade-off를 덧붙이면 됩니다.
핵심 답변
동시성은 여러 작업을 같은 시간대에 다루는 구조입니다. 실제로 동시에 실행되지 않더라도 작업을 잘게 나눠 전환하면서 여러 작업이 진행되는 것처럼 처리할 수 있습니다. 단일 코어에서 이벤트 루프가 여러 I/O 작업을 다루는 것이 동시성의 예입니다.
병렬성은 여러 작업이 실제로 같은 시점에 실행되는 것입니다. 여러 CPU 코어나 여러 스레드가 각각 작업을 동시에 수행할 때 병렬성이 생깁니다.
Node.js 서버를 예로 들면, 이벤트 루프는 많은 I/O 요청을 동시성으로 처리하기 좋습니다. 하지만 CPU를 오래 점유하는 작업은 이벤트 루프를 막을 수 있으므로 worker thread, 별도 프로세스, job queue 등으로 분리하는 것을 검토해야 합니다.
핵심 답변
CPU-bound 작업은 계산 자체가 병목인 작업입니다. 이미지 변환, 암호화 연산, 대량 데이터 계산처럼 CPU를 오래 사용하는 작업이 여기에 가깝습니다. 이런 작업은 CPU 코어 수, 알고리즘 효율, 병렬 처리 전략이 중요합니다.
I/O-bound 작업은 네트워크, 파일 시스템, DB 응답처럼 외부 입출력을 기다리는 시간이 병목인 작업입니다. API 요청, DB 조회, 파일 읽기 같은 작업은 실제 CPU 계산보다 대기 시간이 더 클 수 있습니다.
웹 서버에서는 대부분 요청이 I/O-bound인 경우가 많기 때문에 비동기 I/O와 커넥션 풀을 잘 쓰는 것이 중요합니다. 반대로 CPU-bound 작업을 요청 처리 스레드나 이벤트 루프에서 오래 실행하면 다른 요청까지 지연될 수 있습니다.
핵심 답변
"Node.js는 싱글 스레드"라는 말은 JavaScript 코드가 기본적으로 하나의 메인 스레드 이벤트 루프에서 실행된다는 의미에 가깝습니다. 하지만 모든 일이 한 스레드에서만 처리된다는 뜻은 아닙니다.
Node.js는 이벤트 루프를 통해 비동기 작업의 완료 콜백을 처리하고, 가능한 I/O 작업은 운영체제 커널에 위임합니다. 파일 시스템, DNS, 일부 암호화 작업처럼 스레드가 필요한 작업은 libuv의 worker pool을 사용할 수 있습니다. 즉 메인 스레드는 요청마다 블로킹해서 기다리지 않고, 완료된 작업의 콜백을 큐에서 처리합니다.
중요한 한계는 메인 스레드에서 오래 걸리는 동기 계산을 하면 이벤트 루프가 막힌다는 점입니다. 그래서 CPU-bound 작업은 worker thread, 별도 서버, queue 기반 background job으로 분리하는 것이 좋습니다.
프로젝트 연결
Real World의 Refresh Token 대기 큐는 여러 401 요청을 동시에 다루되, 실제 Refresh 요청은 하나만 수행하도록 만든 동시성 제어 사례로 설명할 수 있습니다.
핵심 답변
커넥션 풀은 DB나 외부 서비스와의 연결을 미리 만들어 두고 재사용하는 구조입니다. 요청마다 DB 연결을 새로 만들면 TCP 연결, 인증, 세션 초기화 비용이 반복되고 응답 시간이 늘어납니다. 풀을 사용하면 기존 연결을 빌려 쓰고 반납하므로 비용을 줄일 수 있습니다.
또 하나의 목적은 DB 보호입니다. 애플리케이션 요청이 갑자기 늘어났을 때 무제한으로 DB 연결을 만들면 DB가 먼저 병목이 되거나 장애가 날 수 있습니다. 커넥션 풀은 동시에 DB에 접근하는 연결 수를 제한해 서버와 DB 사이의 압력을 조절합니다.
다만 풀 크기를 크게 잡는다고 항상 좋은 것은 아닙니다. DB가 처리할 수 있는 연결 수, 애플리케이션 인스턴스 수, 쿼리 시간, 트랜잭션 유지 시간을 함께 봐야 합니다.
핵심 답변
인덱스는 테이블 전체를 처음부터 끝까지 찾지 않고, 특정 컬럼 기준으로 빠르게 위치를 찾을 수 있게 돕는 자료구조입니다. 책의 목차나 색인처럼, 조건에 맞는 행을 찾기 위해 읽어야 하는 범위를 줄여줍니다.
하지만 항상 좋은 것은 아닙니다. 인덱스도 별도 저장 공간을 사용하고, INSERT, UPDATE, DELETE가 발생하면 인덱스도 함께 갱신해야 합니다. 그래서 쓰기 성능이 떨어질 수 있고, 너무 많은 인덱스는 오히려 운영 비용을 늘립니다.
인덱스는 조회 조건, 정렬 조건, 조인 조건, 데이터 분포도를 보고 설계해야 합니다. 예를 들어 BE-realworld의 Article은 작성자별 최신 글 조회를 고려해 authorId, createdAt 인덱스를 둘 수 있고, Comment는 게시글별 댓글 조회를 위해 articleId, createdAt 인덱스가 의미 있습니다.
핵심 답변
트랜잭션 격리 수준은 여러 트랜잭션이 동시에 실행될 때 서로의 중간 상태를 어디까지 볼 수 있는지 정하는 기준입니다. 격리 수준이 낮으면 동시 처리 성능은 좋아질 수 있지만, dirty read, non-repeatable read, phantom read 같은 이상 현상이 생길 수 있습니다.
반대로 격리 수준을 높이면 데이터 일관성은 강해지지만 lock 경합이 늘고 성능이 떨어질 수 있습니다. 그래서 모든 상황에서 가장 높은 격리 수준을 쓰는 것이 정답은 아닙니다.
면접에서는 "트랜잭션 격리 수준은 동시성과 일관성 사이의 trade-off를 조절하는 장치"라고 답하면 됩니다. 예를 들어 주문, 결제, 재고처럼 중간 상태가 노출되면 안 되는 영역은 더 강한 일관성이 필요하고, 단순 목록 조회는 성능과 최신성 요구를 함께 봐야 합니다.
핵심 답변
쿠키는 브라우저에 저장되고 요청 시 조건에 맞으면 자동으로 서버에 전송되는 저장 메커니즘입니다. 인증 방식 그 자체라기보다 값을 저장하고 전달하는 수단입니다.
세션은 서버가 로그인 상태를 저장하고, 클라이언트는 세션 ID만 쿠키로 들고 있는 방식입니다. 서버에서 상태를 관리하므로 강제 로그아웃이나 세션 무효화가 쉽지만, 서버 확장 시 세션 저장소를 공유해야 합니다.
JWT는 토큰 자체에 사용자 식별자와 만료 시간 같은 claim을 담고 서명하는 방식입니다. 서버가 매 요청마다 세션 저장소를 조회하지 않아도 토큰 검증이 가능하지만, 한 번 발급된 토큰을 즉시 무효화하기 어렵고 탈취 시 만료 전까지 위험할 수 있습니다.
프로젝트 답변에서는 Access Token은 짧게, Refresh Token은 더 조심스럽게 다뤄야 한다고 연결하면 좋습니다. BE-realworld에서는 Refresh Token을 DB에 원문이 아니라 해시로 저장하고, HttpOnly Cookie 옵션을 사용한 경험으로 설명할 수 있습니다.
핵심 답변
CORS는 브라우저의 Same-Origin Policy 때문에 발생합니다. 브라우저는 보안상 한 출처의 웹 페이지가 다른 출처의 리소스에 마음대로 접근하지 못하게 제한합니다. 다른 출처로 요청하려면 서버가 Access-Control-Allow-Origin 같은 응답 헤더로 해당 출처를 허용해야 합니다.
인증 정보가 포함된 요청이면 더 조심해야 합니다. 서버는 허용할 origin을 명시해야 하고, 클라이언트는 credentials 옵션을 맞춰야 하며, 쿠키의 SameSite, Secure, Domain 조건도 함께 맞아야 합니다.
CORS 에러가 보인다고 항상 서버 CORS 설정만 문제인 것은 아닙니다. UNIBUSK Kakao OAuth2 문제처럼 실제 원인은 쿠키가 저장된 도메인과 프론트엔드 도메인이 달라 인증 상태가 이어지지 않는 것일 수 있습니다. 그래서 요청 출처, 응답 헤더, 쿠키 저장 위치, credentials 설정을 단계별로 확인해야 합니다.
핵심 답변
Docker는 애플리케이션과 실행에 필요한 의존성, 설정을 이미지로 묶어 컨테이너에서 실행할 수 있게 해줍니다. 로컬, CI, 배포 환경의 Node.js 버전이나 OS 패키지 차이로 생기는 문제를 줄이고, 같은 이미지를 여러 환경에서 재현할 수 있게 하는 것이 핵심 장점입니다.
VM은 하드웨어를 가상화해 각 VM이 별도 OS를 가지는 방식에 가깝고, 컨테이너는 호스트 OS 커널을 공유하면서 프로세스를 격리합니다. 그래서 컨테이너는 일반적으로 더 가볍고 시작이 빠릅니다.
BE-realworld에서는 Dockerfile과 docker-compose를 통해 NestJS 서버와 PostgreSQL 환경을 맞추고, 배포 전 Docker 이미지 빌드와 실행을 검증하는 체크리스트를 정리했습니다. 면접에서는 "Docker를 배포 자체의 마법 도구로 본 것이 아니라, 실행 환경을 재현 가능하게 만들기 위한 도구로 이해했다"고 답하면 좋습니다.
핵심 답변
배열은 메모리상 연속된 공간에 데이터를 저장하는 구조입니다. 인덱스를 알고 있으면 arr[i]처럼 바로 접근할 수 있어 조회가 빠릅니다. 일반적으로 인덱스 접근은 O(1)입니다. 대신 중간에 값을 삽입하거나 삭제하면 뒤의 요소들을 이동해야 해서 비용이 커질 수 있습니다.
연결 리스트는 각 노드가 값과 다음 노드의 참조를 가지는 구조입니다. 중간 삽입과 삭제는 해당 위치의 노드를 알고 있다면 포인터만 바꾸면 되므로 유리할 수 있습니다. 하지만 특정 인덱스에 바로 접근할 수 없고, 앞에서부터 따라가야 하므로 탐색은 O(n)입니다.
실무에서는 JavaScript 배열을 가장 많이 쓰지만, 이 차이를 이해하면 "조회가 많은지", "중간 삽입/삭제가 많은지", "순차 접근이면 충분한지" 같은 선택 기준을 설명할 수 있습니다.
핵심 답변
Stack은 나중에 들어온 데이터가 먼저 나가는 LIFO 구조입니다. 함수 호출 스택, 실행 취소, 브라우저 뒤로 가기, DFS 같은 상황에 사용할 수 있습니다. 가장 최근 상태부터 되돌아가야 하는 문제에 잘 맞습니다.
Queue는 먼저 들어온 데이터가 먼저 나가는 FIFO 구조입니다. 작업 대기열, 메시지 큐, 이벤트 처리, BFS 같은 상황에 사용됩니다. 요청이나 작업을 들어온 순서대로 처리해야 할 때 적합합니다.
프로젝트와 연결하면, Real World의 Refresh Token 대기 큐는 토큰 갱신 중 들어온 요청들을 잠시 보관했다가 갱신이 끝난 뒤 이어서 처리하는 큐 성격의 구조로 설명할 수 있습니다.
핵심 답변
Hash Table은 key를 hash function에 넣어 배열의 index로 바꾸고, 그 위치에 값을 저장하는 자료구조입니다. 잘 설계되면 key 기반 조회, 삽입, 삭제가 평균적으로 O(1)에 가깝습니다.
하지만 서로 다른 key가 같은 index로 매핑되는 충돌이 생길 수 있습니다. 충돌 처리 방식에는 같은 버킷에 여러 값을 연결해 저장하는 chaining, 다른 빈 위치를 찾아 저장하는 open addressing 같은 방식이 있습니다.
실무에서는 JavaScript의 Map, Set, 객체 기반 lookup, id 기준 캐시에서 이 개념이 자주 등장합니다. 예를 들어 게시글 id를 key로 좋아요 수나 favorited 여부를 빠르게 찾고 싶을 때 배열을 매번 순회하기보다 map 형태로 바꾸면 조회 비용을 줄일 수 있습니다.
핵심 답변
JavaScript에서 Array는 순서가 있는 목록 데이터를 표현하기 위한 객체입니다. 숫자 index와 length를 중심으로 동작하고, map, filter, reduce, forEach 같은 배열 전용 메서드를 통해 순차 처리에 적합합니다.
Object는 key-value 형태의 속성 집합을 표현하기 위한 객체입니다. 특정 entity의 속성, API 응답 객체, 설정 값처럼 이름이 있는 필드를 표현할 때 자연스럽습니다.
중요한 점은 JavaScript Array도 객체라는 것입니다. 하지만 의미와 사용 목적이 다릅니다. 목록처럼 순서와 반복이 중요하면 Array를 쓰고, 이름 있는 속성으로 하나의 대상을 표현하거나 key 기반 접근이 중요하면 Object를 씁니다.
예를 들어 주문 목록은 Array가 자연스럽고, 주문 하나의 id, status, totalPrice 같은 필드는 Object가 자연스럽습니다. 배열을 객체처럼 임의 속성 저장소로 쓰거나, 객체를 배열처럼 순서 기반으로 다루면 의도가 흐려지고 유지보수가 어려워집니다.
핵심 답변
Object는 JavaScript의 기본 객체로, 문자열 또는 Symbol key를 중심으로 속성을 저장합니다. 일반적인 데이터 구조나 JSON 형태를 표현할 때 자연스럽습니다.
Map은 key-value 저장을 목적으로 만든 자료구조입니다. 문자열뿐 아니라 객체, 함수, 숫자 같은 다양한 값을 key로 사용할 수 있고, 삽입 순서를 보장합니다. 또한 size, has, get, set, delete 같은 명확한 API가 있어 동적인 key-value 컬렉션에 적합합니다.
면접에서는 "고정된 형태의 데이터 모델은 Object가 자연스럽고, 동적으로 key가 추가·삭제되거나 key 타입이 다양하고 lookup이 중요한 컬렉션은 Map이 적합하다"고 답하면 됩니다.
핵심 답변
Set은 중복 없는 값의 집합을 표현할 때 사용합니다. 어떤 값이 이미 있는지 빠르게 확인하거나, 배열에서 중복을 제거할 때 유용합니다.
예를 들어 태그 목록에서 중복 태그를 제거하거나, 선택된 id 목록을 관리하거나, 이미 처리한 요청 id를 기록할 때 사용할 수 있습니다. includes로 배열을 매번 순회하면 O(n)이지만, Set의 has는 평균적으로 O(1)에 가깝습니다.
다만 순서가 중요한 목록 UI나 중복이 의미 있는 데이터에는 Set이 적합하지 않을 수 있습니다. "중복 제거"와 "빠른 포함 여부 확인"이 핵심일 때 선택합니다.
핵심 답변
Tree는 계층 구조를 표현하는 자료구조입니다. 하나의 root에서 시작해 부모-자식 관계로 내려가며, 일반적으로 cycle이 없습니다. DOM 트리, 파일 시스템, 조직도, 댓글의 대댓글 구조, 라우팅 트리 같은 예시가 있습니다.
Graph는 정점과 간선으로 관계를 표현하는 구조입니다. 방향이 있을 수도 있고 없을 수도 있으며, cycle이 있을 수 있습니다. 지하철 노선, 지도 경로, 사용자 팔로우 관계, 패키지 의존성, 서비스 간 호출 관계처럼 복잡한 연결 관계를 표현할 때 사용합니다.
ItNew 맥락에서는 위치 기반 경로, 배송지 간 연결, 서비스 의존성, 사용자·행사·인증 데이터의 관계를 graph 관점으로도 생각해볼 수 있습니다. 다만 면접에서는 직접 구현 경험을 과장하지 말고, 어떤 문제를 표현하는 데 적합한지 중심으로 말하면 됩니다.
핵심 답변
BFS는 너비 우선 탐색입니다. 시작점에서 가까운 노드부터 단계별로 탐색하며, 보통 queue를 사용합니다. 가중치가 없는 그래프에서 최단 거리나 최소 단계 수를 찾을 때 적합합니다.
DFS는 깊이 우선 탐색입니다. 한 경로를 끝까지 들어간 뒤 되돌아오며, 재귀나 stack을 사용합니다. 모든 경로를 탐색하거나, 트리 구조를 순회하거나, 연결 여부를 확인할 때 사용할 수 있습니다.
면접에서는 "BFS는 가까운 것부터, DFS는 깊은 것부터"라고 시작하고, BFS는 queue, DFS는 stack 또는 재귀와 연결해서 설명하면 됩니다. 프론트엔드에서도 메뉴 트리, 라우트 트리, 댓글 트리, 의존성 탐색 같은 문제에 연결할 수 있습니다.
핵심 답변
시간 복잡도는 입력 크기가 커질 때 실행 시간이 얼마나 증가하는지 보는 기준입니다. 공간 복잡도는 입력 크기가 커질 때 추가 메모리를 얼마나 사용하는지 보는 기준입니다.
작은 데이터에서는 어떤 방식이든 빨라 보일 수 있지만, 데이터가 커지면 O(n^2)과 O(n log n), O(n)의 차이가 크게 드러납니다. 그래서 코드가 현재 데이터에서만 동작하는지, 데이터가 늘어나도 버틸 수 있는지 판단하려면 복잡도를 봐야 합니다.
실무에서는 무조건 가장 낮은 복잡도를 고르는 것이 아니라, 데이터 크기, 코드 가독성, 메모리 사용량, 실제 병목 여부를 함께 봅니다. 예를 들어 배열을 매번 순회해 id를 찾는 코드가 작을 때는 괜찮지만, 반복 렌더링이나 큰 목록에서 자주 실행된다면 Map으로 바꿔 lookup 비용을 줄일 수 있습니다.