최근 당근 부동산에서 6개월 기간의 체험형 인턴 공고가 올라왔고, 지원하여 직무 인터뷰를 기회를 가졌었다.
퇴사하고 첫 인터뷰라 굉장히 떨렸는데 준비가 미흡했던 부분들이 많았던 것 같았다. 특히 직무 인터뷰임에도 관련 프로젝트에 대해 물어볼 수 있는 기술 질문들을 잘 대비하지 못했던 것 같아서 인터뷰가 끝나고 본능적으로 깨달았다.

어떤 질문들을 잘 대답하지 못했는지, 왜 이런 대비를 하지 못했는지 등을 돌아보기 위해 회고를 작성해 보았다.
간결하고 직관적이지 못했던 프로젝트 설명
먼저 가장 최근 진행했던 프로젝트와 관련해서 해당 프로젝트가 어떤 서비스였는지 설명을 해달라고 하셨다.
해당 프로젝트는 회사 내부에서 정부 인증 절차를 수행하는 웹 기반 업무 프로세스를 모바일 현장 업무에 맞는 앱으로 개선하는 프로젝트였다. 인증 절차는 회사 내의 웹 서비스가 이미 존재했고, 기존에는 해당 프로세스를 PC 웹에서만 처리했다. 하지만 기존의 웹 시스템은 PC 환경에 맞춰져 있어 모바일 기기에서 사용하기 어려웠고, 현장에서 스마트폰이나 태블릿으로 접근하기엔 UI가 불편하고 제한적이었다. 따라서 모바일 앱을 통해 이를 개선해야 한다는 니즈가 존재했다.
이 문제를 해결하기 위해 크로스플랫폼 모바일 앱을 개발하게 되었다. 여기서 Expo 기반 React Native와 TypeScript를 사용하여 iOS와 안드로이드에서 모두 동작하는 앱을 새로 만들었고, 프론트엔드 개발을 담당하여 진행했다. 먼저 기존 JSP 기반 백엔드 시스템의 JSON API 문서를 면밀히 분석하여 어떤 데이터 구조와 요청 방식이 사용되는지 파악했고, 이를 바탕으로 서버와 API 연동을 진행하면서, 웹 관리자 페이지에서 제공되던 기능들을 모바일에서 사용할 수 있도록 구현했다.
안타깝게도... 위와 같이 깔끔하게 전달하지 못했다.

진행했던 프로젝트와 서비스에 대한 설명을 위해서는 서비스 개발의 목적과 개발하게 된 배경, 기존의 문제점과 개선에 대한 필요성, 그리고 이에 대한 해결 방안과 구현 과정에 대한 설명들이 포함되어 있어야 한다고 생각한다. 내 설명은 굉장히 정돈되지 못했고, 서비스의 배경과 구현 과정에 대한 간략한 설명만을 담고 있었다.
심지어 중간에 당황하면서 JSP 기반의 서비스를 "JWP 기반의 서비스"라고 말했다.

당시에는 몰랐는데, 이러한 잘못된 용어 사용에 대해 정정하지도 않았으니 굉장히 큰 마이너스였다고 생각한다. 개발자라는 전문직임에도 내가 참여한 프로젝트의 기술 배경에 대해 설명하지 못하고 당황한 것에 엄청난 현타를 겪었다.
주절주절 트러블슈팅 경험 설명
위 프로젝트 관련해서 트러블슈팅 경험(이왕이면 프론트엔드)에 관해 물어보셨다.
당시에 나는
A가 가장 큰 문제였고, B라는 프론트엔드적인 문제가 발생했는데, 이러한 문제를 해결하기 위해 C라는 방식과 D라는 방식을 고민하다가 ~~한 이유로 C가 더 나아 C 방식으로 채택하여 해결했다.
라고 말했다.
A가 가장 큰 문제였는데, 프론트엔드적인 문제는 B라서 B에 대해 설명하고, 이를 설명하는 과정인데...지금 봐도 엉망진창이다.
면접 질문 준비하면서 생각해 두고 정리한 STAR 기법은 어디다가 팔아먹었는지 모르겠다.

