루프팩 프론트엔드 1기 후기

10주라는 시간을 루프팩에서 어떻게 보냈나

프론트엔드회고루퍼스부트캠프루프팩프론트엔드1기LOOPPACK

10주 전의 나

나는 3년차 프론트엔드 개발자고, 실무에서는 Vue만 써왔다. React는 시간이 좀 지난 개인 프로젝트 경험만 있는 상태였다.

처음에 10주는 길게 느껴졌고, React 기반 과정이라는 점도 부담이었다.

하지만 이전부터 기회가 된다면 경력자 대상의 프론트엔드 과정을 들어 보고 싶었다. AI 시대에 프론트엔드 개발자로서 뒤처지고 있지 않을까 하는 생각이 들었고, 실무에 피드백을 줄 수 있는 멘토가 있으면 좋겠다고 생각해 왔다.

돌아보면 React 경험이 부족한 건 문제가 되지 않았다. Props 설계와 컴포넌트 재사용, 상태 관리 아키텍처, 테스트 전략처럼 다루는 주제 자체가 특정 프레임워크에 묶여 있지 않았기 때문이다.

남는 배움이 있는 곳

이 프로그램은 React를 가르치는 과정이 아니다. 1주차 과제 문서의 첫 줄이 이렇게 시작한다.

"잘 돌아가는 코드"와 "좋은 코드"는 다르다. 이번 주는 기능 구현이 아니라, 그 차이를 구분하는 눈과 그걸 강제하는 환경을 만든다.

그래서 1주차에 만든 건 기능이 아니라 ESLint 설정과 git hook이었다. 그리고 10주 내내 적용되는 규칙이 하나 있었다.

본인이 설명할 수 없는 코드는 커밋하지 않는다.

AI가 쓴 코드도 머지하는 순간 내 코드다, 라는 뜻이다.

과제도 단순히 요구사항을 주고, "이거 만들어 오세요"가 아니었다. 하지않을 것을 명확히 정하고 실무에서 실수할 법한 부분을 제한 사항으로 주고 7주차 과제의 경우 "빠른 숫자를 만들려고 느린 API를 없애지 않는다"로 시작하고, 8주차는 "커버리지 숫자를 올리지 않는다"로 시작한다. 무엇을 하지 말아야 하는지가 먼저 적혀 있다.

각 주차는 이론적인 것에 20%만큼 시간을 쓰면, 나머지는 실제 프로젝트를 수행해보며 익히는 시간들이다. 새벽까지 달렸기에 기억에 남지 않는 프로젝트는 없지만, 나의 개발 방식을 되돌아볼 수 있었던 3개의 프로젝트에서 어떤 걸 했고, 어떤 걸 얻었는지 공유하고싶다.

변화 1: 하네스 엔지니어링, AI에게 일을 맡기는 환경 만들기

1주차에 만든 건 기능이 아니라 AI가 일하는 환경이었다. 두 층이다.

규칙을 알려주는 층. CLAUDE.md에 기술 스택, 컴포넌트 규칙, 상태 분류 기준, 완료 전 점검 목록을 적었다. 단, 도구가 잡는 항목은 적지 않는다. ESLint와 TypeScript 설정이 단일 출처이고, CLAUDE.md에는 도구가 판단하지 못하는 것만 남긴다.

어겨도 막는 층. CLAUDE.md는 지시문이라 AI가 따르지 않을 수 있다. 그래서 결정적으로 판별할 수 있는 건 hook에 걸고, 일부러 lint 오류가 있는 파일을 커밋해 실제로 막히는지 확인했다. hook은 내가 만든 커밋에도 AI가 만든 커밋에도 똑같이 걸린다.

.husky/pre-commit   pnpm lint-staged
.husky/pre-push     pnpm tsc --noEmit

그런데 두 층을 세워놓고도 3주차에 이런 피드백을 받았다.

PR 표엔 "useDebouncedValue로 검색어 300ms 지연"이라 적혀있는데, 실제 저장소엔 useDebouncedValue 파일 자체가 없어요. 검색어가 키 입력마다 바로 URL·fetch에 반영돼서 이 항목은 표기와 다르게 아직 미해결이에요.

내가 체크리스트에 체크해서 올린 항목이었다. 파일이 없는데 체크가 되어 있었다. yelling-grandpa

