- 발행일
바닐라 Jinja 화면과 React SPA가 한 저장소에 같이 있었다 — 세 번 시도한 전환이 어디까지 갔나
바닐라 Jinja 화면과 React SPA가 한 저장소에 같이 있었다
이 글은 2026년 9월에 당시 코드를 다시 보며 정리한 것입니다. 저장소는 퇴사 전에 스냅샷으로 보관한 것이라 커밋 이력이 없고 경력기술서와 당시 쓴 지원서·이력서에 적은 담당 범위와 코드를 대조해 썼습니다. 날짜는 코드에 남은 시기 단서 기준입니다.
2021년 4월에 첫 직장으로 들어간 산업 AI 솔루션 기업에서 처음 맡은 화면은 공항 보안 검색 판독 시스템이었습니다. 엑스레이 영상 위에 AI가 찾은 물체를 상자로 그려 주고 검색과 통계를 붙인 장비용 화면이에요. 이 화면을 바닐라 JavaScript에서 React SPA로 바꾸는 것이 그 시기 제가 제안해 관철한 일이었고 경력기술서2022-08-05산업 AI 솔루션 프론트엔드 2021 ~ 2022 경력 기술서산업 AI 솔루션 기업에서 관세청 불법 복제품 판독 시스템과 공항 보안 검색 판독 시스템의 화면을 맡았던 기록입니다. 판독 화면, 웹소켓 실시간 판독, React SPA 전환, 납품처별 설정, 통계 시각화가 핵심이고, 항목마다 근거가 되는 글을 링크해 두었습니다.에도 그렇게 적어 두었습니다.
그런데 그 저장소를 지금 열어 보면 src/html/ 아래에 옛 화면의 서버 템플릿 다섯 개가 그대로 있습니다. 새 화면과 옛 화면이 한 저장소에 같이 살고 있었던 거예요. 그리고 2022년 중반에 제가 쓴 지원서를 다시 읽으니 전환은 한 번이 아니라 세 번에 걸쳐 일어난 일이었습니다.
지원서가 말하는 세 번의 시도
당시 지원서의 경력 항목은 이 프로젝트를 세 토막으로 적고 있습니다. 회사와 발주처 이름은 빼고 옮깁니다.
| 시기 | 지원서 원문(발췌) |
|---|---|
| 2021.04~06 | "3차년도 FE 개발을 위해 리액트로 리팩토링을 시작하면서 예전의 바닐라 자바스크립트 코드를 고치고자 노력을 하였음 … flask로 되어 있는 백엔드와 협업 … 로그인, 설정 초기화, 분석, 통계를 위주로 개발을 하면서 sse 구조, 통계, 그래프 등에 대해 깊게 알 수 있는 기회가 있었으나 처음 개발을 하다보니 요구사항 분석이 잘 되지는 않았음" |
| 2021.10 | "예전에 사용하던 JS에서 나오지 않는 로딩창과 듀얼 모니터 부분에 대한 이슈 사항들을 고쳐서 완료 시키고자 함" |
| 2022.03~05 | "납품건으로 인한 … 제품을 react로 리팩토링 … 4/18 보안 전시회 사용(single monitor version) … 4월 두번째 주 납품 전 확인 명세서 받음(multi monitor version)" |
입사 첫 달부터 React로 다시 짜기 시작했고 그해 10월에는 옛 JS를 다시 고치고 있었고 이듬해 봄에 납품을 계기로 다시 리팩토링했습니다. "제안해서 관철"은 맞지만 한 번에 갈아탄 게 아니라, 옛 화면을 붙들고 있던 시기가 중간에 있었어요. 이력서에는 이 프로젝트의 개발 인원이 프런트 3명·백엔드 1명이고 제 담당은 "로그인, 설정, 웹소켓 아이콘"이라고 적혀 있습니다. 전환을 제안한 건 저였지만 화면 전체를 혼자 옮긴 건 아닙니다.
src/html/에 옛 템플릿이 남은 것도 이 시간표와 맞물립니다. 2021년 10월에 옛 JS의 로딩창과 듀얼 모니터 문제를 고치려면 옛 화면이 저장소 안에 살아 있어야 했을 테고 그 뒤 납품용 리팩토링이 새 화면 쪽에서만 진행되면서 옛 파일은 지워지지 않은 채 남았다고 읽힙니다. 다만 그게 실제 순서였는지는 커밋이 없어 확인할 수 없습니다.
옛 화면은 어떻게 생겼었나
src/html/에는 파일 다섯 개, 703줄이 남아 있습니다. inspection.html이 436줄, settings.html이 242줄이고 나머지 셋은 로그인과 알림 창이에요. 판독 화면인 inspection.html은 이렇게 시작합니다.
<div id="mainPage-img-left" style="display: inline-block; position: absolute; width: 1920px; height: 848px;
background: url({{ url_for('video_1') }})">
<div id="boxes">
<svg id="boxes_svg" width="1920px" height="848px" version="1.1">
</svg>
</div>
</div>
{{ url_for('video_1') }}는 Flask의 Jinja 문법입니다. 지원서에 "flask로 되어 있는 백엔드"라고 쓴 그 서버가 영상 스트림 주소를 템플릿에 심어 주고 그 위에 1920×848 고정 크기 SVG 하나를 얹어 판독 상자를 그리는 구조였어요. 상단 메뉴는 통계·판독·검색·설정·전원 다섯 개였고 id는 stats·detect·search였습니다.
여기서 하나 정직하게 적어야 할 게 있습니다. 경력기술서에는 "화면을 이동할 때마다 전체를 다시 그리니 답답했다"고 썼는데, 이 파일을 보면 검색 화면(#searchPage)과 통계 화면(#stats-page)이 같은 파일 안에 display: none으로 숨겨져 있습니다. 판독·검색·통계는 한 HTML 안에서 div를 켜고 끄는 식이었고 페이지 단위로 갈라져 있던 건 로그인과 설정 창이었어요. 이 화면을 움직이던 스크립트는 스냅샷에 안 남아 있어서 실제로 화면 전환 때 무엇을 다시 그렸는지는 확인할 수 없습니다. 경력기술서의 그 문장은 코드보다 세게 쓴 셈입니다.
새 화면은 같은 메뉴를 어떻게 옮겼나
React 쪽 진입점인 src/App.js는 210줄이고, 라우트는 열 개입니다.
<Route path="/" element={<><VideoTop/><Main isSingleView={isSingleView} isMultiView={isMultiView}
isDualView={isDualView}/></>}/>
<Route path="/status" element={<><VideoTop/><StatusPage ... /></>}/>
<Route path="/search" element={<><VideoTop/><SearchList ... /></>}/>
<Route path="/thumb" element={<><VideoTop/><SearchThumb ... /></>}/>
<Route path="/detail" element={<><VideoTop/><Detail ... /></>}/>
<Route path="/main_side" element={<><VideoSide/><Dual ... /></>}/>
<Route path="/status_side" element={<><VideoSide/><StatusPage ... /></>}/>
...
옛 화면의 판독·검색·통계가 /·/search·/status로 왔고 검색은 목록(/search)과 섬네일(/thumb)로, 상세(/detail)는 따로 라우트가 됐습니다. 그리고 그 다섯 개가 _side를 붙여 한 벌 더 있어요. 위쪽 다섯은 VideoTop, 아래쪽 다섯은 VideoSide를 머리에 얹습니다. 지원서의 "듀얼 모니터"와 "multi monitor version"이 가리키는 두 번째 창의 주소로 읽히는데, 이 복제를 누가 언제 넣었는지는 커밋이 없어 남아 있지 않습니다.
react-router-dom은 6.2, React는 18.2, 빌드는 react-scripts 4.0.3이었습니다. 설정 하나로 화면 모드를 가르는 코드도 여기 있어요.
const isSingleView = (config['config'] === "M1X1")
const isMultiView = (config['config'] === "M1X2")
const isDualView = (config['config'] === "M2X2")
서버의 set_config 응답에서 M1X1·M1X2·M2X2를 읽어 불리언 셋으로 만들고 그 셋을 열 개 라우트의 모든 화면에 prop으로 넘깁니다. 같은 응답의 target(납품처 종류)으로 클래스 목록을 고르는데, 그 갈래는 따로 적었습니다. 경력기술서 2.3절에 "옵션을 설정으로 뺐다"고 쓴 것의 실제 모양이 이겁니다. 설정으로 뺀 건 맞는데, 그 설정이 컴포넌트마다 prop 세 개로 스며드는 방식이었어요.
옮기다 만 자리
옛 파일이 남아 있는 것 자체는 정리를 안 한 것이지만 옛 구조가 새 코드에 그대로 배어 있는 자리가 따로 있습니다.
첫째, SVG 상자입니다. 옛 화면의 #boxes_svg라는 이름이 React 쪽 MainImage.js·MainBottom.js·DualImage.js 세 파일에 그대로 쓰입니다. 상자를 그리는 방식은 React polygon으로 바뀌었는데, 붙잡는 이름은 옛 것을 썼어요.
둘째, 화면 상태를 localStorage에 둔 것입니다. 옛 화면은 detect_on·alarm_on 같은 버튼을 class="on"으로 켰다 껐다 했는데, 새 화면은 같은 값을 localStorage의 detect·alarm 키에 씁니다. 소켓으로 detectOn·alarmOff 같은 메시지가 오면 localStorage에 적고 설정이 저장되면 storage 이벤트로 창을 통째로 새로고침해요. SPA로 바꾸자고 한 이유가 "전체를 다시 그리지 말자"였는데 설정이 바뀌면 reload를 부르는 셈입니다. 이 구조는 따로 적었습니다.
셋째, Redux입니다. package.json에 redux·react-redux가 있고 src/modules/에 리듀서 파일이 다섯 개 있지만 index.js의 Provider와 createStore는 주석 처리돼 있습니다. useSelector를 import한 파일이 셋인데 호출은 0건이에요. 경력기술서 기술 스택에 Redux를 적었는데, 이 스냅샷 기준으로는 붙어 있지 않습니다.
그때의 선택을 지금 읽으면
1년차였던 제가 제안한 건 "React로 다시 만들자"였고 그건 실제로 됐습니다. 라우트가 생겼고 컴포넌트로 쪼개졌고 경력기술서에 쓴 대로 회사별 화면을 조립할 수 있게 됐어요. 그건 코드가 뒷받침합니다. 다만 지원서가 보여 주듯 그 과정은 한 번의 전환이 아니라 첫 시도, 옛 JS 수리, 납품용 재작업의 세 토막이었고 세 사람이 나눠 한 일이었습니다.
그리고 전환이 파일 단위로만 일어났다는 것도 코드가 보여 줍니다. 상태를 어디 둘지, 창끼리 어떻게 맞출지, 설정이 바뀌면 무엇을 다시 그릴지 같은 질문은 옛 화면의 답(전역 저장소에 쓰고 새로고침)을 그대로 가져왔어요. 제안서에 쓴 "전체를 다시 그리지 않는다"는 라우팅에서는 이뤄졌고 설정 변경에서는 안 이뤄졌습니다. 지원서 첫 항목에 스스로 "처음 개발을 하다보니 요구사항 분석이 잘 되지는 않았음"이라고 적은 것과 같은 자리예요.
검증
- 날짜 근거:
MainBottom.js의 주석 처리된 경로에2021-12-23시각 문자열이 6건 있고 스냅샷 커밋은 2022-07-24입니다. 이 글의 날짜는 앞의 것을 썼습니다. 지원서의 시간표(2021.0406 시작, 2021.10 옛 JS 수정, 2022.0305 재작업)와 어긋나지 않습니다. src/html/5파일 703줄,inspection.html436줄,settings.html242줄은wc -l로 셌습니다.url_for는inspection.html에만 1건입니다.App.js라우트 10개(_side5개 포함),M1X1/M1X2/M2X2·AIRPORT/ENTERPRISE/INTEGRATION분기,storage이벤트reload는 파일에서 직접 확인했습니다.boxes_svg를 쓰는 React 파일 3개,useSelectorimport 3파일·호출 0건,Provider/createStore주석 처리는grep으로 확인했습니다.- 지원서·이력서 인용은 원문 PDF를 읽고 옮겼고 회사·발주처 이름만 뺐습니다.
| 항목 | 경력기술서 | 지원서·이력서 | 코드 | 판정 |
|---|---|---|---|---|
| React SPA 전환 | 제안해서 관철 | 2021.04 시작, 10월 옛 JS 수정, 2022.03 재작업 / 프런트 3명 | src/html/ ↔ App.js 라우트 10개 | 일치, 단 세 번에 걸침·3인 작업 |
| 화면 이동마다 전체 재렌더 | 그렇다고 씀 | 언급 없음 | 판독·검색·통계가 한 HTML 안 display:none | 코드보다 세게 씀 |
| Flask 백엔드·SSE | 언급 없음 | "flask 백엔드", "sse 구조" | url_for, EventSource('/draw_box') | 일치 |
| 듀얼 모니터 | 단화면·양화면 모드 | 2021.10 듀얼 모니터 이슈, multi monitor version | _side 라우트 5개, M1X2/M2X2 | 일치 |
| CONFIG→TARGET 구분 | 언급 없음 | 2022.05 "CONFIG 값 … TARGET 대상으로 구분" | config(화면 모드)·target(클래스 목록) | 일치 |
| 기술 스택 Redux | 적음 | 적지 않음 | 스토어 미연결, 호출 0건 | 경력기술서와 어긋남 |
남은 것 · 한계
- 커밋 이력이 없습니다. 옛 파일이 언제 남았는지,
_side라우트를 누가 넣었는지, 프런트 세 사람 사이의 경계가 어디인지 이 저장소로는 알 수 없습니다. 이 글이 "제가 했다"고 쓴 범위는 경력기술서·이력서가 본인 작업으로 적은 SPA 전환 제안과 로그인·설정·웹소켓 아이콘까지입니다. - 옛 화면의 스크립트가 없습니다. HTML만 남아서 실제로 화면 전환 때 무엇이 다시 그려졌는지는 확인하지 못했습니다. 경력기술서의 "전체를 다시 그린다"는 문장을 코드로 확인할 수도, 반박할 수도 없습니다.
- 세 번의 시도 사이에 무엇이 바뀌었는지는 모릅니다. 지원서는 시기와 목적만 적었고 스냅샷은 마지막 상태 하나뿐입니다. 2021년 4월판과 2022년 봄판의 차이는 남아 있지 않습니다.
- 설정 변경 시 새로고침 구조는 그대로 남겼습니다. 4년 뒤 관계도 11개 화면을 렌더러 한 곳에서 바꿨다2026-08-10관계도 11개 화면을 렌더러 한 곳에서 바꿨다 — 클릭이 이동에서 선택으로노드를 클릭하면 곧바로 화면이 떠나서, 이 노드가 뭔지 보려다가 보던 그림을 잃었습니다. 클릭을 선택으로 바꾸고 다음 행동을 우측 패널로 옮겼어요. 공용 렌더러와 공용 본문을 고쳐 공개 6 · 관리자 5, 11개 화면에 한 번에 걸었고, 설정을 템플릿 set에 두면 include로 안 넘어간다는 함정도 같이 정리했습니다.에서 설정을 뷰로 올려 화면 열한 개를 한 곳에서 바꾼 것과 견주면, 여기서는 설정이 바뀌면 창을 통째로 다시 띄웠습니다.
- 발주처 이름과 회사 로고가
inspection.html의title에 박혀 있습니다. 인용에서는 뺐고 이 글에서 회사명은 쓰지 않았습니다.
관련 글: 산업 AI 솔루션 기업 프런트엔드 2021–20222022-08-05산업 AI 솔루션 프론트엔드 2021 ~ 2022 경력 기술서산업 AI 솔루션 기업에서 관세청 불법 복제품 판독 시스템과 공항 보안 검색 판독 시스템의 화면을 맡았던 기록입니다. 판독 화면, 웹소켓 실시간 판독, React SPA 전환, 납품처별 설정, 통계 시각화가 핵심이고, 항목마다 근거가 되는 글을 링크해 두었습니다. · 관계도 11개 화면을 렌더러 한 곳에서 바꿨다2026-08-10관계도 11개 화면을 렌더러 한 곳에서 바꿨다 — 클릭이 이동에서 선택으로노드를 클릭하면 곧바로 화면이 떠나서, 이 노드가 뭔지 보려다가 보던 그림을 잃었습니다. 클릭을 선택으로 바꾸고 다음 행동을 우측 패널로 옮겼어요. 공용 렌더러와 공용 본문을 고쳐 공개 6 · 관리자 5, 11개 화면에 한 번에 걸었고, 설정을 템플릿 set에 두면 include로 안 넘어간다는 함정도 같이 정리했습니다. · WebSocket connection to wss failed2025-01-14WebSocket connection to wss failed — 후보 열 개를 놓고 하루 종일 좁힌 기록로컬에서 잘 되던 STOMP 채팅이 배포하니까 연결이 안 됐습니다. 원인 후보가 서버 상태부터 인증서까지 열 개쯤 되는 상황에서, 하루 8번의 배포 끝에 mixed content — https 페이지에서 ws:// 차단 — 하나로 좁혔습니다. 후보를 어떤 순서로 제거했는지와, 다음에 같은 일을 겪으면 뭘 먼저 볼지를 정리했습니다. · 설정을 localStorage에 두고 소켓 메시지 16갈래로 창끼리 맞췄다2022-07-24설정을 localStorage에 두고 소켓 메시지 16갈래로 창끼리 맞췄다 — 상태 동기화를 새로고침으로 푼 구조공항 보안 검색 판독 화면은 단화면과 양화면이 브라우저 창 두 개였습니다. 한쪽에서 토글을 누르면 다른 창도 따라와야 해서, 설정값을 localStorage에 적고 소켓으로 문자열을 되쏘고 storage 이벤트로 새로고침하는 구조였어요. 스냅샷 코드에서 그 세 층을 다시 읽고, 같은 문제를 2년 뒤 어떻게 다르게 풀었는지 이어 적습니다. · 클래스 팔레트 20종이 다섯 파일에 복사돼 있고 Redux는 import만 남았다2022-07-24클래스 팔레트 20종이 다섯 파일에 복사돼 있고 Redux는 import만 남았다 — 경력기술서의 "Redux"를 코드로 다시 확인하기경력기술서에 기술 스택으로 Redux를 적어 두었는데, 보관해 둔 스냅샷을 열어 보니 스토어는 주석 처리돼 있고 useSelector는 import만 남아 있었습니다. 판정 클래스 스무 종의 이름과 색 팔레트는 App.js와 리듀서 파일 넷에 그대로 복사돼 있었고, 실제 선택 상태는 localStorage가 들고 있었어요. 한 시점의 코드로 확인되는 것과 안 되는 것을 갈라 적습니다.