STAR기법은 S(Situation) - T(Task) - A(Action) - R(Result) 과정에 걸쳐 문제에 대해 설명하는 방식으로 어떤 문제였고 → 어떻게 접근했고 → 어떤 결과를 냈는지를 구조적으로 설명하는 것이 핵심이다.
만약 다시 대답할 기회가 있다면, 아래와 같이 대답할 수 있을 것이다. (예시는 실제 답변이나 겪었던 문제는 아니지만)
S: 프로젝트 진행 중에 API 연동 작업을 하던 중, 특정 공정 단계에서 화면이 비정상적으로 작동하는 이슈가 있었습니다. 입력 폼이 렌더링 되지 않거나, 잘못된 값이 서버로 전송되는 문제가 있었습니다.
T: 저는 이 부분의 프론트엔드 구현을 담당하고 있었기 때문에, 문제의 원인을 분석하고 빠르게 수정해야 했습니다. 특히 공정마다 요청/응답 데이터 구조가 달라서, 일관된 처리가 어려웠던 상황이었습니다.
A: 먼저 Postman으로 문제 구간의 API 요청/응답을 하나하나 다시 테스트해봤고, 공정 A와 공정 B에서 같은 API를 호출하지만 실제 응답 필드가 다르다는 점을 발견했습니다. 이걸 바탕으로 조건 분기 처리가 잘못되어 있던 컴포넌트를 수정했고, 타입 추론이 잘못된 부분을 명확하게 타입 정의해서 오류 가능성을 줄였습니다. 그리고 백엔드 팀과 협업해서 API 응답 포맷을 일부 표준화할 수 있도록 요청했습니다.
R: 그 결과 공정 단계별 입력 폼이 정상적으로 동작하게 되었고, 동일한 문제로 QA에서 발생하던 리포트가 더 이상 발생하지 않게 됐습니다. 이 경험을 통해 다양한 구조의 데이터를 어떻게 유연하게 처리할 수 있을지에 대한 실전 감각을 키울 수 있었습니다.
기술 면접에 대한 대비 부족
기술과 관련된 부분에 대해서 질문이 많았는데, 그 중 기억에 많이 남았던 질문들이 있다. 당시의 내 대답과 개선했으면 할 점들을 다시 한 번 짚어볼 예정이다.
Q. 해당 프로젝트에서 Relay를 사용할 때 fragment를 나누는 기준은 무엇이었나요?
해당 프로젝트는 저희가 중간에 투입되어 있기 때문에, 기존의 컨벤션에 최대한 맞춰서 진행하였습니다. Fragment를 나누는 기준은 정확히 여쭤본 적이 없었지만, 기능 단위로 나누어서 사용하셨던 것 같습니다.
내 대답은 상당히 잘못된 대답이다. 당시 정확히 Fragment의 개념을 정확히 몰랐기 때문에 최대한 생각해서 짜냈던 것이었는데, "기능 단위"로 나누어 사용했다는 것 자체가 틀린 말이다.
먼저 Fragment란, 특정 컴포넌트가 필요로 하는 GraphQL 데이터의 조각이다.
컴포넌트가 스스로 필요한 데이터만 선언하게 하기 위해서 사용하거나, 데이터 구조를 컴포넌트 단위로 분리해서 재사용성과 유지보수성을 향상하는 목적으로 사용할 수 있다. 또한, Relay가 fragment 단위로 데이터 캐싱/최적화를 수행할 수 있게 하기 위해 사용한다.
// UserProfile.tsx
const userFragment = graphql`
fragment UserProfile_user on User {
id
name
avatarUrl
}
`;
export function UserProfile(props) {
const data = useFragment(userFragment, props.user);
return <div>{data.name}</div>;
}
위의 예시를 통해 볼 수 있듯, User객체에서 name, avatarUrl만 필요하다는 걸 fragment로 명시하여 사용하는 방식으로 컴포넌트가 실제로 필요한 데이터만 선언하여 사용하는 것에서부터 fragment를 나누는 기준이 된다.
Fragment를 나누는 기준은 크게 세 가지 이유로 들 수 있다.
1. UI 단위로 분리
// PostItem.tsx
fragment PostItem_post on Post {
title
content
}
UserCard → UserCard_user, PostItem → PostItem_post와 같은 방식으로 하나의 UI 컴포넌트가 하나의 fragment를 가지게 할 수 있다.
직관적이기 때문에 팀원들이 코드를 빠르게 이해하기 쉽고, 각 컴포넌트가 필요한 데이터만 선언하여 책임이 명확하다. 또한, Relay의 자동 의존성 추적 기능과 궁합이 잘 맞다.
단, 너무 잘게 쪼개면 query 트리가 너무 깊어질 수 있고, 비슷한 데이터 구조가 여러 컴포넌트에서 중복될 가능성이 있다.
2. 재사용성을 고려하여 분리
재사용성을 고려하여 분리하면 여러 곳에서 동일한 데이터 구조를 공유할 수 있게 된다.
중복 제거가 가능하도록 코드를 구성할 수 있고, 공통 컴포넌트나 디자인 시스템 컴포넌트에서 유용하게 사용할 수 있다. 더불어 변경 시 하나의 코드만 수정하면 전체에 적용될 수 있다는 추후 개발에 용이하게 사용할 수 있다.
하지만 모든 상황에 딱 맞는 구조가 아니어서, 과하게 일반화하면 특정 케이스에 안 맞을 수 있다. 그리고 재사용을 위해 만든 구조가 오히려 코드의 복잡도를 높일 수 있다.
3. 하위 컴포넌트에 필요한 데이터만 전달
하위 컴포넌트에 필요한 데이터만 전달하기 위해서는 상위 컴포넌트에서 큰 query로 데이터를 받아온 다음, 필요한 부분만 하위 컴포넌트에 fragmentRef 형태로 넘기는 방식을 취한다.
// 상위
const data = usePreloadedQuery(...);
return <UserProfile user={data.user} />;
// 하위
const data = useFragment(userFragment, props.user);
컴포넌트는 "필요한 데이터만 받는다"는 방식을 취하기 때문에 데이터 흐름이 깔끔하고, Relay의 캐시/최적화 로직과 잘 맞는다. 타입 안정성도 높고, 컴포넌트 재사용성도 유지되기 때문에 장점이 많다.
단, fragmentRef를 계속 전달하기 때문에 props drilling처럼 보일 수 있다. 또한, 해당 방식을 사용하기 위해서는 useFragment와 fragmentRef의 연결을 잘 이해해야 하기 때문에 다소 러닝 커브가 있는 것도 단점으로 짚을 수 있다.
따라서 보통 한 가지 기준만 따라서 fragment로 분리하기보다는 상황에 따라 적절히 조합해 사용하는 게 중요하다.
Q. Suspense는 하위 컴포넌트가 던진 Promise를 어떻게 "감지"할 수 있을까요?
음... 이 부분은 제가 더 공부가 필요한 것 같습니다..
해당 질문은 꼬리 질문으로, 먼저 Suspense의 내부 동작 원리에 대한 질문과, 자식 컴포넌트에서의 동작 원리(throw한 Promise를 감지하는 방식)을 대답한 후 나온 질문이었다.
사실상 첫 질문에서 듣고 싶으셨던 부분에 대한 질문이었는데, 어떻게 Suspense는 자식 컴포넌트의 Promise를 감지하여 렌더링을 다르게 할 수 있는지에 대한 정확한 동작 원리를 설명해야 했다.