AI로 초안 작성 후 검토하셨다고 했는데, 검토 단계에서 실제로 눌러보고 확인하는 과정이 빠졌던 것 같아요. 체크리스트에 체크하기 전에 그 기능을 직접 화면에서 확인하는 습관을 들이면 이런 간극을 미리 잡을 수 있을 거예요.

"체크리스트에 적은 것이 실제로 저장소에 있는가"는 CLAUDE.md가 시키지도, hook이 막지도 못한다. 내가 검토했다고 생각한 것과 실제로 검토한 것 사이의 간격이 거기 있었다.

10주차에는 두 번째 층을 CI까지 올렸다. 환경 변수 검증 → Lint → Typecheck → Test → Build job과, 독립적으로 도는 E2E job. 둘 다 머지 필수 조건으로 걸었다. E2E까지 필수로 건 건 쿠키·미들웨어·리다이렉트는 E2E 말고 잡을 검증이 없고, 실측 비용 대부분이 고정비라 미뤄도 줄지 않았기 때문이다.

그리고 1주차처럼 일부러 깨뜨려서 확인했다. 타입 오류를 한 줄 넣은 PR을 올렸더니 Typecheck와 Build가 실패하고, 머지 버튼이 비활성화되고, PR 상태가 BLOCKED으로 바뀌었다.

AI에게 일을 맡기되, 규칙은 CLAUDE.md에 적고 결정적으로 판별할 수 있는 건 전부 기계에 넘긴다. 기계에 넘겼다고 믿는 대신 일부러 깨뜨려서 확인한다. 둘 다 못 잡는 것은 내가 직접 눌러보고 설명할 수 있어야 커밋한다.

관련 글: 하네스 엔지니어링, 결국 두 개의 질문이었다 · 분리는 제 역할을 할 때 가치가 살아난다

변화 2: 성능 최적화는 짐작이 아닌 측정하기

7주차 성능 최적화에서 얻은 건 성능 개선은 숫자를 줄이는 일이 아니라, 무엇 때문에 달라졌는지 증명하는 일이라는 기준이다. 지표 점수가 서비스의 성능을 평가하는 절대적인 기준일거라 생각해왔는데, 이 생각이 바뀐 주차였다.

지표는 어디를 봐야 할지 알려준다. 하지만 그게 고칠 문제인지, 그렇다면 어디를 고쳐야할지는 화면에서 실제로 무슨 일이 일어났는지 확인한 뒤에야 정할 수 있었다.

해당 주차에서는 다음 루프로 작업했다.

before 측정 -> 한 번에 하나만 바꾸기 -> after 측정 -> 효과가 없으면 원복

이 순서로 작업하며 내 가설이 틀렸음을 확인했다.

조건부터 고정한다

배포용 production 빌드, 같은 뷰포트 크기, 같은 주소, 같은 네트워크 및 CPU 조건으로 5번 재고 중앙값을 썼다. 아무것도 바꾸지 않은 상태에서도 LCP 측정값이 최대 39.8ms 까지 흔들렸다. 이보다 작은 변화는 개선이라고 말할 수 없다는 기준이 이때 생겼다.

반대로 너무 큰 변화가 있을 경우에도 다른 측정 환경으로 인한 노이즈로 측정이 부정확할 수 있다. 개선 before, after에서의 측정 시 다른 환경 측정으로 인한 노이즈를 제거하기 위해서 반드시 측정 환경을 문서화하게 됐다.

효과 없는 변경은 되돌린다

상품을 정렬할 때 화면이 밀리는 문제가 있었다. 카드 제목 길이 때문이라고 짐작해 제목을 2줄로 잘랐다. 카드 높이는 균일해졌지만 CLS는 0.217 그대로였다. 실제로 이동한 요소를 확인해보니, 높이가 아닌 정렬 순서가 바뀌며 그리드 안에서 위치를 옮긴 카드 자체였다. 그래서 되돌렸다.

고치지 않는 것도 결정이다, 이유를 남긴다

앞선 CLS 0.217은 사용자가 요청한 정렬로 카드가 자리를 옮긴 결과였다. 응답이 1.5초 걸려 입력 직후 500ms의 집계 제외 구간을 벗어났고, 그래서 "예상치 못한 이동"으로 잡혔다.

