발행일

설정을 localStorage에 두고 소켓 메시지 16갈래로 창끼리 맞췄다 — 상태 동기화를 새로고침으로 푼 구조

설정을 localStorage에 두고 소켓 메시지 16갈래로 창끼리 맞췄다

이 글은 2026년 9월에 당시 코드를 다시 보며 정리한 것입니다. 저장소는 퇴사 전에 스냅샷으로 보관한 것이라 커밋 이력이 없고, 경력기술서와 당시 쓴 지원서·이력서에 적은 담당 범위와 코드를 대조해 썼습니다. 날짜는 코드에 남은 시기 단서 기준입니다.

첫 직장에서 맡은 공항 보안 검색 판독 시스템은 장비에 붙어 나가는 웹 화면이었습니다. 판독관 앞에 모니터가 둘이고, 위에서 찍은 영상과 옆에서 찍은 영상을 각각 띄웁니다. 경력기술서에 "단화면·양화면 모드"라고 적은 그 화면이에요.

두 모니터는 브라우저 창 두 개였습니다. 같은 React 앱을 //main_side 두 주소로 열어 둔 것이고, 서로 다른 탭이니 React 상태를 공유할 방법이 없었습니다. 그런데 한쪽에서 "박스 표시 끄기"를 누르면 다른 쪽도 꺼져야 했어요.

팀은 프런트 셋에 백엔드 하나였고, 2023년 말 이력서에 제 담당을 "로그인, 설정, 웹소켓 아이콘"이라고 적었습니다. 이 글에서 다루는 설정 토글과 소켓 메시지가 그 범위예요. 다만 셋이 같은 파일을 만졌으니 어느 줄이 제 손인지는 가르지 못합니다. 판단의 근거는 커밋에 남아 있지 않아서, 구조가 무엇이었는지와 그게 무엇을 대가로 했는지만 적습니다.

세 층: localStorage, 소켓 문자열, storage 이벤트

