<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>주토피아</title>
    <link>https://jrzzzing.tistory.com/</link>
    <description>Jr. 프론트엔드 개발자의 성장(개발)일지입니다. 
이전 : https://velog.io/@ming0o</description>
    <language>ko</language>
    <pubDate>Tue, 4 Aug 2026 19:14:43 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>ming0o</managingEditor>
    <image>
      <title>주토피아</title>
      <url>https://tistory1.daumcdn.net/tistory/2279479/attach/65a815d7e36945ef8cde22e426b5efe9</url>
      <link>https://jrzzzing.tistory.com</link>
    </image>
    <item>
      <title>[Infra] 인프라 온보딩 - Docker, Kubernetes, ArgoCD, Grafana는 각자 무슨 일을 하나</title>
      <link>https://jrzzzing.tistory.com/61</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프라 온보딩을 받으면서 kubeconfig 열 개 남짓을 한 번에 병합했다. kubectl, helm, k9s, Lens&amp;hellip; 툴체인 설치만 한나절이었는데, 정작 설치가 끝나고 나니 더 막막했다. 이름만 듣던 도구들이 한꺼번에 쏟아졌는데 각자 뭘 하는 앱이고, 언제 열어봐야 하는지가 정리가 안 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 도구라던지, 사용법 자체를 이해하는 걸 포기하고, &lt;b&gt;내 코드가 배포돼서 사용자 화면에 뜨기까지의 흐름을&lt;/b&gt;&amp;nbsp;순서대로 세워봤다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;git push &amp;rarr; CI (Docker 이미지 빌드 &amp;rarr; 레지스트리 저장)
        &amp;rarr; Helm values 에 새 이미지 버전 기록
        &amp;rarr; ArgoCD 가 그걸 보고 Kubernetes 에 반영
        &amp;rarr; Kubernetes 가 새 파드로 교체
        &amp;rarr; 운영 중 상태는 Grafana / k9s 로 관측&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;포장 &amp;mdash; Docker&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;앱을 어디서든 똑같이 도는 상자로 포장하는 도구.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;제 로컬에서는 되는데요&quot;를 없애는 게 존재 이유다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 앱 + Node 버전 + 시스템 의존성까지 통째로 이미지로 굳혀버리면, 어느 서버에서 실행하든 같은 환경이 된다.&lt;/p&gt;