나는 이걸 의도된 동작으로 보고 손대지 않기로 했고, 이런 경우도 다뤄야 하는지 멘토님께 여쭤봤다. 수치를 줄이려고 목록 교체를 늦추기보다, 주변 콘텐츠가 밀리지 않는지와 사용자가 순서 변화를 이해할 수 있는지를 먼저 보라는 답이 돌아왔다. 필요하다면 View Transitions나 FLIP처럼 위치 변화를 transform으로 보여주는 방법이 있지만, 정해진 하나의 해법보다는 실제로 이동한 요소와 화면 녹화로 영향을 확인한 뒤 손대지 않는 이유든 전환 효과를 넣는 이유든 기록으로 남기라는 것이었다.

관련 글: 7.2MB를 79KB로 줄이는 것보다, 측정 조건 고정이 더 오래 걸렸다

변화 3: 근거를 두고 테스트를 설계하기

거의 막바지인 8주차와 9주차에서는 단위부터 E2E 테스트까지 각 층의 테스트 비용을 이해하고 판단기준에 대한 근거에 따라, 각 층에 적합한 테스트 시나리오를 두는 방법을 익혔다.

실무에서 등한시하던 부분이었다. AI가 만든 테스트가 통과하는지만 봤지, 그 테스트가 필요한지에 대해서는 생각해보지 못했다.

무엇을 테스트하나: 사용자가 보는 것

내부 state가 아니라 "이걸 하면 이게 보이는가"를 테스트한다. 그래서 코드를 작성하기 전, 검증 항목 15개를 먼저 적고 항목마다 어느 층에 둘지 근거를 붙여 문서화부터 했다.

어느 층에서 잡나: 같은 회귀를 가장 싸고 분명하게

E2E, 통합 층에서 동일한 시나리오를 검증해봤다. 장바구니 버튼이 담긴 상태를 반대로 표시하도록 망가뜨리자, E2E만 30초 타임아웃 끝에 "버튼을 못 찾았다"고 실패했다. 같은 단언을 통합으로 내리자 43ms 만에 잡혔다.

그 테스트가 진짜 잡나: 일부러 망가뜨려 본다

mutation test를 Stryker로 기계에 넘겼다. 실행에서 살아남은 변형은 손 실험에서는 후보에도 오르지 않은 자리에서 나왔다.

이 기술은 여러 변형 단순 연산자를 바꿔보는 변형 뿐만 아니라 문자열 값 교체, 본문 제거 등의 변형을 할 수 있다. 살아남은 동작을 확인은 정렬 테스트 보완 및 정상 케이스 누락까지 확인하고 테스트를 보완하는 행동으로 이어졌다.

관련 글: 테스트에 대한 테스트 — 경계를 한 칸씩 옮기자 여섯 곳 중 다섯 곳이 손볼 자리였다 · 값을 하는 곳에만 붙였는가 — 7,514세션 로그로 고른 경로 하나를, 망가뜨려서 증명하기

채찍과 당근

주차별 과제를 수행하고나면, 멘토분들로부터 궁금했던 점과 잘한 점, 아쉬운점에 대한 피드백을 받는다.

읽고 감상을 준 게 아니라 내 PR 본문과 실제 코드를 대조한 결과 근거로 정확히 짚어주신다. 그리고 피드백에 두 가지가 더 있었다.

흩어진 문제를 하나로 묶는 진단.

이 두 가지가 겹치면서 race condition 방지도 자연스럽게 안 된 상태예요 — 디바운스가 없으니 요청이 빨리 여러 번 나가는데, 그걸 무시하는 ignore/AbortController도 없거든요. 사실 이 세 가지는 한 원인(디바운스 미적용)에서 갈라진 문제라, useDebouncedValue 훅 하나만 실제로 만들어 넣으면 같이 해결될 가능성이 높아요.

나는 세 개의 문제로 보고 있었는데 하나였음을 알 수 있는 지점이었다.

내 짐작과 원인이 다를 때.

뒤로 가기가 동작하지 않는 문제가 있었다. replaceState를 써서 히스토리가 안 쌓이는 게 원인이라고 생각했다.