이 질문이 나왔을 때, 사실 머릿속이 하얗게 밀렸다.
내가 아는 Suspense는 "일단 fallback을 띄워놓고, 데이터를 불러오면 렌더링 하고자 하는 컴포넌트를 렌더링 한다"였는데, 정확히 어떻게 게, 왜 이렇게 동작하는지는 깊게 생각해보지 못했던 것 같다.
기술 질문 특성상 대답 못하는 부분도 있겠지만, 어떻게 보면 굉장히 기본적인 부분인데 모르는 것 같아서 당황했다.
결론부터 말하자면 컴포넌트가 렌더링 중에 Promise를 throw 하면 React가 그것을 감지해서 fallback을 보여주는 것이다.
그렇다면 React는 이것을 어떻게 감지하는 걸까?
<App>
└── <Page>
└── <Suspense>
└── <PostList>
└── <PostItem> ← 여기서 throw Promise
React는 컴포넌트를 트리로 표현하는데, 이 트리는 각각의 컴포넌트를 "Fiber"라는 단위로 표현한다.
위 코드를 예시로 봤을 때, <App>부터 시작해서 트리 하향식으로 렌더를 시도하고 Promise가 발생된 시점에서 다시 위로 올라가면서 해당 Promise를 처리해 줄 수 있는 대상을 찾는다.
이것이 가능한 이유는, React는 동기적으로 컴포넌트를 실행(render)하면서, 중간에 Promise가 던져지면 그걸 "특수 상황"으로 감지하기 때문이다.
여기서 Suspense의 역할이 중요한데, Suspense는 “에러” 중에서도 Promise만 특별하게 취급한다. 앞서 언급한 특수 상황이 발생하면 되돌아 올라가다가 Suspense를 만나게 되고, Suspense는 이때 준비된 fallback을 렌더링 하게 되는 것이다. 해당 Promise가 resolve 되면, 다시 렌더링 하고자 하던 컴포넌트를 띄우는 것이고.
이 개념을 온전히 이해하게 되자, 비로소 왜 Relay에서 Suspense를 써야 하는지 이해가 된 느낌이었다.
<Suspense fallback={<Loading />}>
<ComponentThatUsesStaticText /> ← 아무 데이터 fetch 안 함
</Suspense>
만약 위와 같은 코드가 있다면, Suspense는 해당 시점에선 무의미하다.
왜냐하면 Suspense 내부의 자식이 Promise를 throw 하지 않기 때문에, Suspense까지 거슬러 올라갈 일이 없기 때문이다.
<Suspense fallback={<Loading />}>
<UserProfile userRef={userData} />
</Suspense>
반대로 위와 같이 데이터를 가져오는 경우에는 useFragment가 Promise throw → Suspense fallback 진입 → fetch 끝나면 자동 retry → 정상 렌더의 과정을 거칠 수 있는 것이다.
해당 질문은 결국 "Relay의 사용법을 이해하고 있고, 이때 사용되는 Suspense가 React의 동작 원리를 어떤 식으로 활용하고 있는지 아는가?"에 대한 굉장히 중요한 질문이었던 것이다.
Q. (무한 스크롤에서) 디바운스와 스로틀의 정확한 차이가 뭔가요?
이 부분은 제가 더 공부가 필요한 것 같습니다..
무한 스크롤을 어떻게 구현할 지에 대한 꼬리질문에서 나온 질문이었는데, 이걸 모른다는 게 참... 부끄러웠다. 디바운스 처리한다고 하지 말걸
디바운스(Debounce)는 이벤트가 연속해서 발생하면, 가장 마지막 이벤트만 실행되도록 지연시키는 것으로, “연속된 이벤트 중 가장 마지막 한 번만 실행” 되게 만드는 방식이다.
const debounce = (fn, delay) => {
let timer;
return (...args) => {
clearTimeout(timer); // 기존 타이머 취소
timer = setTimeout(() => fn(...args), delay); // 새 타이머 시작
};
};
예를 들자면 위 코드처럼 input 요소에 글자를 칠 때마다 API 호출하는 게 아니라, 입력이 멈춘 후 일정 시간 뒤에 호출하는 것이다.
무한 스크롤을 예시로 들었을 때, 스크롤이 멈춘 후 일정 시간이 지나야 이벤트 핸들러를 호출하기 때문에 연속적인 요청을 방지할 수 있지만, 반대로 사용자가 계속 스크롤하는 중엔 요청이 안 된다. 즉, 나쁜 UX를 제공할 수 있다.
최초 질문에서 나는 무한 스크롤의 구현 시 스크롤 이벤트를 기반으로 구현하고 "디바운스 처리"를 한다고 했는데, 해당 답변은 좋은 사용 예시가 아니었던 것이다.
const throttle = (fn, limit) => {
let inThrottle = false;
return (...args) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
};
반대로 스로틀(Throttle)은 지정된 시간마다 한 번만 실행되게 만드는 것으로, "계속 이벤트가 발생하더라도 일정 주기마다 실행" 되게 하는 방식이다.
마찬가지로 무한 스크롤을 예시로 들자면, 스크롤이 너무 자주 일어나도 일정 간격(예: 300ms)마다 한 번씩 이벤트 핸들러를 호출하기 때문에 성능 부하 방지에 효과적이고, 계속 스크롤 중이더라도 주기적인 데이터 요청이 가능하다.
일상생활에서 예시를 들자면,
- 디바운스 : “입 다물고 가만히 있다가, 아무도 말 안 하면 그때 말함”
- 스로틀 : “말이 많아도, 1초에 한 번만 말하게 제한함”
와 같이 예시를 들 수 있을 것 같다.
당연한 얘기겠지만, 무한 스크롤을 구현함에 있어서는 스로틀 방식이 더 적합하다.
디바운스 처리 자체가 완전히 틀린 접근은 아니지만, 모르면서 썼던 것을 그대로 드러낸 답변이었다 🥲