&lt;table border=&quot;1&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령&lt;/th&gt;
&lt;th&gt;뜻&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker build -t myapp .&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dockerfile 대로 이미지 포장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker run -p 3000:3000 myapp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;포장된 이미지 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker ps&lt;/code&gt; / &lt;code&gt;docker logs &amp;lt;id&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;뭐가 돌고 있나 / 로그&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서는 CI가 이 빌드를 대신 해서 이미지 저장소(ECR)에 올린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 배포 파이프라인 로그에서 제일 먼저 보이는 게 docker build 단계다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 &amp;mdash; Kubernetes&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그 상자 수십 개를 대신 돌려주는 관리자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아파트 관리사무소라고 생각하면 편했다. 입주(배포), 퇴거(종료), 고장 신고 접수(죽으면 재기동), 이사(버전 교체 시 하나씩 갈아끼우기)를 사람 대신 해준다. 컨테이너를 담는 최소 단위가 &lt;b&gt;파드(Pod)&lt;/b&gt; 고, &quot;이 앱을 이런 스펙으로 몇 개 띄워라&quot;라는 명세서가 deployment.yaml 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubeconfig 를 여러 개 병합한 이유도 여기서 이해됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터(관리사무소)가 환경/고객사별로 여러 개라서, &lt;code&gt;kubectl ctx&lt;/code&gt; 로 &quot;지금 어느 사무소에 말 걸지&quot;를 바꾸는 거였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보는 법은 k9s 세 동작이면 일단 충분했다.&lt;/p&gt;
&lt;table border=&quot;1&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;th&gt;뜻&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:pods&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파드 목록 (Running 인지, RESTARTS 가 치솟는지)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;엔터&lt;/td&gt;
&lt;td&gt;파드 상세&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;l&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;로그 &amp;mdash; &lt;b&gt;백엔드 디버깅의 시작점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면이 500을 뱉으면 네트워크 탭 다음으로 여는 게 이 로그다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;s눌러서 멈추고 확인만 하고 에러 그냥 클로드에 긁어서 넣어~..&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배포 동기화 &amp;mdash; ArgoCD&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;git에 적힌 배포 상태와 클러스터의 실제 상태를 계속 비교해서 맞추는 지킴이.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람이 kubectl 로 직접 배포하는 게 아니라, git(Helm 차트의 values)을 고치면 ArgoCD 가 클러스터를 git 대로 만들어준다. 이걸 GitOps 라고 부른다. &quot;배포의 진실은 항상 git에 있다&quot;는 발상.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 중요한 건 이거였다: CI가 values 파일에 새 이미지 버전을 적어놔도, &lt;b&gt;ArgoCD 가 Sync 를 안 하면 클러스터는 옛날 그대로다.&lt;/b&gt; &quot;머지했는데 dev에 반영이 안 돼요&quot;의 단골 원인. 웹 UI에서 볼 건 두 개다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table border=&quot;1&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상태&lt;/th&gt;
&lt;th&gt;뜻&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Synced&lt;/td&gt;
&lt;td&gt;git 과 클러스터가 일치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Healthy&lt;/td&gt;
&lt;td&gt;떠 있는 파드들이 정상&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 초록이면 배포는 끝난 거고, OutOfSync(노랑)면 Sync 버튼, 빨강이면 리소스 트리를 타고 내려가서 어느 파드가 죽었는지 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;관측 &amp;mdash; Grafana&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파드들의 CPU/메모리/재시작 횟수를 시계열 그래프로 보여주는 대시보드.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(수집은 보통 Prometheus 가 하고, Grafana 는 그리는 담당이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그와 역할이 다르다는 걸 이해하는 게 핵심이었다.&lt;/p&gt;
&lt;table border=&quot;1&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;답해주는 질문&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;로그 (k9s)&lt;/td&gt;
&lt;td&gt;무슨 일이 있었나 (문장)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메트릭 (Grafana)&lt;/td&gt;
&lt;td&gt;언제부터 나빠졌나 (그래프)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;파드가 자꾸 죽어요&quot;라면 &amp;mdash; 로그에서 죽기 직전 에러를 보고, Grafana 에서 그 시간대 메모리가 천장을 치는지(OOM) 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘을 같이 봐야 원인이 잡힌다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;장애 때 보는 순서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외울 건 도구가 아니라 이 순서 하나였다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;화면 이상
 &amp;rarr; 네트워크 탭: 어떤 API 가 몇으로 실패? (404/502=라우팅, 500=앱 에러)
 &amp;rarr; k9s: 파드 Running? 로그에 스택트레이스?   &amp;larr; 대부분 여기서 원인
 &amp;rarr; 배포가 반영 안 된 거면 &amp;rarr; ArgoCD Synced 확인
 &amp;rarr; 반복 재시작이면 &amp;rarr; Grafana 메모리/CPU&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리하며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하고 나서 알았는데, 이 구조는 낯선 게 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 모니터링 시스템을 만드는 FE 개발자다 &amp;mdash; 설비 데이터를 mqtt로 받아와 실시간 폴링해서 트렌드 차트를 그리고 임계치 알람을 띄우는 화면을 매일 만든다. &lt;b&gt;Grafana 는 그걸 파드에 하고 있는 것뿐이었다.&lt;/b&gt; 대상이 컨테이너로 바뀌었을 뿐, 수집&amp;rarr;시각화&amp;rarr;알람이라는 구조는 내가 만들던 그것과 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;막막함의 대부분은 도구가 어려워서가 아니라 이해가 없어서였다. 아직 바로 운영을 들어가기엔 걱정이 많이 앞서긴 하지만 ..ㅜㅜ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 장애 때 어느 계층부터 열어볼지 아는 것 &amp;mdash; 온보딩에서 얻어야 할 건 그거면 충분한 것 같다.&lt;/p&gt;</description>
      <category>Dev/Infra &amp;amp; DevOps</category>
      <author>ming0o</author>
      <guid isPermaLink="true">https://jrzzzing.tistory.com/61</guid>
      <comments>https://jrzzzing.tistory.com/61#entry61comment</comments>
      <pubDate>Sun, 2 Aug 2026 22:54:43 +0900</pubDate>
    </item>
    <item>
      <title>[Vue3] canvas에 Figma 같은 정렬 가이드 만들기 - transform 위에서 정확히 그리는 법</title>
      <link>https://jrzzzing.tistory.com/60</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도면 위에 lamp 요소를 수십 개 배치하는 작업이 잦았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열 맞춰서 나열하다보면 1px, 2px로 같은 가로인데 두번 세번 반복작업이 짜증이 나더라,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 입장에서도 이건 손떨림 방지라던지, 가이드 표시를 해주는 게 나을 것 같단 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 요소와 좌표가 가까워지면 자동으로 스냅 + Figma 처럼 흰 점선 가이드가 짠 하고 뜰 수 있도록..?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 문제가 하나 있었다. 캔버스가 &lt;code&gt;transform: scale(N)&lt;/code&gt; 으로 줌이 적용된 상태였다는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구체적 배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캔버스 안의 요소 좌표는 unscaled 좌표 (예: &lt;code&gt;bodyX: 100&lt;/code&gt;)고, 시각적 위치는 &lt;code&gt;scale&lt;/code&gt; 만큼 곱해져서 화면에 그려진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가이드 라인은 두 요소를 잇는 흰 점선이어야 하는데, 어디에 그릴 것인가가 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택지는 둘이었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;캔버스 안에 그리기: element 들이랑 같은 좌표계니 &lt;code&gt;transform: scale&lt;/code&gt; 의 영향을 같이 받음&lt;/li&gt;
&lt;li&gt;캔버스 밖, viewport 좌표로 그리기: 캔버스 transform 과 분리됨&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번을 처음 시도했더니 줌이 1x 가 아닌 상태에서 가이드 위치가 계속 어긋났다. 캔버스 transform-origin 이 center 인데, 가이드를 그릴 unscaled 좌표를 viewport 로 변환하려면 매번 scale + 캔버스 위치 + transform-origin 까지 다 계산해야 했다. 코드도 지저분해지고 디버깅도 어려웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 - Teleport + position: fixed&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가이드 라인을 캔버스 밖, body 직속 자식으로 띄우는 쪽으로 잡았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vue 3 의 &lt;code&gt;Teleport&lt;/code&gt; 를 쓰면 컴포넌트 안에서 선언하지만 실제 DOM 은 다른 곳에 마운트된다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;&amp;lt;Teleport to=&quot;body&quot;&amp;gt;
  &amp;lt;div
    v-if=&quot;$store.state.smartGuides.vertical !== null&quot;
    class=&quot;smart-guide smart-guide-v&quot;
    :style=&quot;{
      left: `${$store.state.smartGuides.vertical}px`,
      top: `${$store.state.smartGuides.bounds.top}px`,
      height: `${$store.state.smartGuides.bounds.height}px`,
    }&quot;
  &amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/Teleport&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.smart-guide {
  position: fixed;
  pointer-events: none;
  z-index: 9999;
}
.smart-guide-v {
  width: 0;
  border-left: 1px dashed rgba(255, 255, 255, 0.55);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;position: fixed&lt;/code&gt; + viewport 좌표로 그리니 캔버스의 &lt;code&gt;transform: scale&lt;/code&gt; 과 무관하게 정확히 원하는 위치에 점선이 떴다. 캔버스가 줌인 / 팬되어도 가이드 좌표 계산을 다시 하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vue에서 Teleport는 React(Next)에서 뭐였던가..? 싶어서 조금 찾아봤다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[Vue3] Teleport 와 React createPortal &amp;mdash; 같은 개념, 다른 이름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캔버스 편집기에 정렬 가이드를 만들면서 Vue 3 의 &lt;code&gt;Teleport&lt;/code&gt; 를 처음 진지하게 써봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;익숙한 React 출신 머리로는 &quot;어 이거 뭐였더라&quot; 싶은 기억이 나서 짚어봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 &lt;code&gt;createPortal&lt;/code&gt; 의 Vue 버전이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이거.. 내가 썼던가.. 싶은 생각이 드는데 써봤던가.. 너무 오래전이라 기억을 되살려야만 했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 트리상의 위치와 실제 DOM 마운트 위치를 분리하는 기능&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성하는 코드 입장에선 자식 컴포넌트지만, 브라우저가 그릴 때는 다른 곳(보통 &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; 직속)에 마운트된다. 컴포넌트의 props/state 흐름은 그대로 유지되면서 시각적 위치만 분리하는 게 핵심.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코드 비교&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React - &lt;code&gt;createPortal&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { createPortal } from &quot;react-dom&quot;;

function Modal({ children }) {
  return createPortal(
    &amp;lt;div className=&quot;modal-backdrop&quot;&amp;gt;
      &amp;lt;div className=&quot;modal&quot;&amp;gt;{children}&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;,
    document.body
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 호출 형태. 첫 인자는 JSX, 두 번째 인자는 마운트할 DOM 노드.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vue 3 &amp;mdash; &lt;code&gt;&amp;lt;Teleport&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;Teleport to=&quot;body&quot;&amp;gt;
    &amp;lt;div class=&quot;modal-backdrop&quot;&amp;gt;
      &amp;lt;div class=&quot;modal&quot;&amp;gt;&amp;lt;slot /&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/Teleport&amp;gt;
&amp;lt;/template&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 형태. &lt;code&gt;to&lt;/code&gt; prop 에 셀렉터 문자열이나 노드 직접 전달.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;API 차이&lt;/h4&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;React Portal&lt;/th&gt;
&lt;th&gt;Vue Teleport&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;도입 시기&lt;/td&gt;
&lt;td&gt;2017년 (React 16)&lt;/td&gt;
&lt;td&gt;2020년 (Vue 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용 형태&lt;/td&gt;
&lt;td&gt;함수 (&lt;code&gt;createPortal&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;컴포넌트 (&lt;code&gt;&amp;lt;Teleport&amp;gt;&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;target 지정&lt;/td&gt;
&lt;td&gt;DOM 노드 직접 전달&lt;/td&gt;
&lt;td&gt;셀렉터 문자열 또는 노드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비활성화&lt;/td&gt;
&lt;td&gt;분기 처리 직접 작성&lt;/td&gt;
&lt;td&gt;&lt;code&gt;disabled&lt;/code&gt; prop 한 줄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSR&lt;/td&gt;
&lt;td&gt;잘 됨&lt;/td&gt;
&lt;td&gt;잘 됨 (Vue 3.2+)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vue 가 컴포넌트로 감싼 형태라 더 직관적이긴 하다. React 는 함수 호출 패턴이 처음 보면 낯설다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실제 쓰이는 용도&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 모달 / 다이얼로그&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 흔한 케이스.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 컴포넌트 CSS 영향(특히 &lt;code&gt;overflow: hidden&lt;/code&gt;, &lt;code&gt;transform&lt;/code&gt;, &lt;code&gt;z-index&lt;/code&gt;)에서 벗어나기 위해 body 직속에 마운트한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// React
function ConfirmDialog({ open, message }) {
  if (!open) return null;
  return createPortal(
    &amp;lt;div className=&quot;dialog&quot;&amp;gt;{message}&amp;lt;/div&amp;gt;,
    document.body
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;django&quot;&gt;&lt;code&gt;&amp;lt;!-- Vue --&amp;gt;
&amp;lt;Teleport to=&quot;body&quot;&amp;gt;
  &amp;lt;div v-if=&quot;open&quot; class=&quot;dialog&quot;&amp;gt;{{ message }}&amp;lt;/div&amp;gt;
&amp;lt;/Teleport&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 확실히 vue가 간편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 툴팁 / 팝오버&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요소 위에 떠야 하는데, 트리거 요소의 부모 컨테이너가 &lt;code&gt;overflow: hidden&lt;/code&gt; 이거나 좁은 영역인 경우 클리핑되어 잘릴 수 있다. portal 로 body 에 띄우면 화면 어디든 자유롭게 그려진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 컨텍스트 메뉴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마우스 우클릭 시 화면 임의 위치에 뜨는 메뉴. 클릭한 지점의 viewport 좌표로 그려야 하는데, 부모 transform 이 끼면 좌표 계산이 꼬인다. portal 로 빼면 viewport 좌표 그대로 사용 가능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. 드롭다운 / Select 옵션 리스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션 리스트가 부모 밖으로 펼쳐져야 하는데 부모에 &lt;code&gt;overflow: auto&lt;/code&gt; 면 클리핑됨. Material UI, Radix, Headless UI 등 거의 모든 React 컴포넌트 라이브러리는 내부적으로 createPortal 을 쓴다. 그래서 사용자가 직접 호출할 일이 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5. 캔버스 위 정렬 가이드 / 미니맵 / 측정 도구&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 이번에 진짜 필요하다고 느낀 케이스. 캔버스가 &lt;code&gt;transform: scale&lt;/code&gt; 로 줌이 적용된 상태에서, 캔버스 위에 떠 있어야 하지만 transform 의 영향은 받지 말아야 하는 UI. 가이드 라인 / 미니맵 / 좌표 표시 / 호버 카드 같은 것들. body 직속에 portal 로 띄우면 좌표 계산이 깔끔해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 평소엔 안 보이는가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본인이 모달을 만든 적 있어도 portal 을 직접 호출 안 했다면, 부모 구조가 운 좋게&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;overflow: hidden 없음&lt;/li&gt;
&lt;li&gt;transform 없음&lt;/li&gt;
&lt;li&gt;새로운 stacking context 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건이었을 가능성이 크다. 이러면 그냥 &lt;code&gt;&amp;lt;div style={{ position: fixed }}&amp;gt;&lt;/code&gt; 박아도 잘 뜬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리(Material UI, Antd, Chakra 등) 를 쓰는 환경이라면 그 안에 portal 이 이미 박혀 있어서 사용자는 영영 못 보고 지나간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 필요해지는 순간은:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디자인 시스템 도입하면서 부모에 &lt;code&gt;overflow&lt;/code&gt; 가 생기는 순간&lt;/li&gt;
&lt;li&gt;사이드바 / 모달 안에 또 다른 floating UI 를 띄울 때&lt;/li&gt;
&lt;li&gt;캔버스 위에 transform 영향 안 받는 layer 가 필요할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 portal/Teleport 의 존재를 알고 있어야 빨리 떠올릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;한 줄 요약&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Teleport(Vue) &amp;asymp; createPortal(React). &lt;br /&gt;부모 CSS 영향 (overflow, transform, stacking context) 에서 벗어나야 하는 floating UI 의 탈출구.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q. 스냅 좌표는 어떻게 잡는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스냅 후보가 될 다른 요소의 viewport 좌표를 받아오기만 하면 된다. &lt;code&gt;getBoundingClientRect()&lt;/code&gt; 가 그걸 그대로 알려준다.&lt;/p&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;const getElementScreenCenter = (id) =&amp;gt; {
  const node = document.getElementById(`divMonitorElem_${id}`);
  if (!node) return null;
  // .point2 는 숨겨진 텍스트/툴까지 포함해 bounding box 가 큼
  // &amp;rarr; 보이는 영역(.point2_Inbox) 기준으로 중심 계산
  const inbox = node.querySelector(&quot;.point2_Inbox&quot;);
  const target = inbox || node;
  const rect = target.getBoundingClientRect();
  return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드래그 중인 요소의 좌표가 다른 요소와 5px 이내로 가까워지면 좌표 보정 + 가이드 라인을 그 viewport 좌표에 표시한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const SNAP_THRESHOLD = 5;
let bestX = null;
let bestY = null;

for (const other of elements) {
  if (other.id === draggedId) continue;
  const dx = other.bodyX - $ui.position.left;
  if (Math.abs(dx) &amp;lt; SNAP_THRESHOLD &amp;amp;&amp;amp; (!bestX || Math.abs(dx) &amp;lt; Math.abs(bestX.delta))) {
    bestX = { target: other, delta: dx };
  }
  // dy 도 동일
}

if (bestX) $ui.position.left = bestX.target.bodyX;  // 스냅&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;X 와 Y 를 독립적으로 추적하니 가로/세로 동시 스냅도 자연스럽게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Q. 가이드 영역 클리핑&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가이드 라인이 화면 끝까지 무한정 그어지면 헤더/하단 바를 침범해 어색하다. 편집 모드 상하단 바의 &lt;code&gt;getBoundingClientRect&lt;/code&gt; 로 캔버스 가시 영역을 잡고 그 안으로 클리핑했다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const topBar = document.querySelector(&quot;.edit_mode_top_bar&quot;);
const bottomBar = document.querySelector(&quot;.edit_mode_bottom_bar&quot;);
const sceneContainer = el.closest(&quot;.sceneContainer&quot;);
const scRect = sceneContainer?.getBoundingClientRect();
const topRect = topBar?.getBoundingClientRect();
const bottomRect = bottomBar?.getBoundingClientRect();

const bounds = {
  left: topRect?.left ?? scRect.left,
  top: topRect ? topRect.bottom : scRect.top,
  width: (topRect?.right ?? scRect.right) - (topRect?.left ?? scRect.left),
  height: (bottomRect?.top ?? scRect.bottom) - (topRect?.bottom ?? scRect.top),
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캔버스 위에 transform 과 무관한 layer 를 따로 두면, 좌표 변환 로직 없이도 정확한 시각 표시가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Teleport 로 body 직속에 올리고 viewport 좌표(&lt;code&gt;getBoundingClientRect&lt;/code&gt;) 로 그리면 끝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 패턴은 미니맵, 측정 도구, 호버 툴팁, 컨텍스트 메뉴같이 &lt;b&gt;&quot;캔버스 위에 떠 있어야 하지만 캔버스 transform 의 영향은 받지 말아야 하는&quot;&lt;/b&gt; 모든 UI 요소에 적용된다.&lt;/p&gt;</description>
      <category>Dev/Frontend</category>
      <author>ming0o</author>
      <guid isPermaLink="true">https://jrzzzing.tistory.com/60</guid>
      <comments>https://jrzzzing.tistory.com/60#entry60comment</comments>
      <pubDate>Fri, 8 May 2026 10:04:36 +0900</pubDate>
    </item>
    <item>
      <title>[Vue3] 캔버스 편집 모드 드래그가 자꾸 원위치로 튀는 이유 - reactive 재렌더와 jQuery UI 충돌</title>
      <link>https://jrzzzing.tistory.com/59</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;상황&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;도면을 띄우고 그 위에 lamp 같은 요소들을 자유롭게 배치하는 캔버스 편집 모드를 만들고 있었다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;요소 이동은 jQuery UI &lt;/span&gt;&lt;span&gt;draggable&lt;/span&gt;&lt;span&gt; 로 처리하는 레거시 &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;코드 위에서 일하던 중이었다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;편집 모드를 켜고 lamp 를 드래그하면 잠깐 따라오다가 다시 원위치로 튀는 현상이 있었다. &lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;콘솔에 에러는 없고, jQuery UI 자체도 정상 동작하고, Vuex 도 멀쩡했다. 다른 페이지에서 같은 라이브러리로 만든 드래그는 잘 됐는데 이 화면만 이상했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;CSS &lt;/span&gt;&lt;span&gt;transition: none !important&lt;/span&gt;&lt;span&gt; 까지 박아봐도 똑같았다. 무언가 외부에서 위치를 계속 덮어쓰고 있다는 감만 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;원인 추적&lt;/span&gt;&lt;span&gt; &lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;Vuex DevTools 를 켜고 드래그 중 어떤 mutation이 fire 되는지 봤다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;mousemove 한 번에 &lt;/span&gt;&lt;span&gt;setHoveredDeviceId&lt;/span&gt;&lt;span&gt; 가 수십 번 호출되고 있었다.&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;캔버스 편집 모드에는 같은 디바이스에 속한 요소들끼리 hover 시 함께 하이라이트하는 기능이 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;마우스가 요소 위를 지나갈 때마다 &lt;/span&gt;&lt;span&gt;hoveredDeviceId&lt;/span&gt;&lt;span&gt; 가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;갱신 &amp;rarr; 컴포넌트 재렌더 &amp;rarr; &lt;/span&gt;&lt;span&gt;:style=&quot;{ left: bodyX+'px', top: bodyY+'px' }&quot;&lt;/span&gt;&lt;span&gt; 가 다시 적용되고 있었다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;jQuery UI 가 라이브로 변경하고 있던 &lt;/span&gt;&lt;span&gt;element.style.left&lt;/span&gt;&lt;span&gt; 값을 Vue가 매 프레임 옛 &lt;/span&gt;&lt;span&gt;bodyX&lt;/span&gt;&lt;span&gt; 로 덮어쓰는 구조였다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;&lt;span&gt;&lt;span&gt;도대체 이 사악한 코드는 왜&lt;/span&gt;&lt;/span&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;[mousemove] &lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;rarr; setHoveredDeviceId mutation &lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;rarr; 컴포넌트 재렌더 &lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;rarr; patchStyle (el.style.left = oldBodyX + &quot;px&quot;)&lt;span&gt;&amp;nbsp; &lt;/span&gt;&amp;larr; 여기서 jQuery UI 가 만든 위치를 덮음&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Vue 3 의 &lt;/span&gt;&lt;span&gt;patchStyle&lt;/span&gt;&lt;span&gt; 은 reactive 가 트리거되면 inline style 의 키를 다시 적용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;props의 &lt;/span&gt;&lt;span&gt;bodyX/bodyY&lt;/span&gt;&lt;span&gt;는 드래그가 끝나기 전까진 갱신이 안 되니 옛&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;값이 그대로 박혔다. 즉 Vue 입장에선 &quot;변한 게 없는데?&quot; 라는 게 정답이었지만, 그 &quot;변한 게 없는&quot; 좌표를 매 프레임 다시 박아넣는 게 jQuery UI 의 라이브&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;위치를 덮어쓰는 부작용을 만들었다. &lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;해결&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;해결 방향은 두 가지였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;1. 드래그 중에는 hover mutation 을 막기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;2. hover state 를 Vuex 밖으로 빼서 재렌더 자체를 안 일으키게 하기&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;1번은&amp;nbsp;&lt;/span&gt;&lt;span&gt;드래그 시작/종료를 감지해 mutation 을 일시 중단해야 하고, 다른 reactive 트리거가 또 끼어들면 같은 문제가 재발한다. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;2번이 더 깔끔했다. Vue 가 모르는 변화면 재렌더가 안 일어나기 때문이다.&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778118101044&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  // 기존 &amp;mdash; Vuex commit으로 reactive 갱신 
  store.commit(&quot;setHoveredDeviceId&quot;, { deviceId });  
  
  
  // 수정 &amp;mdash; DOM 속성 직접 토글
  document.querySelectorAll(`[device_id=&quot;${deviceId}&quot;]`).forEach((el) =&amp;gt; { 
  	el.setAttribute(&quot;data-device-hovered&quot;, &quot;true&quot;);
  });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;CSS 도 &lt;/span&gt;&lt;span&gt;[data-device-hovered]&lt;/span&gt;&lt;span&gt; 셀렉터로 반응하도록 바꿨다.&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778118212315&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.data-box-Item[data-device-hovered] &amp;gt; .point2_Inbox { 
	outline: 0.75px solid rgba(255, 255, 255, 0.7); 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;DOM 속성 토글은 Vue reactive 시스템 밖이라 컴포넌트가 재렌더되지 않는다. 드래그 중 마우스가 요소 위를 지나가도 &lt;/span&gt;&lt;span&gt;:style&lt;/span&gt;&lt;span&gt; 이 다시 적용되지 않으니 &lt;/span&gt;&lt;span&gt;jQuery UI 가 만든 위치가 그대로 살아남는다. &lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;span&gt;배운 점&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;reactive 시스템 안에는 &quot;사용자가 의도한 영속 상태&quot; 만 두는 편이 맞다는 걸 다시 느꼈다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;hover, 임시 마우스 위치, 스크롤 좌표같이 &lt;/span&gt;&lt;span&gt;&lt;b&gt;자주 변하지만 &lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;영속할 필요 없는 transient state&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 는 reactive 밖으로 빼야 외부 DOM 조작 라이브러리와 충돌이 안 난다. &lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;같은 패턴은 Sortable.js, d3, Three.js, leaflet 같이 직접 DOM 을 조작하는 라이브러리들 모두에 적용된다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Vue 의 reactive 가 그 라이브러리가 다루는 &lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;속성에 트리거를 거는 순간, 라이브 동작이 깨질 위험이 따라온다. &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Dev/Frontend</category>
      <author>ming0o</author>
      <guid isPermaLink="true">https://jrzzzing.tistory.com/59</guid>
      <comments>https://jrzzzing.tistory.com/59#entry59comment</comments>
      <pubDate>Thu, 7 May 2026 10:44:41 +0900</pubDate>
    </item>
    <item>
      <title>[LLM] claude의 흐름 톺아보기 - 컨텍스트를 통한 프롬프팅에 대해</title>
      <link>https://jrzzzing.tistory.com/58</link>
      <description>&lt;div class=&quot;meta&quot;&gt;기록일: 2026-04-29&lt;/div&gt;
&lt;div class=&quot;meta&quot;&gt;출처: Claude(Opus 4.7) 대화&lt;/div&gt;
&lt;div class=&quot;meta&quot;&gt;정리 목적: 컨텍스트 추출과 프롬프팅 참고&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;meta&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원래 요청&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;합본 파일을 보면 도면이 떨어져있어 중간에 여백이 있거든? 배경과 여백이 있는데, 이걸 없애주고 도면을 합쳐줬으면 좋겠어&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표면적으로는 &lt;b&gt;&quot;여백 제거 + 합치기&quot;&lt;/b&gt; 두 가지만 명시되어 있는 요청.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두가지의 요청을 통해 만들어졌다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 단순히 합쳐줘 (기존의 요청)&lt;br /&gt;2. 부자연스러운데, 이어지게끔 다시 합쳐줘 (추가적인 요청)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 나는 원래 하나였던 것과 같은 멀쩡한 상태의 도면을 받았고, 이에 궁금증을 느껴 질문한 결과를 기록하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬에 있는 모든 라이브러리를 적재적소에 쓴다? 라기 보단 적재적소에 이걸 써야겠다라고 느낀 의식의 흐름이 궁금했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 사고 흐름 (4단계)&lt;/h2&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div&gt;&lt;span class=&quot;num&quot;&gt;1. &lt;/span&gt;&lt;b&gt;표면 요청 파싱&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;여백 없애기&quot; + &quot;도면 합치기&quot; 라는 두 가지 작업 키워드 추출. 일단 그대로 받아들임.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div&gt;&lt;span class=&quot;num&quot;&gt;2. &lt;/span&gt;&lt;b&gt;컨텍스트 수집&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;폴더 탐색 &lt;span class=&quot;arrow&quot;&gt;&amp;rarr;&lt;/span&gt; 기존에 작업해둔&amp;nbsp;&lt;code&gt;stitch.py&lt;/code&gt; 발견: 단순히 합성(왼쪽+오른쪽)을 위해 사용&lt;/li&gt;
&lt;li&gt;스크립트 안에 &lt;code&gt;OVERLAP = 120&lt;/code&gt; 변수 + 주석 &quot;검은 띠 시각적 제거&quot; 발견&lt;/li&gt;
&lt;li&gt;개별 도면 PNG 시각 확인 &lt;span class=&quot;arrow&quot;&gt;&amp;rarr;&lt;/span&gt; _1, _2가 같은 건물의 좌/우 절반인 걸 인식&lt;/li&gt;
&lt;li&gt;&lt;code&gt;개별크롭/&lt;/code&gt; 폴더 발견 &lt;span class=&quot;arrow&quot;&gt;&amp;rarr;&lt;/span&gt; 사용자가 이미 UI 패널 잘라낸 버전 보유&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div&gt;&lt;span class=&quot;num&quot;&gt;3. &lt;/span&gt;&lt;b&gt;의도 추론 (요청 너머)&lt;/b&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;table style=&quot;height: 73px; width: 576px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 33px;&quot;&gt;
&lt;th style=&quot;height: 33px; width: 102px;&quot;&gt;표면 요청&lt;/th&gt;
&lt;th style=&quot;height: 33px; width: 134px;&quot;&gt;해석 A (좁게)&lt;/th&gt;
&lt;th style=&quot;height: 33px; width: 340px;&quot;&gt;해석 B (넓게)&lt;/th&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px; width: 102px;&quot;&gt;여백 제거&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 134px;&quot;&gt;검은 픽셀만 트리밍&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 340px;&quot;&gt;여백 + 매끄러운 도면 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px; width: 102px;&quot;&gt;도면 합치기&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 134px;&quot;&gt;옆에 붙여놓기&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 340px;&quot;&gt;건물 외곽선이 자연스럽게 이어지도록&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드의 &lt;code&gt;OVERLAP=120&lt;/code&gt;이 결정적 단서 &amp;mdash; &quot;이미 사용자도 이 문제를 인식하고 시도한 적이 있다&quot;는 신호. 해석 B로 판단.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;step&quot;&gt;
&lt;div&gt;&lt;span class=&quot;num&quot;&gt;4. &lt;/span&gt;&lt;b&gt;도구 &amp;rarr; 알고리즘 매핑&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지 처리 도메인 &lt;span class=&quot;arrow&quot;&gt;&amp;rarr;&lt;/span&gt; Python의 &lt;code&gt;PIL&lt;/code&gt; + &lt;code&gt;numpy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&quot;여백 자동 감지&quot; &lt;span class=&quot;arrow&quot;&gt;&amp;rarr;&lt;/span&gt; 픽셀 밝기 threshold + bbox 추출&lt;/li&gt;
&lt;li&gt;&quot;좌/우 자연스럽게 연결&quot; &lt;span class=&quot;arrow&quot;&gt;&amp;rarr;&lt;/span&gt; template matching의 단순 버전 (슬라이딩 윈도우 + 픽셀 차이 평균)&lt;/li&gt;
&lt;li&gt;&quot;층 간 간격 0&quot; &lt;span class=&quot;arrow&quot;&gt;&amp;rarr;&lt;/span&gt; Image.paste()로 누적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 작업을 진행하며 어떤 프롬프팅을 따라갔기에 반환값을 도출할 수 있었고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 처음엔 제대로 실행하지 못했는지 물어본 결과를 정리해달라고 요청했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시행착오 (재미있는 부분)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1차 결과&lt;/b&gt;: 오버랩 매칭으로 자동 정렬 &lt;span class=&quot;arrow&quot;&gt;&amp;rarr;&lt;/span&gt; 사용자 피드백 &quot;여백 보임, 그냥 딱 붙여줘&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2차 결과&lt;/b&gt;: 오버랩 0px로 완전히 옆에 붙임 &lt;span class=&quot;arrow&quot;&gt;&amp;rarr;&lt;/span&gt; 사용자 피드백 &lt;b&gt;&quot;억지로 이어붙인 느낌&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3차 결과 (최종)&lt;/b&gt;: 오버랩 매칭 복귀 + 더 빡센 자동 크롭 (테두리 무시 threshold 60, min_run 3)&lt;/p&gt;
&lt;div class=&quot;reflection&quot;&gt;&lt;b&gt;자기반성:&lt;/b&gt; &quot;딱 붙여달라&quot;를 글자 그대로 받아 0px로 가버린 게 over-correction이었음. 표면 표현보다 &lt;b&gt;맥락(왜 그렇게 말했는지)&lt;/b&gt;을 봐야 했음. 사용자는 &quot;여백을 없앤 깔끔한 결과&quot;를 원한 거지 &quot;수학적으로 0px로 붙이는 것&quot;을 원한 게 아니었음.&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;상당히 구체적인 피드백이 스스로 오간다. 구어체로 말했음에도 불구하고 수학적으로 해석하여 그에 맞는 합당한 방법을 추론해낸다.&amp;nbsp;&lt;br /&gt;사용자가 원하는 결과물을 제안하기 위해 어떤 걸 도출해야할지 구체적으로 말하진 않았지만, 어떤 걸 원하는지 나름 추론하며 진행하는 모습이 놀라웠다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프롬프팅 관점 &amp;mdash; 좋은 대화의 패턴&lt;/h2&gt;
&lt;div class=&quot;takeaway&quot;&gt;&lt;b&gt;왜 이 작업이 잘 풀렸나:&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;맥락이 풍부했음&lt;/b&gt; &amp;mdash; 같은 폴더에 기존 시도(stitch.py), 중간 산출물(개별크롭), 최종본(합본/) 다 있어서 의도 추론이 쉬웠음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;피드백 사이클이 짧음&lt;/b&gt; &amp;mdash; &quot;이상하다 &amp;rarr; 이렇게 바꿔봐 &amp;rarr; 다시 보여줌&quot; 반복으로 점점 의도에 수렴&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시(이미지) 즉시 공유&lt;/b&gt; &amp;mdash; 글자로 &quot;어색하다&quot;라고만 하면 모르는데 결과 png를 즉시 확인할 수 있어서 빠른 교정&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;takeaway&quot;&gt;&lt;b&gt;일반화 가능한 프롬프팅 팁:&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;기존 시도가 있으면 함께 공유&lt;/b&gt; &amp;mdash; 코드든 실패한 결과물이든. AI가 의도 + 시도 흔적을 함께 보면 추론 정확도가 크게 올라감&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&quot;왜&quot;를 한 줄 추가&lt;/b&gt; &amp;mdash; &quot;여백 없애줘&quot; 대신 &quot;여백 없애줘 (PPT에 깔끔하게 올리려고)&quot; &amp;mdash; 목적을 알면 어디까지 다듬을지 판단 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;너무 좁게 명시하지 말기&lt;/b&gt; &amp;mdash; &quot;0px로 붙여줘&quot;보다 &quot;끊긴 부분 없이 자연스럽게&quot;가 더 좋음. AI가 알고리즘 선택의 자유를 가질 수 있게&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중간 결과물 같이 보기&lt;/b&gt; &amp;mdash; 최종 산출물 한 번에 보지 말고, 단계별로 확인. 이번 대화도 1차 &amp;rarr; 2차 &amp;rarr; 3차 거치면서 정답에 수렴&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;나는 이 결과가 굉장히 흥미로웠다. 나는 프롬프팅을 할 때 단계적 접근을 선호한다. &lt;br /&gt;중간에 어떤 결과물이 나와도 우선적으로 전부를 진행하지 말고, 한 개씩만 예시로 나에게 보여준 다음 그 다음에 일괄적으로 전체를 건드려도 된다 하는 편이다. 그리고 내가 진행하는 프롬프팅과 맥락, 그리고 언어적 표현과 내 코드 스타일을 전부 학습하라고 던져놓는 편이다. 메모리 md 파일을 만들어 '지금까지 작업한 내용을 전부 메모리에 저장해' 라고 하면 알아서 저장을 해주고 계속 참고를 한다.&amp;nbsp;&lt;br /&gt;말 그대로 전체 프로세스를 돌릴 때 늘 0순위로 내 메모리를 읽는다라고 생각하면 좋을 듯 하다.&amp;nbsp;&lt;br /&gt;또한, ai를 학습시키기 위해 해당 문제가 되는 장면을 크롭해서 사진을 첨부해 보냈다. 이것도 도움이 많이 되는 것 같다.&amp;nbsp;&lt;br /&gt;마지막으로 프롬프팅을 할 때 자기만의 스타일도 좋지만 ai가 잘 알아듣는 게 어떤 방향일지 고민을 많이 해보는 것도 좋은 것 같다.&lt;br /&gt;ex) 단순화라던지, 다양한 케이스를 뿌려달라던지, 오히려 자유도가 없는 것보단 적당한 자유도 안에서 ai가 고민할 수 있는 여지를 남겨두는 게 좋은 것 같다.&lt;/blockquote&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술 스택 (참고용) - 사용된 라이브러리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Pillow (PIL)&lt;/b&gt;: 이미지 열기/자르기/붙이기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;numpy&lt;/b&gt;: 픽셀을 2D 배열로 변환해서 수치 연산 (mask, bbox)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;core 알고리즘&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;auto_crop: &lt;code&gt;arr &amp;gt; threshold&lt;/code&gt; 마스크 &amp;rarr; row/col density로 bbox 결정&lt;/li&gt;
&lt;li&gt;find_overlap: &lt;code&gt;min_o ~ max_o&lt;/code&gt; 범위 슬라이딩 &amp;rarr; 좌측 우측 겹침 영역의 픽셀 차이 평균이 최소인 위치 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전부 파이썬 라이브러리를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 takeaway&lt;/h2&gt;
&lt;div class=&quot;takeaway&quot;&gt;AI에게 작업을 시킬 때, &lt;b&gt;표면 요청을 그대로 코드로 옮기는 것&lt;/b&gt;과 &lt;b&gt;요청 뒤의 의도를 추론해서 한 번 더 다듬는 것&lt;/b&gt;은 결과 품질이 크게 다르다.&lt;br /&gt;&lt;br /&gt;이번 사례는 후자가 잘 작동한 예시 &amp;mdash; 단, 사용자 피드백을 받으며 조정해야 함. AI 혼자 추론 다 맞추기는 어렵다.&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;좋은 협업 = 사용자가 풍부한 맥락 제공 + AI가 의도 추론 + 빠른 피드백 루프&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발자로 살아남기/기술 정리</category>
      <author>ming0o</author>
      <guid isPermaLink="true">https://jrzzzing.tistory.com/58</guid>
      <comments>https://jrzzzing.tistory.com/58#entry58comment</comments>
      <pubDate>Wed, 29 Apr 2026 10:49:12 +0900</pubDate>
    </item>
    <item>
      <title>[AIoT] 게이트웨이가 뭐길래? Level vs Edge Triggered 알람 이슈</title>
      <link>https://jrzzzing.tistory.com/57</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;AIoT 플랫폼 개발사에서 프론트를 담당하고 있다. 프론트 개발자라고 해서 React/Vue만 알면 될 것 같지만, &lt;/span&gt;&lt;span&gt;산업 IoT 도메인에서는 &lt;/span&gt;&lt;span&gt;센서가 어떻게 서버로 데이터를 쏘는지? &lt;/span&gt;&lt;span&gt;까지 이해해야 하는 순간이 온다...&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트 개발자로 일하면서 &quot;게이트웨이&quot;라는 단어를 제대로 이해해야 할 날이 올 줄 몰랐다. 알람 카운트 버그 하나를 쫓다가, 결국 &lt;b&gt;센서 &amp;rarr; 게이트웨이 &amp;rarr; 서버 &amp;rarr; 프론트&lt;/b&gt;로 이어지는 데이터 흐름 전체를 머릿속에 그려야 했다. 오늘은 그 과정에서 배운 &lt;b&gt;Level-triggered vs Edge-triggered&lt;/b&gt; 개념을 정리해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상황 &amp;mdash; 미확인 알람이 34,560건, 일주일 간 누적 합 69180건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IoT 모니터링 서비스에서 글로벌 알람 뱃지에 &lt;b&gt;34,560건&lt;/b&gt;이라는 비정상적인 숫자가 찍혔다. 알람관리 페이지를 열어보니 더 이상했다. 성격이 전혀 다른 8개 알람(모터 누수, 배터리 이상, CAS 비정상 등)이 &lt;b&gt;전부 똑같이 8,640건&lt;/b&gt;씩 쌓여 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 기기의 센서가 고장났다고 해도 이상한 숫자다. 서로 다른 알람이 &lt;b&gt;&quot;같은 숫자로 수렴&quot;&lt;/b&gt;한다는 게 핵심 단서였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함정 1 &amp;mdash; 게이트웨이는 &quot;이벤트&quot;를 보내지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하드웨어 연결을 맡고 계신 차장님께 문의하니 이런 답이 돌아왔다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;게이트웨이는 현재 알람 상태값(level)을 주기적으로 송신합니다. 별도의 알람 이벤트 발생 메시지나 OFF&amp;rarr;ON 카운트 이벤트는 보내지 않습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 처음으로 &lt;b&gt;Level-triggered&lt;/b&gt;라는 단어를 배웠다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Level-triggered&lt;/b&gt;: &quot;지금 상태가 이래요&quot;를 주기적으로 쏴줌. 조건 충족 여부와 관계없이 &lt;b&gt;현재 값&lt;/b&gt;만 보냄.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Edge-triggered&lt;/b&gt;: &quot;조건이 충족된 &lt;b&gt;바로 그 순간&lt;/b&gt;에 한 번&quot;만 이벤트를 쏨.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게이트웨이가 level-triggered라는 건, 센서 값이 경계치를 넘었든 말든 &lt;b&gt;1분마다 &quot;지금 상태&quot;&lt;/b&gt;를 계속 보낸다는 뜻이다. &quot;알람이 발생했어요!&quot;가 아니라 &quot;알람 플래그가 켜져 있어요 &amp;middot; 계속 켜져 있어요 &amp;middot; 계속&amp;hellip;&quot;을 반복하는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함정 2 &amp;mdash; 성격이 다른 8개 알람이 같은 숫자였던 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게이트웨이가 level-triggered라면, 조건 전이(false&amp;rarr;true, true&amp;rarr;false)를 감지해서 &lt;b&gt;새 이벤트를 생성할지 말지 결정하는 건 누구의 책임&lt;/b&gt;인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답은 &lt;b&gt;서버&lt;/b&gt;다. 서버가 동일 rule/source에 대해 &lt;b&gt;이전 상태를 기억&lt;/b&gt;하면서 관리해야 한다.&lt;/p&gt;
&lt;table style=&quot;height: 110px; width: 597px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px; width: 105px;&quot;&gt;이전 상태&lt;/th&gt;
&lt;th style=&quot;height: 20px; width: 95px;&quot;&gt;현재 수신값&lt;/th&gt;
&lt;th style=&quot;height: 20px; width: 397px;&quot;&gt;서버가 해야 할 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px; width: 105px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 95px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 397px;&quot;&gt;아무것도 안 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px; width: 105px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;height: 25px; width: 95px;&quot;&gt;true&lt;/td&gt;
&lt;td style=&quot;height: 25px; width: 397px;&quot;&gt;INSERT (새 알람 생성)&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px; width: 105px;&quot;&gt;true&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 95px;&quot;&gt;true&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 397px;&quot;&gt;아무것도 안 함 (또는 lastSeen만 업데이트)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px; width: 105px;&quot;&gt;true&lt;/td&gt;
&lt;td style=&quot;height: 25px; width: 95px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;height: 25px; width: 397px;&quot;&gt;UPDATE (alarm 해제 처리)&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 우리 서버는 이 &quot;상태 기억&quot; 로직이 빠져 있었다. level-triggered로 들어오는 true 신호를 받을 때마다 &lt;b&gt;무지성 INSERT&lt;/b&gt;를 하고 있었던 것이다. 1분 주기 &amp;times; 24시간 &amp;times; 24일 = &lt;b&gt;34,560&lt;/b&gt;. 서로 다른 알람들이 각자의 조건이 활성화된 시점부터 꾸준히 폴링마다 INSERT되어서, 결국 비슷한 숫자로 수렴한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 update가 아닌 insert인지, 사실 당연한 것 같기도 했다. 알람이 10초에 한번씩 받아오지만 알람은 당연히 계속 기기에 대한 정보를 내려야하기 때문에 계속 받아와야 하는 것 아닌가? 했는데, 우리는 여기서 규칙으로 알람을 정의했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'조건 충족 시 한 번만 호출' &amp;lt; 이 규칙 정의가 작동하고 있지 않다는 걸 여기서 눈치챌 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 심증은 있었으나 물증이 없었고, 이를 정의하지 않으면 BE와 HW쪽에 전달할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함정 3 &amp;mdash; 프론트는 이 흐름에서 어디쯤 있는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버그를 쫓다가 &quot;혹시 프론트가 숫자를 누적하고 있나?&quot; 의심도 했다. 코드를 다시 봤다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const updateAlarmCount = (count) =&amp;gt; {
  countUnknownAlarms.value = count;  // 덮어쓰기, 누적 아님
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트는 &lt;b&gt;서버가 준 count를 그대로 표시만&lt;/b&gt; 하고 있었다. += 연산자로 카운트를 건드리는 코드는 &lt;b&gt;0건&lt;/b&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이 문제에서 각 레이어의 책임은 이렇게 나뉜다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;게이트웨이&lt;/b&gt;: 현재 상태값을 주기적으로 송신 (이게 원래 역할)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버&lt;/b&gt;: 이전 상태를 기억하고 &lt;b&gt;전이(transition)를 감지&lt;/b&gt;해서 이벤트 저장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프론트&lt;/b&gt;: 서버가 집계한 결과를 &lt;b&gt;그대로 보여주기만&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책임 분리가 무너지면, 다른 레이어가 뒤집어쓰기 딱 좋은 구조가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방향 &amp;mdash; 어디를 고쳐야 하는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진단이 확실해졌으니 공은 서버로 넘어갔다. 확인할 포인트는 세 가지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것 또한 차장님께서 작성해주신 체크리스트다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;기존 active 알람이 있을 때 INSERT가 아닌 &lt;b&gt;유지/UPDATE&lt;/b&gt; 되는지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;false&amp;rarr;true 전이&lt;/b&gt;에서만 신규 알람이 생성되는지&lt;/li&gt;
&lt;li&gt;OR 조건 알람의 clear 기준이 &lt;b&gt;&quot;전체 false&quot;&lt;/b&gt;인지&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 이 로직을 제대로 구현하면, level-triggered로 들어오는 상태값이 edge-triggered 이벤트로 &lt;b&gt;정확히 한 번만&lt;/b&gt; DB에 기록된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 결국 버그의 실체는 거창한 설계 결함이 아니라 서버의 depth 한 계층 어긋남이었다. DB 설계도 정상이었고, 프론&lt;/span&gt;&lt;span&gt;트 로직도 정상이었는데, 서버가 저장 경로와 다른 경로에서 값을 읽고 있었다,,는 단순한 이유로 전체 시스템이 level-trigge&lt;/span&gt;&lt;span&gt;red처럼 오작동하고 있었다. 저장 구조와 읽기 구조가 한 뼘이라도 어긋나면 defaults로 미끄러지면서 완전히 다른 동작 모델&lt;/span&gt;&lt;span&gt;이 되어버린다는 것.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;체크리스트 &amp;mdash; &quot;카운트가 이상하다&quot; 싶을 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✓ 게이트웨이(또는 데이터 소스)가 &lt;b&gt;이벤트를 쏘는지, 상태값을 쏘는지&lt;/b&gt; 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✓ level-triggered라면 &lt;b&gt;상태 전이 관리 책임&lt;/b&gt;이 서버에 있는지 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✓ 서로 다른 알람이 &lt;b&gt;같은 숫자로 수렴&lt;/b&gt;한다면 polling hit 기준 집계를 의심&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✓ 프론트에서 += 같은 누적 연산이 있는지 먼저 점검 (대부분 없음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✓ API 응답 구조에 count, activeCount, unreleasedCount 같은 &lt;b&gt;여러 카운트 필드가 있는지&lt;/b&gt; 살펴보기 (간혹 서버가 다른 필드에 정답을 숨겨놓음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;교훈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트 개발자로 살면서 하드웨어 게이트웨이의 송신 방식까지 파헤칠 날이 올 줄 몰랐다. 사실 컴퓨터 공학 전공하면서 이런 게 있다고만 배웠지 그걸 내가 실제로 연결하고 데이터를 받아보는 날이 올 줄 알았겠냐.. ㅠ?? 그런데 그걸 모르면 &lt;b&gt;버그의 진짜 원인을 설명할 수 없었다&lt;/b&gt;. IoT, 실시간 대시보드, 알림 시스템 같은 도메인에서는 &lt;b&gt;Level-triggered vs Edge-triggered&lt;/b&gt; 구분이 생각보다 자주 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;센서가 뭘 쏘는지 &amp;rarr; 서버가 어떻게 저장하는지 &amp;rarr; 프론트가 뭘 보여주는지, 이 흐름 전체를 머릿속에 그릴 수 있으면 &lt;b&gt;&quot;공을 어디로 넘겨야 하는지&quot;&lt;/b&gt;가 명확해진다. 프론트의 영역 바깥이라고 외면하면, 본인이 엉뚱한 버그를 떠안게 되는 순간이 온다. (진짜 무서웠슨)&lt;/p&gt;</description>
      <category>개발자로 살아남기/트러블슈팅</category>
      <author>ming0o</author>
      <guid isPermaLink="true">https://jrzzzing.tistory.com/57</guid>
      <comments>https://jrzzzing.tistory.com/57#entry57comment</comments>
      <pubDate>Tue, 21 Apr 2026 10:19:56 +0900</pubDate>
    </item>
    <item>
      <title>[Vue3] ref(객체)에 watch 안 걸어도 되는 이유</title>
      <link>https://jrzzzing.tistory.com/56</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 Vue 3 프로젝트에서 규칙 생성 화면을 손보던 중이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 라디오 버튼을 클릭해도 저장 시 서버로 나가는 payload에는 &lt;b&gt;초기값만 찍혀 있는&lt;/b&gt; 상황이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트는 대충 이런 구조였다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    &amp;lt;input type=&quot;radio&quot; :value=&quot;true&quot; v-model=&quot;options.runAlways&quot; /&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;      
 	const props = defineProps&amp;lt;{ body: Body }&amp;gt;(); 
  	const options = ref&amp;lt;Options&amp;gt;(props.body?.options ?? { runAlways: false });  
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;아, local &lt;code&gt;options&lt;/code&gt; ref가 별도 객체라서 스토어 반영이 안 되는 건가?&quot; 라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 번째 대응 &amp;mdash; watch 5개 박기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스토어로 값을 밀어넣는 watch를 추가했다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;watch(
    options,
    () =&amp;gt; {
      ruleStore.setAlarmTask(props.id, { options: { ...options.value } });
    },
    { deep: true }
  );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 &lt;b&gt;알람 / 이메일 / SMS / 제어명령 / 외부서버 요청 5개 컴포넌트에 전부&lt;/b&gt; 추가했다. 뿌듯했다. &quot;완벽하다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 수정이 끝나고 생각이 들었다. &lt;b&gt;&quot;근데 굳이 watch 필요했나?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함정 1 &amp;mdash; &lt;code&gt;ref(obj)&lt;/code&gt;는 객체를 복제하지 않는다&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const obj = { runAlways: false }; 
const myRef = ref(obj); 
  
myRef.value.runAlways = true; 
console.log(obj.runAlways); // true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ref(someObject)&lt;/code&gt;는 내부적으로 &lt;code&gt;reactive()&lt;/code&gt;로 감싸 프록시를 만들지만, &lt;b&gt;결국 같은 메모리를 바라본다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제가 아니라 '반응형으로 감싸는 작업'이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;ref(props.body.options)&lt;/code&gt;로 만든 local ref의 &lt;code&gt;.value&lt;/code&gt;는 &lt;code&gt;props.body.options&lt;/code&gt;와 같은 객체다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한쪽을 바꾸면 다른 쪽도 바뀐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// 라디오 클릭 &amp;rarr; v-model 동작
options.value.runAlways = true;

// 결과적으로... 
props.body.options.runAlways === true;          // ✅  
store.task.body.options.runAlways === true;     // ✅ (props가 store를 가리키므로)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;watch 없이도 클릭 &amp;rarr; 스토어 반영이 알아서 됐던 것&lt;/b&gt;이다. 반응형이 대신 일해줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함정 2 &amp;mdash; 진짜 버그는 &lt;code&gt;||&lt;/code&gt; fallback이었다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 코드를 다시 살펴봤다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const options = ref((props.body?.options || props.options) ?? { runAlways: false });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fallback이 2단으로 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째가 &lt;code&gt;props.body.options&lt;/code&gt; (&lt;b&gt;body 안쪽&lt;/b&gt;), 두 번째가 &lt;code&gt;props.options&lt;/code&gt; (&lt;b&gt;task 바깥쪽&lt;/b&gt;).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 데이터 구조가 섞여 있어서 &lt;code&gt;props.body.options&lt;/code&gt;가 &lt;code&gt;undefined&lt;/code&gt;인 채로 두 번째 fallback으로 떨어지는 경우가 있었다. 그러면 local ref는 &lt;b&gt;body 바깥 쪽 options를 감쌌고&lt;/b&gt;, 유저가 라디오를 클릭하면 그 잘못된 위치에 값이 반영됐다. 서버는 &lt;b&gt;body 안쪽&lt;/b&gt;을 기대하니 당연히 못 읽고 기본값으로 처리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;버그는 &quot;반응형 연결 문제&quot;가 아니라 &quot;데이터 위치 문제&quot;&lt;/b&gt;였다. watch 추가는 오진이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;watch 대신, 스토어에 들어오고 나가는 지점에서 데이터 구조를 정규화했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;로드 시&lt;/b&gt;: 서버에서 &lt;code&gt;task.options&lt;/code&gt;가 task-level로 내려오면 &lt;code&gt;task.body.options&lt;/code&gt;로 옮기고 task-level은 제거&lt;/li&gt;
&lt;li&gt;&lt;b&gt;저장 시&lt;/b&gt;: 혹시 남아있는 task-level &lt;code&gt;options&lt;/code&gt;가 있으면 삭제 후 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 local ref는 항상 &lt;code&gt;props.body.options&lt;/code&gt;를 가리키게 되고, 반응형이 알아서 스토어까지 전파한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;watch는 없어도 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;체크리스트 &amp;mdash; &quot;반응형이 안 되는 것 같다&quot; 싶을 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✓ local ref가 감싼 객체가 &lt;b&gt;정말 원본 객체&lt;/b&gt;와 같은 참조인지 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✓ fallback &lt;code&gt;||&lt;/code&gt;이 여러 단 있다면, 어느 쪽으로 떨어지는지 런타임에 찍어보기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✓ &lt;code&gt;ref.value = 새객체&lt;/code&gt;처럼 &lt;b&gt;레퍼런스를 갈아끼우는 코드&lt;/b&gt;가 있는지 확인 (이럴 땐 원본 연결 끊김)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✓ 스토어 값을 콘솔이나 Vue Devtools로 먼저 확인 &amp;mdash; 진짜 반영이 안 된 건지 아니면 엉뚱한 곳에 반영된 건지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;교훈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ref(obj)&lt;/code&gt;는 &lt;b&gt;원본 객체를 반응형 프록시로 감싸는 작업&lt;/b&gt;이지 복제가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.value&lt;/code&gt;의 속성을 바꾸면 원본까지 자동 전파된다. 그래서 &lt;code&gt;v-model&lt;/code&gt;로 객체 속성을 묶어두면, &lt;b&gt;watch 없이도 스토어 반영이 된다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;반응형이 안 되나?&quot; 싶을 때 watch부터 박기 전에, 데이터가 &lt;b&gt;어느 객체를 가리키고 있는지&lt;/b&gt;부터 확인하자. Vue 3 reactivity는 생각보다 똑똑하고, 과잉 방어 코드는 결국 가독성의 적이 된다.&lt;/p&gt;</description>
      <category>Dev/Frontend</category>
      <author>ming0o</author>
      <guid isPermaLink="true">https://jrzzzing.tistory.com/56</guid>
      <comments>https://jrzzzing.tistory.com/56#entry56comment</comments>
      <pubDate>Mon, 20 Apr 2026 18:40:23 +0900</pubDate>
    </item>
    <item>
      <title>[Git] 브랜치 간 변동 사항을 옮길 때 주의할 점</title>
      <link>https://jrzzzing.tistory.com/55</link>
      <description>&lt;article style=&quot;max-width: 720px; padding: 1.5rem 0; font-size: 15px; line-height: 1.7; color: var(--color-text-primary);&quot;&gt;
&lt;p style=&quot;color: var(--color-text-secondary); margin: 0px 0px 2rem;&quot; data-ke-size=&quot;size16&quot;&gt;: develop &amp;rarr; gov 브랜치 반영 과정에서 겪은 함정과 해결법&lt;/p&gt;
&lt;h3 style=&quot;font-weight: 500; margin: 2rem 0px 0.75rem;&quot; data-ke-size=&quot;size23&quot;&gt;상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code style=&quot;background: var(--color-background-secondary); padding: 2px 6px; border-radius: 4px; font-size: 13px;&quot;&gt;develop&lt;/code&gt; 브랜치에서 조직도 기능을 개발했고, 이걸 &lt;code style=&quot;background: var(--color-background-secondary); padding: 2px 6px; border-radius: 4px; font-size: 13px;&quot;&gt;gov&lt;/code&gt; 브랜치에 반영해야 했다. cherry-pick 하기 편하게 &lt;code style=&quot;background: var(--color-background-secondary); padding: 2px 6px; border-radius: 4px; font-size: 13px;&quot;&gt;gov&lt;/code&gt; 기반으로 새 브랜치를 만들고 파일만 가져오기로 했다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot; style=&quot;background: var(--color-background-secondary); border: 0.5px solid var(--color-border-tertiary); border-radius: 8px; padding: 1rem 1.25rem; font-size: 13px; overflow-x: auto; margin: 1rem 0;&quot;&gt;&lt;code&gt;git checkout gov
git checkout -b feature/feat-org-page
git checkout develop -- src/public/pages/serviceUsersManagement.vue&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단해 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: 500; margin: 2rem 0px 0.75rem;&quot; data-ke-size=&quot;size23&quot;&gt;함정 1 &amp;mdash; 기능과 무관한 변경이 딸려온다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code style=&quot;background: var(--color-background-secondary); padding: 2px 6px; border-radius: 4px; font-size: 13px;&quot;&gt;git checkout branch -- file&lt;/code&gt;은 해당 파일의 브랜치 최신 상태 &lt;b&gt;전체&lt;/b&gt;를 가져온다. 조직도 변경만 원했는데, develop에서 별도로 진행된 변경까지 모두 딸려왔다.&lt;/p&gt;
&lt;pre class=&quot;css&quot; style=&quot;background: var(--color-background-secondary); border: 0.5px solid var(--color-border-tertiary); border-radius: 8px; padding: 1rem 1.25rem; font-size: 13px; overflow-x: auto; margin: 1rem 0;&quot;&gt;&lt;code&gt;- import GlobalAlarm from &quot;@/js/common/globalAlarm.js&quot;;
- import Menu from &quot;@/components/common/menu.vue&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;develop에서는 이미 &lt;code style=&quot;background: var(--color-background-secondary); padding: 2px 6px; border-radius: 4px; font-size: 13px;&quot;&gt;GlobalAlarm&lt;/code&gt;과 &lt;code style=&quot;background: var(--color-background-secondary); padding: 2px 6px; border-radius: 4px; font-size: 13px;&quot;&gt;Menu&lt;/code&gt;를 제거한 상태였다. 하지만 gov에서는 아직 사용 중이었고, 결과적으로 사이드바 메뉴가 통째로 사라졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: 500; margin: 2rem 0px 0.75rem;&quot; data-ke-size=&quot;size23&quot;&gt;함정 2 &amp;mdash; API 파일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code style=&quot;background: var(--color-background-secondary); padding: 2px 6px; border-radius: 4px; font-size: 13px;&quot;&gt;memberRequest.js&lt;/code&gt;에도 같은 문제가 있었다.&lt;/p&gt;
&lt;div style=&quot;display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 1rem 0;&quot;&gt;
&lt;div style=&quot;background: var(--color-background-success); border: 0.5px solid var(--color-border-success); border-radius: 8px; padding: 0.875rem 1rem;&quot;&gt;
&lt;p style=&quot;font-weight: 500; color: var(--color-text-success); margin: 0px 0px 8px;&quot; data-ke-size=&quot;size16&quot;&gt;추가해야 할 것&lt;/p&gt;
&lt;ul style=&quot;margin: 0; padding-left: 1.2rem; font-size: 13px; color: var(--color-text-primary);&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;requestOrganizations&lt;/li&gt;
&lt;li&gt;requestOrgMembers&lt;/li&gt;
&lt;li&gt;requestEgovLink&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;background: var(--color-background-danger); border: 0.5px solid var(--color-border-danger); border-radius: 8px; padding: 0.875rem 1rem;&quot;&gt;
&lt;p style=&quot;font-size: 12px; font-weight: 500; color: var(--color-text-danger); margin: 0 0 8px;&quot; data-ke-size=&quot;size16&quot;&gt;딸려오는 것&lt;/p&gt;
&lt;ul style=&quot;margin: 0; padding-left: 1.2rem; font-size: 13px; color: var(--color-text-primary);&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;requestLoginForPWA 삭제&lt;/li&gt;
&lt;li&gt;requestSetPassword 시그니처 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통째로 가져오면 gov에서 아직 쓰고 있는 함수가 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: 500; margin: 2rem 0px 0.75rem;&quot; data-ke-size=&quot;size23&quot;&gt;해결 방법&lt;/h3&gt;
&lt;h4 style=&quot;font-weight: 500; margin: 1.5rem 0px 0.5rem;&quot; data-ke-size=&quot;size20&quot;&gt;1. 새 파일은 통째로 가져와도 된다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gov에 없던 파일은 충돌 가능성이 없다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;background: var(--color-background-secondary); border: 0.5px solid var(--color-border-tertiary); border-radius: 8px; padding: 1rem 1.25rem; font-size: 13px; overflow-x: auto; margin: 1rem 0;&quot;&gt;&lt;code&gt;git checkout develop -- src/public/components/serviceUsersManagement/modal/ModalEgovLink.vue
git checkout develop -- src/public/components/serviceUsersManagement/CustomLoadingOverlay.ts
git checkout develop -- src/public/assets/css/orgChartMode.css&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;font-weight: 500; margin: 1.5rem 0px 0.5rem;&quot; data-ke-size=&quot;size20&quot;&gt;2. 기존 파일은 diff를 먼저 확인한다&lt;/h4&gt;
&lt;pre class=&quot;x86asm&quot; style=&quot;background: var(--color-background-secondary); border: 0.5px solid var(--color-border-tertiary); border-radius: 8px; padding: 1rem 1.25rem; font-size: 13px; overflow-x: auto; margin: 1rem 0;&quot;&gt;&lt;code&gt;git diff gov develop -- src/public/api/memberRequest.js&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 조직도와 무관한 변경을 식별하고, 해당 파일은 통째로 가져오지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;font-weight: 500; margin: 1.5rem 0px 0.5rem;&quot; data-ke-size=&quot;size20&quot;&gt;3. 혼합된 파일은 수동으로 필요한 부분만 추가한다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gov 버전을 유지한 채로 필요한 함수만 직접 붙여넣는다. &lt;code style=&quot;background: var(--color-background-secondary); padding: 2px 6px; border-radius: 4px; font-size: 13px;&quot;&gt;memberRequest.js&lt;/code&gt;의 경우 조직도 API 3개 함수만 마지막에 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: 500; margin: 2rem 0px 0.75rem;&quot; data-ke-size=&quot;size23&quot;&gt;체크리스트&lt;/h3&gt;
&lt;div style=&quot;background: var(--color-background-secondary); border: 0.5px solid var(--color-border-tertiary); border-radius: 8px; padding: 1rem 1.25rem;&quot;&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 14px; height: 125px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;border-bottom: 0.5px solid var(--color-border-tertiary); height: 25px;&quot;&gt;
&lt;td style=&quot;padding: 8px 0px; width: 24px; color: var(--color-text-success); height: 25px;&quot;&gt;✓&lt;/td&gt;
&lt;td style=&quot;padding: 8px 0px; height: 25px;&quot;&gt;&lt;code style=&quot;background: var(--color-background-primary); padding: 2px 6px; border-radius: 4px; font-size: 12px;&quot;&gt;git diff source target -- file&lt;/code&gt;로 전체 변경사항 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;border-bottom: 0.5px solid var(--color-border-tertiary); height: 25px;&quot;&gt;
&lt;td style=&quot;padding: 8px 0px; color: var(--color-text-success); height: 25px;&quot;&gt;✓&lt;/td&gt;
&lt;td style=&quot;padding: 8px 0px; height: 25px;&quot;&gt;새 파일인가? &amp;rarr; 통째로 가져와도 OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;border-bottom: 0.5px solid var(--color-border-tertiary); height: 25px;&quot;&gt;
&lt;td style=&quot;padding: 8px 0px; color: var(--color-text-success); height: 25px;&quot;&gt;✓&lt;/td&gt;
&lt;td style=&quot;padding: 8px 0px; height: 25px;&quot;&gt;기존 파일인가? &amp;rarr; 기능과 무관한 변경이 섞여있는지 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;border-bottom: 0.5px solid var(--color-border-tertiary); height: 25px;&quot;&gt;
&lt;td style=&quot;padding: 8px 0px; color: var(--color-text-success); height: 25px;&quot;&gt;✓&lt;/td&gt;
&lt;td style=&quot;padding: 8px 0px; height: 25px;&quot;&gt;삭제된 import/함수가 대상 브랜치에서 아직 사용 중인지 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;padding: 8px 0px; color: var(--color-text-success); height: 25px;&quot;&gt;✓&lt;/td&gt;
&lt;td style=&quot;padding: 8px 0px; height: 25px;&quot;&gt;패키지 의존성(&lt;code style=&quot;background: var(--color-background-primary); padding: 2px 6px; border-radius: 4px; font-size: 12px;&quot;&gt;package.json&lt;/code&gt;)도 같이 옮겨야 하는지 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 style=&quot;font-weight: 500; margin: 2rem 0px 0.75rem;&quot; data-ke-size=&quot;size23&quot;&gt;교훈&lt;/h3&gt;
&lt;div style=&quot;border-left: 3px solid var(--color-border-info); padding-left: 1rem; margin: 1rem 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: var(--color-text-primary);&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code style=&quot;background: var(--color-background-secondary); padding: 2px 6px; border-radius: 4px; font-size: 13px;&quot;&gt;git checkout branch -- file&lt;/code&gt;은 &quot;이 파일의 이 기능만 가져와&quot;가 아니라 &lt;b&gt;&quot;이 파일의 모든 것을 가져와&quot;&lt;/b&gt;다.&lt;/p&gt;
&lt;p style=&quot;margin: 0; color: var(--color-text-primary);&quot; data-ke-size=&quot;size16&quot;&gt;파일 단위 cherry-pick이라고 생각하면 안 되고, &lt;b&gt;파일 전체 덮어쓰기&lt;/b&gt;라고 생각해야 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/article&gt;</description>
      <category>개발자로 살아남기/트러블슈팅</category>
      <author>ming0o</author>
      <guid isPermaLink="true">https://jrzzzing.tistory.com/55</guid>
      <comments>https://jrzzzing.tistory.com/55#entry55comment</comments>
      <pubDate>Mon, 6 Apr 2026 17:04:58 +0900</pubDate>
    </item>
    <item>
      <title>[CSS] ag-grid autoHeight vs normal 모드에서 가로 스크롤 제어하기</title>
      <link>https://jrzzzing.tistory.com/54</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vue3 + ag-grid 환경에서 테이블을 만들고 있었다. 요구사항은 간단했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컬럼이 컨테이너보다 넓으면 가로 스크롤&lt;/li&gt;
&lt;li&gt;스크롤바는 표의 &lt;b&gt;맨 하단&lt;/b&gt;에 고정&lt;/li&gt;
&lt;li&gt;데이터가 없으면 스크롤바 숨김&lt;/li&gt;
&lt;li&gt;데이터가 없어도 표 영역은 화면 아래까지 채우기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단할 줄 알았는데, ag-grid의 &lt;code&gt;domLayout&lt;/code&gt; 설정에 따라 전혀 다르게 동작해서 꽤 고생했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;domLayout: autoHeight의 함정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ag-grid의 기본적인 높이 제어 방식은 두 가지다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;domLayout: &quot;autoHeight&quot; // 행 수에 맞춰 높이 자동 조정
domLayout: &quot;normal&quot; // 컨테이너 높이에 맞춤 (스크롤 포함)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;autoHeight&lt;/code&gt;를 썼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행이 적어도 표가 자연스럽게 보이니까. 그런데 문제가 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 스크롤바가 데이터 바로 밑에 붙는다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;autoHeight&lt;/code&gt;에서는 ag-grid가 행 높이만큼만 영역을 잡기 때문에, 가로 스크롤바가 마지막 행 바로 아래에 위치한다. 표 하단이 아니라 데이터 끝에 따라다니는 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 데이터가 없어도 스크롤바가 보인다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;autoSizeColumns()&lt;/code&gt;로 헤더 텍스트 기준으로 컬럼 너비를 잡으면, 헤더만으로도 컨테이너를 초과해서 가로 스크롤이 생긴다. &quot;표시할 데이터가 없습니다&quot; 상태인데 스크롤바가 떡하니 있는 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;domLayout: normal로 전환... 그런데 표가 사라졌다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;normal&lt;/code&gt; 모드로 바꾸면 스크롤바가 표 하단에 고정된다. 문제 해결! 이라고 생각했는데 표가 아예 안 보였다.&lt;br /&gt;&lt;code&gt;normal&lt;/code&gt; 모드에서는 ag-grid에 &lt;b&gt;명시적인 높이&lt;/b&gt;가 필요하다. 보통 이렇게 한다:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.ag-theme-alpine {
height: 100%;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 height: 100%는 부모에 높이가 있어야 동작한다. 내 DOM 구조는 이랬다:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.content-box
└ .pad\_0\_10
└ .row
└ .col-lg-12
└ .table-section
└ .ag-theme-alpine &amp;larr; height: 100% 인데 부모 높이가 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간 경로의 어떤 요소에도 height나 flex: 1이 없어서, 100%가 결국 0px이 된 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결: flex 체인 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모부터 ag-grid까지 빈틈없이 flex로 연결했다.&lt;/p&gt;
&lt;pre id=&quot;code_1775462147377&quot; class=&quot;css&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;css&quot;&gt;&lt;code&gt;** content-box부터 table-section까지 flex 체인 **
.svc-user-list .content-box {
    display: flex;
    flex-direction: column;
}

.svc-user-list .content-box &amp;gt; .pad_0_10 {
    flex: 1;
    display: flex;
    flex-direction: column;
}

.svc-user-list .content-box &amp;gt; .pad_0_10 &amp;gt; .row:last-child {
    flex: 1;
    display: flex;
}

.svc-user-list .content-box &amp;gt; .pad_0_10 &amp;gt; .row:last-child &amp;gt; .col-lg-12 {
    flex: 1;
    display: flex;
    flex-direction: column;
}

.svc-user-list .table-section {
    flex: 1;
    overflow: hidden;
}

.svc-user-list .table-section .ag-theme-alpine {
    height: 100%;
    width: 100%;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;하나라도 끊기면 안 된다&lt;/b&gt;는 것이다. flex: 1이 중간에 빠지면 높이 전달이 멈추고, height: 100%는 다시 0이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 2: 데이터 유무에 따른 컬럼 너비 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flex 체인을 연결하고 나니, 마지막 문제가 남았다. 데이터가 없을 때도 가로 스크롤이 보이는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 `autoSizeColumns()`가 헤더 텍스트 기준으로 컬럼을 넓히기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;사용자 휴대전화&quot; 같은 긴 헤더가 컨테이너를 넘겨버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결은 단순했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 유무에 따라 분기:&lt;/p&gt;
&lt;pre id=&quot;code_1775462228625&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const autoSizeAll = () =&amp;gt; {

    let hasRows = false;
    gridApi.value.forEachNode(() =&amp;gt; { hasRows = true; });

    if (hasRows) {
        // 데이터 있으면: 내용 기준으로 컬럼 너비 자동 조정
        const allColIds = gridColumnApi.value.getAllColumns().map(col =&amp;gt; col.colId);
        gridColumnApi.value.autoSizeColumns(allColIds, false);
    } else {
        // 데이터 없으면: 컨테이너에 맞춤 &amp;rarr; 스크롤 안 생김
        gridApi.value.sizeColumnsToFit();
    }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 setRowData() 직후에 autoSizeAll()을 호출하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 이걸 `onGridReady`에서만 호출하고, 검색 결과 변경 시에는 빼먹어서 한참 헤맸다.&lt;/p&gt;
&lt;pre id=&quot;code_1775462320847&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const searchServiceUsers = async () =&amp;gt; {
    const { items, count } = await API.requestServiceUsers(...);
    gridApi.value.setRowData(items);
    // 이걸 빼먹으면 이전 컬럼 너비가 유지되면서 스크롤이 안 사라진다
    autoSizeAll();
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 14px; height: 93px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;background: var(--color-background-secondary); height: 17px;&quot;&gt;
&lt;th style=&quot;padding: 10px 14px; text-align: left; border: 1px solid var(--color-border-tertiary); font-weight: 500; color: var(--color-text-secondary); height: 17px;&quot;&gt;항목&lt;/th&gt;
&lt;th style=&quot;padding: 10px 14px; text-align: left; border: 1px solid var(--color-border-tertiary); font-weight: 500; color: var(--color-text-primary); height: 17px;&quot;&gt;autoHeight&lt;/th&gt;
&lt;th style=&quot;padding: 10px 14px; text-align: left; border: 1px solid var(--color-border-tertiary); font-weight: 500; color: var(--color-text-primary); height: 17px;&quot;&gt;normal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;padding: 10px 14px; border: 1px solid var(--color-border-tertiary); color: var(--color-text-secondary); height: 19px;&quot;&gt;높이&lt;/td&gt;
&lt;td style=&quot;padding: 10px 14px; border: 1px solid var(--color-border-tertiary); height: 19px;&quot;&gt;행 수에 맞춤&lt;/td&gt;
&lt;td style=&quot;padding: 10px 14px; border: 1px solid var(--color-border-tertiary); height: 19px;&quot;&gt;컨테이너에 맞춤&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: var(--color-background-secondary); height: 19px;&quot;&gt;
&lt;td style=&quot;padding: 10px 14px; border: 1px solid var(--color-border-tertiary); color: var(--color-text-secondary); height: 19px;&quot;&gt;가로 스크롤 위치&lt;/td&gt;
&lt;td style=&quot;padding: 10px 14px; border: 1px solid var(--color-border-tertiary); height: 19px;&quot;&gt;데이터 바로 밑&lt;/td&gt;
&lt;td style=&quot;padding: 10px 14px; border: 1px solid var(--color-border-tertiary); height: 19px;&quot;&gt;표 하단 고정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;padding: 10px 14px; border: 1px solid var(--color-border-tertiary); color: var(--color-text-secondary); height: 19px;&quot;&gt;빈 상태&lt;/td&gt;
&lt;td style=&quot;padding: 10px 14px; border: 1px solid var(--color-border-tertiary); height: 19px;&quot;&gt;표 영역 축소&lt;/td&gt;
&lt;td style=&quot;padding: 10px 14px; border: 1px solid var(--color-border-tertiary); height: 19px;&quot;&gt;표 영역 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: var(--color-background-secondary); height: 19px;&quot;&gt;
&lt;td style=&quot;padding: 10px 14px; border: 1px solid var(--color-border-tertiary); color: var(--color-text-secondary); height: 19px;&quot;&gt;필요 조건&lt;/td&gt;
&lt;td style=&quot;padding: 10px 14px; border: 1px solid var(--color-border-tertiary); height: 19px;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;padding: 10px 14px; border: 1px solid var(--color-border-tertiary); height: 19px;&quot;&gt;부모 높이 필수 (flex 체인)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- `autoHeight`는 간편하지만 스크롤 제어가 어렵다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- `normal`은 스크롤이 깔끔하지만 부모부터 ag-grid까지 높이 체인이 하나라도 끊기면 표가 사라진다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 데이터 유무에 따라 autoSizeColumns/ sizeColumnsToFit 분기가 필요하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- `setRowData()` 후 반드시 컬럼 너비 재계산을 호출해야 한다&lt;/p&gt;</description>
      <category>개발자로 살아남기/트러블슈팅</category>
      <author>ming0o</author>
      <guid isPermaLink="true">https://jrzzzing.tistory.com/54</guid>
      <comments>https://jrzzzing.tistory.com/54#entry54comment</comments>
      <pubDate>Mon, 6 Apr 2026 17:00:39 +0900</pubDate>
    </item>
    <item>
      <title>[BFF] BFF란? express로도 가능한거야?</title>
      <link>https://jrzzzing.tistory.com/53</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;발단: 레거시에 있던 Express 서버를 보고, 이미 서버가 있는데 왜 필요하지?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 프로젝트(Express 풀스택)와 새로운 모노레포 프로젝트를 동시에 보고 있는데, 문득 이상한 점을 발견했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모노레포에서는 BFF라는 서버를 사용해 프록시 및 토큰을 사용하는데, 레거시 프로젝트에도 Express를 쓰고 있던걸 발견했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 서버가 있는데 express? 설마 예전 레거시에서 express도 bff의 역할을 한건가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 프록시를 한다면 프록시 서버도 있던걸로 아는데, 프록시 서버에서 토큰은 관리 못할라나..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시에서도 bff를 도입할 수 있었던가..?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터 꼬리에 꼬리를 물고 파보기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프록시 서버, BFF &amp;mdash; 비슷한데 뭐가 다를까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 &quot;둘 다 중간에서 요청 전달해주는 거 아냐?&quot; 라고 생각했다. 그런데 파보니까 확실히 달랐다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 직접 호출 (중간 서버 없음)&lt;/h3&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;브라우저 &amp;rarr; 백엔드 API (직접)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프론트에서 백엔드 도메인을 직접 호출&lt;/li&gt;
&lt;li&gt;API 주소가 브라우저 Network 탭에 그대로 노출&lt;/li&gt;
&lt;li&gt;CORS 문제를 프론트/백엔드 양쪽에서 해결해야 함&lt;/li&gt;
&lt;li&gt;토큰은 프론트(상태관리, localStorage)에서 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 프록시 서버&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;브라우저 &amp;rarr; 프록시 서버 &amp;rarr; 백엔드 API
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청을 받아서 백엔드로 &lt;b&gt;그대로 전달&lt;/b&gt;만 함&lt;/li&gt;
&lt;li&gt;CORS 우회, 백엔드 도메인 은닉 정도의 역할&lt;/li&gt;
&lt;li&gt;데이터 가공 없음 &amp;mdash; 백엔드 응답 그대로 내려줌&lt;/li&gt;
&lt;li&gt;&lt;b&gt;토큰은 여전히 프론트가 관리&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. BFF (Backend For Frontend)&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;브라우저 &amp;rarr; BFF 서버 &amp;rarr; 백엔드 API
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시 기능은 기본이고, 그 위에 더 많은 일을 함&lt;/li&gt;
&lt;li&gt;여러 API를 조합해서 프론트에 맞는 응답으로 가공&lt;/li&gt;
&lt;li&gt;&lt;b&gt;토큰/인증을 서버(httpOnly 쿠키)에서 관리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;프론트는 BFF 하나만 바라보면 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 표로 정리하면:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;직접 호출&lt;/th&gt;
&lt;th&gt;프록시 서버&lt;/th&gt;
&lt;th&gt;BFF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API 은닉&lt;/td&gt;
&lt;td&gt;X (다 보임)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CORS 해결&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 가공&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X (그대로 전달)&lt;/td&gt;
&lt;td&gt;O (프론트 맞춤)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API 조합&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O (여러 API &amp;rarr; 하나로)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;토큰 관리&lt;/td&gt;
&lt;td&gt;프론트 (상태관리)&lt;/td&gt;
&lt;td&gt;프론트 (상태관리)&lt;/td&gt;
&lt;td&gt;서버 (httpOnly 쿠키)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보안&lt;/td&gt;
&lt;td&gt;취약&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;td&gt;강함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은, 프록시 서버는 &quot;전달만&quot; 하니까 토큰은 프론트가 들고 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BFF는 프론트를 위한 &quot;전용 백엔드&quot;이기 때문에 토큰까지 서버가 책임진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시에서 한 단계 진화한 게 BFF인 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 레거시 Express는 뭐였을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 정리하고 나니, 다시 레거시 프로젝트가 궁금해졌다. 우리 Express 서버는 단순 프록시일까, BFF일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 확인해봤더니:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프론트 요청을 받아서 axios로 원격 백엔드 API를 대신 호출하고 있었다&lt;/li&gt;
&lt;li&gt;인증 쿠키를 Express가 직접 관리하고 백엔드에 전달하고 있었다&lt;/li&gt;
&lt;li&gt;응답 데이터를 가공해서 프론트에 내려주는 부분도 있었다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 전달이 아니라 가공과 인증까지 &amp;mdash; &lt;b&gt;사실상 BFF였다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;# 레거시 (Express 풀스택)
브라우저 &amp;rarr; Express (프론트 서빙 + BFF 겸업) &amp;rarr; 백엔드 API

# 모노레포
브라우저 &amp;rarr; Vue 앱 (프론트) &amp;rarr; BFF 서버 (독립) &amp;rarr; 백엔드 API
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차이는 &lt;b&gt;분리&lt;/b&gt;에 있었다. 레거시는 하나의 Express가 프론트 서빙도 하고 BFF도 겸업하고 있었고, 모노레포는 그 역할을 독립적으로 나눈 것이다. 새로운 개념이 추가된 게 아니라, 이미 있던 걸 정리한 거였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;잠깐, 서버가 토큰을 관리한다고?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 Express가 BFF 역할을 하고 있다는 건 알겠는데, 하나 충격적인 부분이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Express가 인증 쿠키/토큰을 직접 관리하고 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 당연히 토큰은 프론트엔드 상태관리(Pinia, Vuex)에서 관리하는 거라고 생각했다. 프록시 서버를 쓸 때도 토큰은 늘 프론트 몫이었으니까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 BFF 방식은 달랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프록시 / 직접 호출 (토큰이 프론트에 있음):&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;로그인 &amp;rarr; 백엔드가 토큰 반환 &amp;rarr; Pinia/Vuex에 저장 &amp;rarr; 매 요청마다 헤더에 붙임
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토큰이 JavaScript 메모리에 있음&lt;/li&gt;
&lt;li&gt;XSS 공격 한 방이면 털릴 수 있음&lt;/li&gt;
&lt;li&gt;개발자도구에서 토큰이 바로 보임&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BFF (토큰이 서버에 있음):&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;로그인 &amp;rarr; BFF가 백엔드에서 토큰 받음 &amp;rarr; httpOnly 쿠키로 브라우저에 내려줌
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;httpOnly 쿠키라 JavaScript로 접근 자체가 불가&lt;/li&gt;
&lt;li&gt;브라우저가 자동으로 쿠키를 보내니까 프론트가 토큰을 만질 필요 없음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;document.cookie&lt;/code&gt;로도 꺼낼 수 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BFF가 브라우저와 백엔드 사이에 껴있는 중간 서버이기 때문에 가능한 것이다. 프록시는 전달만 하니까 토큰을 관리할 이유가 없지만, BFF는 프론트를 위한 &quot;전용 백엔드&quot;이니까 토큰까지 책임지는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그러면 상태관리는 뭘 하는 거야?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 또 의문이 생겼다. BFF가 토큰을 관리하면, 상태관리 라이브러리는 뭘 하는 거지? 쓸모가 없어지는 건가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니다. 상태관리가 잘못된 게 아니라 &lt;b&gt;뭘 담느냐&lt;/b&gt;의 문제였다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;담아도 되는 것&lt;/th&gt;
&lt;th&gt;담으면 위험한 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;로그인한 유저 이름, 프로필&lt;/td&gt;
&lt;td&gt;인증 토큰 (access/refresh token)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI 상태 (사이드바, 다크모드)&lt;/td&gt;
&lt;td&gt;API 키&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버에서 받아온 데이터 캐싱&lt;/td&gt;
&lt;td&gt;민감한 개인정보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;필터/검색 조건, 페이지네이션&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역할 분담은 이렇게 된다:&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;보안 민감한 것 (토큰, 키)     &amp;rarr; BFF 서버 (httpOnly 쿠키)
화면 상태 + 공유 데이터       &amp;rarr; 상태관리 (Pinia, Vuex)
일회성 데이터                &amp;rarr; 컴포넌트 로컬 state (ref, reactive)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, BFF에서 유저 정보를 한번 받아오면 상태관리에 캐싱한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에는 API를 다시 호출하지 않고 상태관리에서 바로 꺼내 쓰면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;토큰&lt;/b&gt;은 BFF가 쿠키로 들고 있고, &lt;b&gt;토큰으로 가져온 데이터&lt;/b&gt;는 상태관리가 들고 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안이 필요한 건 서버가, 화면에 필요한 건 프론트가 &amp;mdash; 각자의 영역에서 각자의 역할을 하는 거였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프록시 서버&lt;/b&gt;는 요청을 전달만 한다. CORS와 API 은닉은 해결되지만, 토큰은 프론트가 관리해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BFF&lt;/b&gt;는 프록시에서 한 단계 진화한 것이다. 데이터 가공, API 조합, 토큰 관리까지 서버에서 처리한다.&lt;/li&gt;
&lt;li&gt;레거시 Express 풀스택도 알고 보니 &lt;b&gt;BFF 역할을 겸하고 있었다.&lt;/b&gt; 모노레포는 이걸 독립적으로 분리한 것이다.&lt;/li&gt;
&lt;li&gt;상태관리는 보안 금고가 아니라 &lt;b&gt;여러 컴포넌트가 같이 보는 칠판&lt;/b&gt;이다. 토큰 같은 민감한 건 BFF에, 화면 데이터는 상태관리에 두는 게 맞다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Dev/Backend</category>
      <author>ming0o</author>
      <guid isPermaLink="true">https://jrzzzing.tistory.com/53</guid>
      <comments>https://jrzzzing.tistory.com/53#entry53comment</comments>
      <pubDate>Fri, 3 Apr 2026 18:15:00 +0900</pubDate>
    </item>
    <item>
      <title>[BFF/모노레포] 모노레포 환경에서의 BFF 구조와 pnpm</title>
      <link>https://jrzzzing.tistory.com/52</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;BFF (Backend For Frontend) 란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드를 위한 전용 백엔드 서버다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 프론트엔드는 백엔드 API를 직접 호출하지만, 프로젝트가 커지면 몇 가지 문제가 생긴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 화면을 그리기 위해 API를 3~4개씩 호출해야 한다&lt;/li&gt;
&lt;li&gt;백엔드 응답에 프론트가 쓰지 않는 필드가 잔뜩 들어있다&lt;/li&gt;
&lt;li&gt;CORS, 인증 토큰 처리를 프론트에서 직접 해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BFF는 이 사이에 끼어서 &lt;b&gt;프론트가 편하게 쓸 수 있는 API를 만들어주는 중간 서버&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 일반 구조
브라우저 &amp;rarr; 백엔드 API

# BFF 구조
브라우저 &amp;rarr; BFF 서버 &amp;rarr; 백엔드 API
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BFF가 하는 일&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API 조합&lt;/td&gt;
&lt;td&gt;여러 백엔드 API를 호출해서 하나로 합쳐 내려줌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 가공&lt;/td&gt;
&lt;td&gt;프론트에 필요한 필드만 골라서 응답&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프록시&lt;/td&gt;
&lt;td&gt;CORS 우회, 인증 헤더 자동 부착&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인증 처리&lt;/td&gt;
&lt;td&gt;로그인/토큰 갱신 등을 BFF에서 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;간단 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트에서 대시보드 화면을 그리려면 유저 정보 + 알림 목록 + 통계 데이터가 필요하다고 가정하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BFF 없이:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;// 프론트에서 3번 호출
const user = await fetch('/api/user/me')
const alerts = await fetch('/api/alerts')
const stats = await fetch('/api/dashboard/stats')
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BFF 있으면:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;// 프론트에서 1번 호출
const dashboard = await fetch('/bff/dashboard')
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// BFF 서버 (Node.js)
app.get('/bff/dashboard', async (req, res) =&amp;gt; {
  const [user, alerts, stats] = await Promise.all([
    fetchFromBackend('/api/user/me'),
    fetchFromBackend('/api/alerts'),
    fetchFromBackend('/api/dashboard/stats'),
  ])

  res.json({
    userName: user.name,
    alertCount: alerts.length,
    recentAlerts: alerts.slice(0, 5),
    totalCount: stats.total,
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트 입장에서는 호출 한 번이면 끝이고, 필요한 데이터만 깔끔하게 내려온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모노레포에서 BFF는 어떻게 쓰일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모노레포(Monorepo)는 여러 프로젝트를 하나의 저장소에서 관리하는 방식이다. 프론트엔드 앱이 여러 개 있을 때, 각 앱마다 API 호출 로직을 따로 만들면 중복이 심해진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반적인 모노레포 + BFF 구조&lt;/h3&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;monorepo/
├── apps/
│   ├── console/          # 메인 콘솔 앱 (Vue)
│   ├── auth/             # 인증 앱 (로그인/회원가입)
│   └── admin/            # 관리자 앱
├── packages/
│   ├── shared-ui/        # 공통 컴포넌트
│   ├── api-client/       # API 호출 함수 모음
│   └── types/            # 공통 타입 정의
├── bff/
│   └── server/           # BFF 서버 (Express/Fastify)
├── pnpm-workspace.yaml
└── package.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API 호출 흐름&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Vue 컴포넌트
  &amp;rarr; packages/api-client의 request 함수
    &amp;rarr; BFF 서버 (프록시)
      &amp;rarr; 실제 백엔드 API
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계를 예시로 보면:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1단계: 컴포넌트에서 호출&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;script setup&amp;gt;
import { getDeviceList } from '@packages/api-client'

const devices = ref([])

onMounted(async () =&amp;gt; {
  devices.value = await getDeviceList()
})
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2단계: api-client 패키지&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// packages/api-client/device.js
import { request } from './request'

export const getDeviceList = () =&amp;gt; {
  return request.get('/api/devices')
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3단계: BFF 프록시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// bff/server/routes/devices.js
router.get('/api/devices', async (req, res) =&amp;gt; {
  const token = req.headers.authorization

  const response = await axios.get(`${BACKEND_URL}/devices`, {
    headers: { Authorization: token },
  })

  // 프론트에 필요한 필드만 가공해서 내려줌
  const devices = response.data.map((d) =&amp;gt; ({
    id: d.device_id,
    name: d.device_name,
    status: d.is_active ? 'active' : 'inactive',
  }))

  res.json(devices)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인증 처리 흐름 (IAM 리다이렉트 방식)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모노레포에서 인증 앱이 분리되어 있는 경우, 보통 이런 흐름을 따른다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 콘솔 앱 접속&lt;/li&gt;
&lt;li&gt;토큰이 없으면 &amp;rarr; 인증 앱(IAM)으로 리다이렉트&lt;/li&gt;
&lt;li&gt;인증 앱에서 로그인&lt;/li&gt;
&lt;li&gt;토큰 발급 후 &amp;rarr; 콘솔 앱으로 리다이렉트 (토큰 포함)&lt;/li&gt;
&lt;li&gt;콘솔 앱이 토큰 저장 &amp;rarr; 이후 BFF 요청 시 헤더에 포함&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 간단한 인증 가드 예시
router.beforeEach((to, from, next) =&amp;gt; {
  const token = localStorage.getItem('access_token')

  if (!token &amp;amp;&amp;amp; to.meta.requiresAuth) {
    // 인증 앱으로 리다이렉트
    window.location.href = `${IAM_URL}/login?redirect=${window.location.href}`
    return
  }

  next()
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;pnpm vs npm &amp;mdash; 뭐가 다를까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모노레포를 쓰면서 pnpm을 처음 접했는데, npm과 꽤 다르다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 차이&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;npm&lt;/th&gt;
&lt;th&gt;pnpm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;node_modules 구조&lt;/td&gt;
&lt;td&gt;플랫(flat) &amp;mdash; 모든 패키지가 최상위에 깔림&lt;/td&gt;
&lt;td&gt;심볼릭 링크 &amp;mdash; 실제 파일은 글로벌 store에 한 벌만 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;디스크 사용량&lt;/td&gt;
&lt;td&gt;프로젝트마다 중복 설치&lt;/td&gt;
&lt;td&gt;하드링크로 공유, 디스크 절약&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;유령 의존성&lt;/td&gt;
&lt;td&gt;발생함 (직접 설치 안 한 패키지도 import 가능)&lt;/td&gt;
&lt;td&gt;차단됨 (직접 선언한 것만 접근 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모노레포 지원&lt;/td&gt;
&lt;td&gt;workspaces 있지만 기본적&lt;/td&gt;
&lt;td&gt;네이티브 workspace 지원이 강력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;속도&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;td&gt;빠름 (캐시 + 하드링크)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;유령 의존성(Phantom Dependency)이 뭔데?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm의 가장 큰 문제 중 하나다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;// package.json에 axios만 설치했다고 가정
{
  &quot;dependencies&quot;: {
    &quot;axios&quot;: &quot;^1.0.0&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;axios가 내부적으로 &lt;code&gt;follow-redirects&lt;/code&gt;를 사용한다. npm은 node_modules를 flat하게 깔기 때문에:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;node_modules/
├── axios/
└── follow-redirects/    &amp;larr; axios의 의존성인데 최상위에 깔림
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 내가 &lt;code&gt;follow-redirects&lt;/code&gt;를 직접 설치하지 않았는데도 import할 수 있다. 이게 &lt;b&gt;유령 의존성&lt;/b&gt;이다. axios가 버전업하면서 이 패키지를 안 쓰게 되면? 내 코드가 갑자기 깨진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pnpm은 이걸 구조적으로 막는다:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;node_modules/
├── .pnpm/               &amp;larr; 실제 패키지는 여기에
│   ├── axios@1.0.0/
│   └── follow-redirects@1.15.0/
└── axios -&amp;gt; .pnpm/axios@1.0.0   &amp;larr; 심볼릭 링크 (내가 설치한 것만 접근 가능)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;follow-redirects&lt;/code&gt;는 최상위에 링크가 없으므로 import 자체가 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;pnpm workspace 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모노레포에서 pnpm을 쓸 때는 루트에 &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;을 만든다:&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
  - 'bff/*'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지 간 참조도 간단하다:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;// apps/console/package.json
{
  &quot;dependencies&quot;: {
    &quot;@packages/api-client&quot;: &quot;workspace:*&quot;,
    &quot;@packages/shared-ui&quot;: &quot;workspace:*&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;workspace:*&lt;/code&gt;는 &quot;이 모노레포 안에 있는 패키지를 쓰겠다&quot;는 뜻이다. npm registry에서 받는 게 아니라 로컬 패키지를 직접 참조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;BFF&lt;/b&gt;는 프론트 전용 백엔드. API 조합, 데이터 가공, 인증 처리를 중간에서 해준다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모노레포&lt;/b&gt;에서 BFF를 쓰면 여러 앱이 하나의 BFF를 공유하면서 API 호출 로직 중복을 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;pnpm&lt;/b&gt;은 npm의 유령 의존성 문제를 해결하고, 디스크도 아끼고, 모노레포 지원도 강력하다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Dev/Backend</category>
      <author>ming0o</author>
      <guid isPermaLink="true">https://jrzzzing.tistory.com/52</guid>
      <comments>https://jrzzzing.tistory.com/52#entry52comment</comments>
      <pubDate>Fri, 3 Apr 2026 15:28:17 +0900</pubDate>
    </item>
  </channel>
</rss>