그런데 원인이 조금 특이해요 — useUrlSearchParams가 실제 브라우저 popstate가 아니라 직접 만드신 커스텀 이벤트만 구독하고 있어서, replaceState라 히스토리가 안 쌓이는 문제 이전에 진짜 뒤로가기를 눌러도 이 훅 자체가 반응을 안 해요.

내가 본 원인은 두 번째 원인이었고, 그 앞에 하나가 더 있었다. 이런 건 코드를 실제로 열어보지 않으면 나올 수 없는 지적이다.

잘한 부분도 근거와 함께 왔다. 6주차에는 레이어 간 의존을 직접 검사한 결과를 붙여줬다. grep -rn "features/\|widgets/\|_pages" src/entities src/shared 0건, features 간 교차 0건, widgets 간 교차 0건. "잘하셨어요"가 아니라 확인한 명령과 결과였다.

5주차 피드백 — '상세 갔다 돌아왔을 때 목록이 바로 떠야 한다'는 요구는 staleTime이 아니라 gcTime이 해결한다는 점, Suspense 경계는 페이지 전체가 아니라 데이터가 실제로 필요한 서브트리에만 걸어야 검색·필터 같은 재시도 수단이 폴백 밖에 남는다는 점을 설명하고, 잘한 점과 아쉬운 점을 각각 근거와 함께 짚는다
5주차 — 내가 물은 것보다 한 겹 아래를 짚어준 답변
6주차 피드백 — entity가 feature store를 직접 구독하지 않게 정리한 점을 짚고, _pages/home과 mock 백엔드가 서로를 참조해 순환이 생긴 지점을 파일과 줄 번호로 지목한다. shared 여부는 재사용이 아니라 '도메인을 알고 있는지'로 판단하라는 기준도 함께 제시한다
6주차 — 레이어 간 의존을 직접 검사해서 확인해 준 피드백
9주차 리뷰 포인트 답변 — aria-busy를 모든 화면에 기본으로 심을지에 대해 '이 화면이 내용을 이미 보여준 채로 갱신되는가'를 기준으로 제시하고, 실측 데이터가 없는 신규 기능은 실패 비용과 재현 난이도 두 축으로 고르되 '배포 2주 뒤 로그를 보고 E2E 목록 재검토' 항목을 RFC에 남기라고 조언한다
9주차 — 비싼 테스트를 어디에 붙일지

함께 하는 과정

평일에는 팀별 코어타임이 있었다. 각자 과제를 하면서 같은 공간에 앉아 있고, 막히면 서로 물어본다.

별것 아닌 것 같지만, 혼자하면 막혔을 때 그냥 덮고 넘어가게 된다. 옆자리에 같은 과제를 하는 사람이 앉아 있으면, 질문을 부담없이 나눌 수 있다. 과제 뿐만 아니라 일을 할 때 AI를 어떻게 쓰는지나 소소한 회사 얘기도 했다.

우리 팀의 경우 휴강 주차에는 지난 과제 코드리뷰도 하며 하루를 그냥 보내지 않으려했다. 개발 열정 빼면 시체인 사람들과 함께해서 더 의미있게 보냈다.

팀별 코어 타임
팀별 코어타임

'1기' 코어타임이 되어버린 '2팀'의 코어타임. 이게 바로 1기 수료율 100%의 비결..?

마치며

10주 동안 배운 건 React와 Next의 어떤 기능이 아니었다. 특정 프레임워크, 기술이나 도메인 의존없이 어느 기술을 쓰든 어느 도메인에 속해있든 무관해서 오래 남을 배움들이었다.

예를 들어 AI에게 맡겨도되는 부분과, 사람이 결정해야하는 부분을 가르는 판단이나 도메인과 데이터에 대한 이해기반으로 캐싱 전략, 테스트 설계를 하는 것 들은 이 과정에 한정되어 사용할 수 있는 것이 아닌 실무에서 재사용 가능한 지식들이었다.

10주 후 지금 시점의 나는 AI와 함께 개발 환경 세팅부터 배포까지 한 사이클을 돌리는 과정을 경험하고, 이 과정에서 나에게 필요한 피드백을 받고 부족한 부분들을 채우면서 AI와 함께 일하는 것에 대해 자신감을 얻었다.

이 후기가 참여를 고민하고 계신 분들에게 조금이나마 도움이 되길 바란다.


레퍼럴코드: 28QK3