설정값의 정본은 localStorage였습니다. 왜 서버가 아니라 브라우저 저장소였는지는 2022년 중반에 쓴 지원서에 두 줄로 남아 있어요. "통계 작업 시에 데이터 값을 기억할 수 있도록 설계", "페이지마다 데이터를 호출하다 보면 로딩 시간이 길어져서 해결할 수 있는 방법을 모색". 화면을 옮길 때마다 서버에 다시 묻지 않으려고 값을 브라우저에 붙들어 둔 것이고, 그게 제 담당 항목이었습니다. 앱이 처음 뜰 때 App.js가 기본값을 심습니다.

    if (localStorage.length === 0) {
        localStorage.setItem('detect', 'on');
        localStorage.setItem('alarm', 'off');
        localStorage.setItem('label', 'on');
        localStorage.setItem('roi', 'on');
        localStorage.setItem('save_setting', 'unSave');
        // … direction, only_top, bottom_top, label_top

토글 하나를 누르면 두 가지를 합니다. 내 창의 localStorage를 바꾸고(setItem('detect', 'off')), 소켓으로 문자열을 보냅니다(send('detectOff')). 서버는 그 문자열을 연결된 모든 클라이언트에 되돌려 줍니다. 받는 쪽은 index.js 맨 위, React 바깥에 있습니다.

socketConnection.on('message', function (msg) {
    if (msg === 'refresh') {
        window.history.go(0)
    }
    else if (msg === 'detectOff') {
        localStorage.setItem('detect', 'off')
    }
    else if (msg === 'detectOn') {
        localStorage.setItem('detect', 'on')
    }
    // … alarm, onlyTop, label, bottomTop, labelUp, roi, change_rail 짝이 이어짐

if가 열여섯 갈래입니다. 설정 여덟 가지에 on/off 한 쌍씩. 그 여덟 가지가 어디서 왔는지는 2022년 중반에 쓴 지원서에 있습니다. 2022년 5월 이후 납품처 한 곳의 추가 요구로 "옵션을 추가(레이블 표시, 레이블 위/아래, bbox 추가 등)", "화살표 방향을 기계에 추가 입력"이라고 적어 뒀는데, label·label_top·detect·change_rail 갈래가 그 항목들이에요. 요구가 하나 올 때마다 localStorage 키 하나와 소켓 문자열 한 쌍이 늘어난 셈입니다. 그리고 마지막 else가 소켓으로 오는 판독 결과 JSON을 받는 자리라, 제어 메시지와 데이터 메시지가 한 채널의 문자열 비교로 갈립니다.

세 번째 층은 브라우저의 storage 이벤트입니다. 다른 탭이 localStorage를 바꾸면 이 이벤트가 오는데, e.key === 'save_setting'이면 window.location.reload(true)를 부르는 리스너를 App.js에 달아 설정 저장 신호로 썼습니다. 설정 모달에서 저장을 누르면 save_setting'Save'로 바꾸고 1초 뒤 자기 창을 새로고침합니다. 그 쓰기가 다른 창에 storage 이벤트로 닿고, 다른 창도 새로고침합니다. 동기화의 최종 수단이 새로고침이었던 거예요.

짝이 뒤집힌 메시지

열여섯 갈래를 옮겨 적다가 걸리는 게 있었습니다. detectalarmOff'off'를 쓰는데, 뒤의 넷은 반대입니다.

    else if (msg === 'labelOff') {
        localStorage.setItem('label', 'on');
    }
    else if (msg === 'labelOn') {
        localStorage.setItem('label', 'off');
    }

보내는 쪽 Setting.js를 보면 이유가 보입니다. 거기서는 label이 켜져 있으면 'off'로 쓰고 labelOff를 보내요. 즉 메시지 이름이 "지금 상태"가 아니라 "누른 버튼"이고, 받는 쪽이 그걸 다시 뒤집어 적은 겁니다. onlyTopOff'true'를, change_rail_right'left'를 쓰는 것도 같은 결입니다. 두 창의 값이 결과적으로 맞긴 하는데, 메시지 이름만 봐서는 어느 쪽이 켜지는지 알 수 없는 프로토콜이 됐습니다. 지원서대로라면 이 옵션들은 2022년 5월 이후 한 건씩 덧붙은 것이라, 앞의 짝을 안 고치고 뒤에 이어 붙이다 이름이 굳은 것으로 읽힙니다. 어느 갈래가 제 손인지는 프런트 셋이라 못 가릅니다.

깜박임은 서버에 부탁했다

index.js 맨 위에 이런 주석이 있습니다.

/*TODO: 예비용 장비 로컬에 pycharm main.py line 309 socket_io.send('refresh') 추가
* 수정 전) mainBottom에 버튼이 있었을 때는 화면 깜박임(새로고침) 없이 작동되었음
* 수정 후) Header에서는 서버에 추가한 코드로 인해 화면이 깜박인 후 다시 로드 됨.
*
* 프론트에서는 기능을 우선해서 구현할테니.. 백엔드에서 깜박이는거 없애주세여.. */

버튼이 화면 아래에 있을 때는 그 컴포넌트 안에서 상태를 바꾸면 됐는데, 헤더로 옮기면서 서버가 refresh를 쏘고 창이 통째로 다시 뜨게 된 상황입니다. 깜박임의 원인이 서버가 보내는 refresh 한 줄이라는 것까지는 적혀 있고, 그걸 프런트에서 풀 방법은 적혀 있지 않습니다. 당시 저는 창 사이 상태 공유를 "새로고침 없이" 하는 방법을 몰랐고, 그 사실이 주석에 그대로 남았습니다.

뷰 모드 셋과 _side 라우트 다섯

같은 파일에서 서버 설정 한 값(M1X1·M1X2·M2X2)이 isSingleView·isMultiView·isDualView 세 불리언이 되어 화면 다섯 개 전부에 prop으로 내려가고, 라우트는 /·/status·/search·/thumb·/detail 다섯에 _side가 붙은 다섯이 더 있어 옆 창은 같은 컴포넌트를 헤더만 바꿔 한 번 더 등록한 것입니다. 시기는 지원서에 있어요. 2022년 4월 18일 전시회에 단일 모니터 판을 냈고 4월 둘째 주에 납품처 한 곳의 다중 모니터 명세를 받았다고 적혀 있어 M1X1M1X2가 갈라진 게 그 무렵이고, 박스 좌표는 두 창 모두 소켓과 별도로 EventSource(/draw_box, 옆 창은 /draw_box_side)로 받는데, 이 SSE는 지원서의 2021년 4~6월 첫 리팩토링 항목에 적힌 "SSE 구조"가 남은 것으로 읽힙니다.

검증

  • 날짜 근거: 스냅샷 커밋이 2022-07-24 두 개뿐이고, 코드 안 날짜 문자열은 2021-12-23 6건·2022-04-01 1건입니다. 지원서는 React 리팩토링을 2022년 3~5월, 옵션 추가를 5월 이후로 적고 있어요. 이 글의 날짜는 스냅샷 시각으로 잡았습니다.
  • 경력기술서·지원서·이력서와 대조:
경력기술서 문장코드지원서·이력서판정
웹소켓 양방향 통신으로 실시간 데이터 전송socket.io-client로 판독 JSON 수신, 제어 문자열 송수신이력서 담당 "웹소켓 아이콘"일치
단화면·양화면 모드M1X1/M1X2/M2X2 → 불리언 셋, _side 라우트 5개지원서 4/18 단일 모니터 전시, 4월 둘째 주 다중 모니터 명세일치, 시기 2022-04
줌/확대Detail.js의 캔버스 스케일·휠 줌언급 없음일치
2.3 옵션을 설정으로토글 8종 → localStorage 키 + 소켓 문자열지원서 "레이블 표시·위/아래·bbox·화살표 방향"일치, 시기 2022-05 이후
(언급 없음)옆 창 박스 데이터는 EventSource지원서 2021-04~06 "SSE 구조"경력기술서에만 없음
  • 이 글을 쓰며 센 것: socketConnection.send( 호출 38곳(8개 파일), localStorage 키 20종 남짓, window.history.go(0)·reload 7곳. 그날의 측정치는 없습니다.

남은 것 · 한계

  • 커밋 이력이 없어 "왜"를 못 씁니다. 소켓 문자열로 갈 것인지, 서버가 상태를 들고 있을 것인지를 견줬는지 기록이 없어요. 지원서와 이력서로 담당 범위와 시기까지는 좁혔지만, 프런트 셋이 같이 만진 파일이라 어느 줄이 제 것인지는 끝내 못 가릅니다.
  • 제어와 데이터가 한 채널입니다. else로 떨어진 문자열을 전부 JSON.parse하므로, 새 제어 메시지가 추가되고 프런트가 못 받으면 파싱 예외가 납니다.
  • 동기화가 새로고침에 기대고 있습니다. 설정 저장 때마다 두 창이 다시 뜨고, 판독 중이던 화면이 끊깁니다. 주석의 TODO는 스냅샷 시점까지 해결되지 않았습니다.
  • 같은 문제를 2년 뒤 TripTune에서 다시 만났습니다. 채팅 연결 객체를 어디 둘지를 놓고 STOMP 클라이언트를 useState에서 useRef로 옮겼고, 화면 사이 공유 상태는 스토어로 옮겼습니다. 창 두 개를 맞추는 문제는 그 뒤로 만나지 않았습니다.

관련 글: 산업 AI 솔루션 기업 프런트엔드 2021–2022 · STOMP 클라이언트를 useState에서 useRef로 · WebSocket connection to wss failed · 관계도 11개 화면을 렌더러 한 곳에서 바꿨다 · 바닐라 Jinja 화면과 React SPA가 한 저장소에 같이 있었다 · 메인 화면의 판독 박스는 서버 설정 셋이 그린다