- 발행일
메인 화면의 판독 박스는 서버 설정 셋이 그린다 — 색·임계값·켜짐을 화면 열두 곳이 각자 물었다
메인 화면의 판독 박스는 서버 설정 셋이 그린다
이 글은 2026년 9월에 당시 코드를 다시 보며 정리한 것입니다. 저장소는 퇴사 전에 스냅샷으로 보관한 것이라 커밋 이력이 없고, 경력기술서와 당시 쓴 지원서·이력서에 적은 담당 범위와 코드를 대조해 썼습니다. 날짜는 코드에 남은 시기 단서 기준입니다.
공항 보안 검색 판독 시스템의 메인 화면은 엑스레이 영상 위에 상자를 그리는 화면입니다. AI가 "이 자리에 이 클래스가 이 점수로 있다"고 보내면, 화면이 그 자리에 다각형을 그리고 색을 입히고, 점수가 높으면 소리를 냅니다. 이 글은 그 상자 하나가 그려지기까지 화면이 무엇을 묻는지를 코드로 따라간 기록이에요. 경력기술서2022-08-05산업 AI 솔루션 프론트엔드 2021 ~ 2022 경력 기술서산업 AI 솔루션 기업에서 관세청 불법 복제품 판독 시스템과 공항 보안 검색 판독 시스템의 화면을 맡았던 기록입니다. 판독 화면, 웹소켓 실시간 판독, React SPA 전환, 납품처별 설정, 통계 시각화가 핵심이고, 항목마다 근거가 되는 글을 링크해 두었습니다. 2.3절에 "옵션을 설정으로 뺐다"고 한 줄로 적은 것의 실제 모양이고, 이력서에 제 담당으로 적은 "로그인, 설정, 웹소켓 아이콘" 중 설정이 바로 이 자리입니다.
옵션은 어디서 왔나
2022년 중반에 쓴 지원서의 5월 항목은 이렇게 적고 있습니다. 발주처 이름은 뺐습니다.
추가된 요구사항을 만족을 시킬 수 있도록 설계 (공항 납품처 한 곳)
- 화살표 방향을 기계에 추가 입력
- 옵션을 추가 (레이블 표시, 레이블 위/아래, bbox 추가 등)
- 설정의 thres 값을 직접 작성함으로써 사용자의 보기 편한 시선으로 제작
이 세 줄이 코드 어디에 있는지는 바로 찾힙니다. 화살표 방향은 localStorage의 direction 키와 소켓 메시지 change_rail_left·change_rail_right로, 레이블 표시와 위/아래는 label·label_top 키로, 상자 표시는 roi 키와 BboxOn prop으로, 임계값 직접 입력은 설정 화면의 input type="number"로요. 지원서는 같은 항목에 "너무나 다양한 요구사항 탓에 기본 제품의 틀이 많이 망가져 있어서 확장성은 고려하기 힘들 것으로 판단"이라고도 적었습니다. 설정 항목이 늘어난 배경이 그것이었어요.
상자 하나에 값 셋
src/view/main/mainImage/MainImage.js는 화면이 뜨면 서버 설정을 먼저 받아 둡니다.
const [settingColor, setSettingColor] = useState([]);
useEffect(() => {
axios.get(SETTINGS_URL + '/get_settings')
.then(res => setSettingColor(res.data))
.catch(err => console.log(err));
}, []);
응답은 클래스별 객체의 배열이고, 각 객체에 code·color·thres·check가 있습니다. 그리고 그 배열에서 값을 꺼내는 함수가 셋 있어요.
const setIconBackgroundColor = (icon) => { /* code가 같은 항목의 color */ }
const setThresDisplay = (icon) => { /* code가 같은 항목의 thres */ }
const setToggleDisplay = (icon) => { /* code가 같은 항목의 check */ }
이름은 set…인데 하는 일은 조회입니다. 셋 다 for 문으로 배열을 훑어 code가 맞는 항목의 필드 하나를 돌려줘요. 상자를 그리는 자리에서 이 셋을 한 번에 씁니다.
<polygon key={i}
className="box_top"
points={BboxXY}
data-class={setTopCls}
data-scores={setScore}
style={{
display: setThresDisplay(setTopCls) < setScore && setToggleDisplay(setTopCls) === "on" ? "flex" : "none",
stroke: setIconBackgroundColor(setTopCls)
}}
/>
상자가 보일 조건은 두 개의 AND입니다. 그 클래스의 임계값보다 점수가 높고, 그 클래스가 켜져 있을 것. 색은 세 번째 값이에요. 즉 상자 하나는 서버 설정의 값 셋으로 결정되고, 화면은 그 셋을 조합만 합니다. 설정 화면(Setting.js)에서 색을 고르거나 임계값을 숫자로 적으면 같은 모양 {code: {check, color, thres}}로 modify_settings에 보내고, 저장되면 localStorage에 save_setting을 써서 다른 창이 새로고침하게 합니다.
이 세 값 모델은 SPA로 오면서 만든 게 아닙니다. 옛 서버 템플릿 settings.html에도 "탐지유형색상"이라는 제목 아래 클래스마다 input type="color"와 0~100 range 슬라이더, 켜고 끄는 버튼이 한 칸씩 있었어요. 새 화면은 그 모델을 받아 React로 옮기면서, 지원서에 적은 대로 슬라이더 옆에 숫자 입력을 붙였습니다.
같은 질문을 열두 파일이 각자 한다
문제는 이 조회가 한 곳에 있지 않다는 겁니다. get_settings를 부르는 파일이 열두 개예요. 메인 화면의 위쪽 영상(MainImage.js)과 아래쪽 이력 띠(MainBottom.js)가 각각 부르고, 둘을 감싼 Main.js도 부릅니다. 검색 목록·섬네일·상세·통계·설정·양화면까지 전부 자기 useEffect에서 같은 주소를 칩니다.
그리고 MainBottom.js 517~537행에는 MainImage.js의 조회 함수 셋이 이름까지 같게 한 벌 더 있습니다. 같은 파일 48행을 보면 부모에게서 getSetting이라는 prop으로 설정을 이미 받고 있는데도요. 한 파일 안에 설정이 두 경로로 들어오고, 조회 함수는 다른 파일 것을 복사해 둔 상태였습니다.
이걸 그때 왜 그렇게 뒀는지는 커밋이 없어서 남아 있지 않습니다. 코드에서 읽히는 건 화면을 하나씩 만들 때마다 "설정을 받아서 조회 함수 셋을 만든다"를 반복했다는 것뿐이에요. 지원서 같은 항목에 "페이지마다 데이터를 호출하다 보면 로딩 시간이 길어져서 해결할 수 있는 방법을 모색"이라고 적혀 있는데, 그 로딩에 이 열두 번의 설정 조회가 한몫했을 가능성이 있습니다.
보일지와 울릴지의 경계가 달랐다
상자를 그리는 것 말고, 점수가 높으면 소리를 내는 판정이 MainBottom.js에 있습니다.
const isEmptyBox = (file) => {
let settingCheck = []
getSetting.map((setting) => {
if (setting.check === "on") settingCheck.push(setting.code)
})
let thres;
for (let i = 0; i < file['top'].cls_ids.length; i++) {
const cls = file['top'].cls_ids[i]
for (let n = 0; n < getSetting.length; n++) {
if (getSetting[n].code === cls) thres = getSetting[n].thres;
if (file['top'].scores[i] >= thres && settingCheck.includes(cls)) return true
}
}
return false;
}
같은 값 셋으로 같은 종류의 판정을 하는데, 상자는 thres < score이고 알람은 score >= thres입니다. 점수가 임계값과 정확히 같으면 소리는 나는데 상자는 안 보이는 조건이에요. 점수가 소수라 실제로 같은 값이 나올 일은 드물었겠지만, 두 판정이 같은 규칙이라는 보장이 코드 어디에도 없었습니다. 함수가 한 곳에 있었다면 생기지 않았을 차이입니다. 임계값을 사용자가 숫자로 직접 적게 된 뒤로는 정수 경계에 정확히 걸릴 가능성도 그만큼 커졌을 텐데, 그때 이 차이를 알고 있었는지는 남은 기록이 없습니다.
안쪽 for 문도 지금 보면 이상합니다. thres를 찾는 if와 점수를 비교하는 if가 같은 반복 안에 있어서, 아직 이 클래스의 thres를 못 찾은 순번에서도 이전 클래스의 thres로 비교가 한 번씩 돕니다. 결과가 틀리는 경우가 실제로 있었는지는 확인할 방법이 없습니다.
그때의 선택을 지금 읽으면
경력기술서에 "요구가 올 때마다 분기 코드를 넣는 대신 설정 항목을 늘렸다"고 썼고, 그건 코드가 뒷받침합니다. 클래스 하나가 늘어도 화면 코드를 고칠 필요는 없었어요. 서버 설정 배열에 한 항목이 더 오면 색도 임계값도 켜짐도 따라옵니다. 지원서가 말하는 납품처 요구 셋도 새 분기가 아니라 localStorage 키 하나씩으로 받았습니다.
그런데 설정을 읽는 쪽은 설계가 없었습니다. 값이 한 벌인 건 서버 덕분이고, 화면은 열두 곳에서 각자 물었고, 조회 함수는 복사됐고, 경계 비교는 갈렸어요. 1년차의 저는 "설정으로 빼자"까지는 갔고 "설정을 읽는 자리를 하나로 두자"까지는 못 갔습니다. 지원서에 "확장성은 고려하기 힘들 것으로 판단"이라고 적은 게 그 자리의 고백이었다고 지금은 읽힙니다.
검증
- 날짜 근거:
MainBottom.js86행에 이력 조회의 시작일로2022-04-01 / 00:00이 박혀 있고 주석에 "임의의 날짜"라고 적혀 있습니다. 코드에 남은 시기 단서 중 가장 늦은 것이라 이 글의 날짜로 썼습니다. 스냅샷 커밋은 2022-07-24이고, 지원서의 납품처 요구 항목은 2022년 5월 이후로 적혀 있어 스냅샷이 그 반영 뒤의 상태라는 것과 맞습니다. get_settings호출 파일 12개는grep -rl로 셌습니다.MainImage.js의 조회 함수 셋(2040행)과537행),MainBottom.js의 복제(517MainBottom.js48행의getSettingprop은 파일에서 직접 확인했습니다.- 상자 조건
thres < score(MainImage.js114행)와 알람 조건score >= thres(MainBottom.js258행)는 원문 그대로입니다. Setting.js56~74행에서{code: {check, color, thres}}로modify_settings에 보내고save_setting을 쓰는 것, 247행의input type="number",settings.html의input type="color"·range구조는 확인했습니다.- 지원서 옵션 셋과 코드 키의 대응(
direction·change_rail_*,label·label_top,roi·BboxOn)은index.js·MainHeader.js·MainImage.js에서grep으로 확인했습니다. 지원서·이력서는 원문 PDF를 읽고 옮겼고 회사·발주처 이름만 뺐습니다.
| 항목 | 경력기술서 2.3절 | 지원서·이력서 | 코드 | 판정 |
|---|---|---|---|---|
| 옵션을 설정으로 | 통계 설정·옵션 | 설정이 본인 담당(이력서) | 클래스별 color·thres·check, 화면은 조합만 | 일치 |
| 임계값 직접 입력 | 언급 없음 | "thres 값을 직접 작성"(2022.05, 납품처 요구) | Setting.js input type="number" | 일치 |
| 레이블 표시·위/아래, bbox | 언급 없음 | 옵션 추가(2022.05, 납품처 요구) | label·label_top·roi 키, BboxOn | 일치 |
| 화살표 방향 | 언급 없음 | "화살표 방향을 기계에 추가 입력" | direction 키, change_rail_* 소켓 | 일치 |
| 카메라 위치 설정 | 적음 | 언급 없음 | 해당 코드 없음 | 이 저장소에서 확인 불가 |
| 2D/3D 비디오 판독 설정 | 적음 | 언급 없음 | 해당 코드 없음 | 이 저장소에서 확인 불가 |
남은 것 · 한계
- 커밋 이력이 없습니다. 열두 곳의 호출과 함수 복제 중 어디까지가 제 손인지 이 저장소로는 알 수 없습니다. 이력서가 본인 담당으로 적은 건 로그인·설정·웹소켓 아이콘이고, 지원서가 본인 작업으로 적은 건 납품처 요구 옵션과 임계값 입력입니다. 이 글은 그 범위에서만 "했다"고 썼습니다.
- 경계 비교 차이가 실제 오판을 냈는지 모릅니다. 점수는 소수라 같은 값이 드물었을 것이라는 건 추정이고, 로그도 재현 환경도 없습니다.
- 카메라 위치·2D/3D 설정은 이 코드에도 지원서에도 없습니다. 경력기술서에 적은 그 항목이 다른 저장소에 있었는지, 서버 쪽 설정이었는지는 확인하지 못했습니다.
- 로딩 시간 문제의 원인이 설정 조회였다는 건 추정입니다. 지원서는 "방법을 모색"이라고만 적었고, 무엇을 했는지는 스냅샷에서 구분되지 않습니다.
- 4년 뒤 팔레트에 없는 색은 조용히 안 칠해진다2026-08-24팔레트에 없는 색은 조용히 안 칠해진다 — 색 클래스 142곳 정리와 대비 감사토큰에 없는 색 이름은 빌드 오류가 아니라 그냥 안 칠해집니다. 그렇게 죽어 있던 클래스 142곳을 토큰으로 바꾸고, 다시 들어오면 빌드를 세우는 가드를 붙였어요. 이어서 팔레트 안에 있는 색들의 명도 대비를 재봤더니 회색 스케일이 통째로 한 칸 밝아 입력 경계와 회색 글자가 기준 미달이었습니다. 못 고치기로 한 35곳도 이유와 함께 적었습니다.에서는 색이 한 팔레트에서 오지 않으면 화면이 조용히 틀린다는 걸 142곳을 고치며 배웠는데, 여기서는 색이 서버 한 벌에서 오는 대신 읽는 쪽이 열두 벌이었습니다. 같은 문제의 반대편이에요.
관련 글: 산업 AI 솔루션 기업 프런트엔드 2021–20222022-08-05산업 AI 솔루션 프론트엔드 2021 ~ 2022 경력 기술서산업 AI 솔루션 기업에서 관세청 불법 복제품 판독 시스템과 공항 보안 검색 판독 시스템의 화면을 맡았던 기록입니다. 판독 화면, 웹소켓 실시간 판독, React SPA 전환, 납품처별 설정, 통계 시각화가 핵심이고, 항목마다 근거가 되는 글을 링크해 두었습니다. · 팔레트에 없는 색은 조용히 안 칠해진다2026-08-24팔레트에 없는 색은 조용히 안 칠해진다 — 색 클래스 142곳 정리와 대비 감사토큰에 없는 색 이름은 빌드 오류가 아니라 그냥 안 칠해집니다. 그렇게 죽어 있던 클래스 142곳을 토큰으로 바꾸고, 다시 들어오면 빌드를 세우는 가드를 붙였어요. 이어서 팔레트 안에 있는 색들의 명도 대비를 재봤더니 회색 스케일이 통째로 한 칸 밝아 입력 경계와 회색 글자가 기준 미달이었습니다. 못 고치기로 한 35곳도 이유와 함께 적었습니다. · 관계도 11개 화면을 렌더러 한 곳에서 바꿨다2026-08-10관계도 11개 화면을 렌더러 한 곳에서 바꿨다 — 클릭이 이동에서 선택으로노드를 클릭하면 곧바로 화면이 떠나서, 이 노드가 뭔지 보려다가 보던 그림을 잃었습니다. 클릭을 선택으로 바꾸고 다음 행동을 우측 패널로 옮겼어요. 공용 렌더러와 공용 본문을 고쳐 공개 6 · 관리자 5, 11개 화면에 한 번에 걸었고, 설정을 템플릿 set에 두면 include로 안 넘어간다는 함정도 같이 정리했습니다. · 설정을 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가 들고 있었어요. 한 시점의 코드로 확인되는 것과 안 되는 것을 갈라 적습니다.