면접이 끝났을 때는 만신창이였다.
제대로 답변하지 못한 질문이 많았고, 부족한 면모를 많이 드러낸 것 같아 많이 반성하게 된 것 같다.
실제로 취업 당시 운이 좋게도 면접을 많이 거치지 않고 일하게 되어서 이런 부분에 대한 준비가 내 생각보다도 더욱 많이 모자랐던 것 같다.
개발자라는 직업에서 사용하고 있는 기술 스택의 동작 원리와 방식에 대해 잘 인지하지 못하는 것은 큰 문제가 되는 부분이기 때문에, 관련해서 좀 더 공부를 많이 해야겠다는 생각을 하게 되었다.
내가 아는 것보다 모르는 것이 더 많다는 것을 체감하게 된 날이었다.
'개발자 일기 > 기록(회고)' 카테고리의 다른 글
글또 10기 회고 (0) | 2025.03.30 |
---|---|
[카카오페이 채용 연계형 인턴십]사전 질문 회고 (0) | 2025.03.02 |
2024년 회고록 (2) | 2025.01.05 |
[Upstage AI Lab]컴퓨터공학 개론 학습 후기 (2) | 2024.12.11 |
[Upstage AI Lab]확률과 통계 학습 후기 (2) | 2024.12.02 |
댓글