<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Devvvvvv</title>
    <link>https://danbom425.tistory.com/</link>
    <description>FE Developer</description>
    <language>ko</language>
    <pubDate>Fri, 25 Sep 2026 05:51:00 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Whiimsy</managingEditor>
    <image>
      <title>Devvvvvv</title>
      <url>https://tistory1.daumcdn.net/tistory/4348094/attach/1b953f9ab3ef47e3819dd76a33197a66</url>
      <link>https://danbom425.tistory.com</link>
    </image>
    <item>
      <title>PDF 위 박스가 어긋나는 건 확대 탓이 아니다</title>
      <link>https://danbom425.tistory.com/117</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;브라우저에서 문서 다루기 #2 &amp;mdash; 박스를 화면 좌표로 저장하면 배율이 바뀔 때 어긋나고, PDF 좌표로 저장해도 y만 뒤집으면 잘린 PDF와 돌아간 PDF에서 어긋나요. 그린 것과 같은 viewport로 바꾸면 셋 다 사라집니다.&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cover-s2-2.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFi5cu/dJMcah0eCza/TePlDE1ZYa5KfBWRfgs5T0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFi5cu/dJMcah0eCza/TePlDE1ZYa5KfBWRfgs5T0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFi5cu/dJMcah0eCza/TePlDE1ZYa5KfBWRfgs5T0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFi5cu%2FdJMcah0eCza%2FTePlDE1ZYa5KfBWRfgs5T0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;브라우저에서 문서 다루기 2편 커버&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-filename=&quot;cover-s2-2.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://danbom425.tistory.com/116&quot;&gt;지난 글&lt;/a&gt;에서 react-pdf를 Next.js에 띄우는 데까지 했어요. 띄우고 나면 바로 다음 요구가 오는데요, PDF 위에 박스를 그리고 저장했다가 다시 열었을 때 같은 자리에 보여 달라는 거죠. 이 글에서는 그 박스가 왜 어긋나는지, 무엇을 저장해야 안 어긋나는지 정리해 볼게요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 만든 박스는 제자리에 있다가 확대하면 슬금슬금 밀려났어요. 배율 문제를 고치고 나니 이번엔 어떤 PDF에서만 옆으로 비껴났고요. 두 번 다 원인은 좌표를 무엇으로 저장했느냐였어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;좌표계가 두 개예요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PDF 안의 좌표는 포인트(1/72인치) 단위이고, 원점이 페이지 &lt;b&gt;왼쪽 아래&lt;/b&gt;, y는 &lt;b&gt;위로&lt;/b&gt; 커집니다. 화면은 CSS 픽셀 단위에 원점이 왼쪽 위, y가 아래로 커지죠. 박스가 어긋나는 문제는 거의 다 이 둘을 손으로 잇다가 생겨요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pdf.js에는 둘을 잇는 객체가 따로 있어요. &lt;code&gt;page.getViewport()&lt;/code&gt;가 돌려주는 viewport입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1790140955395&quot; class=&quot;yaml&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const viewport = page.getViewport({ scale: 1 })
viewport.transform                         // [1, 0, 0, -1, 0, 792]
viewport.convertToViewportPoint(100, 700)  // [100, 92]  PDF &amp;rarr; 화면
viewport.convertToPdfPoint(100, 92)        // [100, 700] 화면 &amp;rarr; PDF&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;높이 792포인트짜리 레터 용지라면 &lt;code&gt;[1, 0, 0, -1, 0, 792]&lt;/code&gt;는 &quot;x는 그대로, y는 792에서 뺀다&quot;는 뜻이에요. 여기까지만 보면 손으로 뒤집어도 될 것 같은데요, 이 행렬은 페이지마다 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인하려고 세 쪽짜리 PDF를 만들었어요. 세 쪽 모두 PDF 좌표 (100, 600)&amp;ndash;(250, 700)에 파란 사각형이 그려져 있고, 그 위에 박스를 얹어서 겹치는지 봅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A&lt;/b&gt; &amp;mdash; 평범한 쪽. MediaBox &lt;code&gt;[0 0 612 792]&lt;/code&gt;&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;B&lt;/b&gt; &amp;mdash; CropBox가 &lt;code&gt;[50 100 562 742]&lt;/code&gt;인 쪽. 스캔본이나 여백을 잘라낸 PDF에서 흔해요&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;C&lt;/b&gt; &amp;mdash; &lt;code&gt;/Rotate 90&lt;/code&gt;인 쪽. 가로 문서를 세로 용지에 저장하고 돌려 둔 경우예요&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;화면 좌표로 저장하면 저장한 배율에서만 맞아요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 사용자가 그린 박스의 CSS 좌표를 그대로 저장했어요. A쪽을 scale 1.5로 띄워 놓고 사각형 위에 그리면 &lt;code&gt;{ left: 150, top: 138, width: 225, height: 150 }&lt;/code&gt;이 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 박스를 다른 배율에서 다시 그려 볼게요. 빨간 박스가 저장한 값, 초록 박스가 PDF 좌표에서 바꾼 값입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;fig-scale.png&quot; data-origin-width=&quot;2316&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m4mf1/dJMcadKi3Tf/XwyEWAR9yz8gRJnCuVVUN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m4mf1/dJMcadKi3Tf/XwyEWAR9yz8gRJnCuVVUN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m4mf1/dJMcadKi3Tf/XwyEWAR9yz8gRJnCuVVUN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm4mf1%2FdJMcadKi3Tf%2FXwyEWAR9yz8gRJnCuVVUN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;같은 박스를 scale 1, 1.5, 2에서 그린 비교. 화면 좌표로 저장한 빨간 박스는 1.5에서만 맞는다&quot; loading=&quot;lazy&quot; width=&quot;2316&quot; height=&quot;674&quot; data-filename=&quot;fig-scale.png&quot; data-origin-width=&quot;2316&quot; data-origin-height=&quot;674&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scale 1에서는 정답이 (100, 92)인데 저장한 박스는 (150, 138)에 있어서, 왼쪽 위만 68px 떨어져요. 크기도 1.5배로 커져 있고요. scale 2에서는 반대로 박스가 작고 왼쪽 위로 올라가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함정이 하나 더 있어요. &quot;100%&quot;가 라이브러리마다 같은 크기가 아니거든요. pdf.js 기본 뷰어의 100%는 1포인트를 96/72 CSS px로 그려서 레터 용지 폭이 816px이고, react-pdf의 &lt;code&gt;scale={1}&lt;/code&gt;은 1포인트를 1px로 그려서 612px이에요. pdf.js 뷰어에서 잡은 좌표를 react-pdf 화면에 옮기면 배율을 맞췄다고 생각해도 1.33배 차이가 납니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PDF 좌표로 저장해도 y만 뒤집으면 두 곳에서 틀려요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 PDF 좌표로 저장하기로 했어요. 화면에 그릴 때는 흔히 이렇게 바꿉니다.&lt;/p&gt;
&lt;pre id=&quot;code_1790140955398&quot; class=&quot;processing&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 원점이 (0, 0)이고 회전이 없다고 가정한 코드
const left = x * scale
const top = (page.view[3] - y) * scale&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A쪽에서는 이 코드가 맞아요. B와 C에서 어떻게 되는지 같이 볼게요. 주황 점선이 이 코드, 초록이 viewport로 바꾼 값입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;fig-crop-rotate.png&quot; data-origin-width=&quot;2196&quot; data-origin-height=&quot;982&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btvF6R/dJMcacdBRmr/dd0qn4rljYju5N6FuWOhfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btvF6R/dJMcacdBRmr/dd0qn4rljYju5N6FuWOhfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btvF6R/dJMcacdBRmr/dd0qn4rljYju5N6FuWOhfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtvF6R%2FdJMcacdBRmr%2Fdd0qn4rljYju5N6FuWOhfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;A, B, C 세 쪽에서 y만 뒤집은 주황 박스와 viewport로 바꾼 초록 박스 비교&quot; loading=&quot;lazy&quot; width=&quot;2196&quot; height=&quot;982&quot; data-filename=&quot;fig-crop-rotate.png&quot; data-origin-width=&quot;2196&quot; data-origin-height=&quot;982&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scale 1에서 박스 왼쪽 위 좌표를 표로 정리하면 이래요.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;쪽&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;viewport로 변환 (정답)&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;y만 뒤집기&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;화면 좌표 (1.5에서 저장)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;A &amp;middot; 평범한 쪽&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;(100, 92)&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;(100, 92)&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;(150, 138)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;B &amp;middot; CropBox 원점 (50, 100)&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;(50, 42)&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;(100, 42)&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;(150, 138)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;C &amp;middot; /Rotate 90&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;(600, 100), 100 &amp;times; 150&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;(100, 92), 150 &amp;times; 100&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;(150, 138)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 쪽 모두에서 맞는 건 첫 열뿐이에요. 나머지 두 방식은 A쪽에서만, 그것도 한쪽은 저장한 배율에서만 맞습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B쪽은 화면 왼쪽 위가 PDF의 (0, 792)가 아니라 (50, 742)예요. viewport 행렬도 &lt;code&gt;[1, 0, 0, -1, -50, 742]&lt;/code&gt;로 x를 50만큼 당깁니다. &lt;code&gt;view[3]&lt;/code&gt;을 써서 y는 맞았지만 x는 원점만큼, scale 2라면 100px 밀려요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C쪽은 방향부터 달라요. &lt;code&gt;/Rotate 90&lt;/code&gt;은 시계 방향으로 90도 돌려서 보여 주라는 뜻이라, 화면이 792 &amp;times; 612로 눕고 행렬이 &lt;code&gt;[0, 1, 1, 0, 0, 0]&lt;/code&gt;이 됩니다. 화면 x가 PDF y, 화면 y가 PDF x예요. 박스의 가로세로도 서로 바뀝니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-pdf를 쓴다면 버전도 봐야 해요. 10.2.0까지는 &lt;code&gt;page.originalHeight&lt;/code&gt;가 &lt;code&gt;view[3]&lt;/code&gt;, 그러니까 위 코드와 같은 값이었어요. 10.3.0에서 회전한 쪽 크기를 바로잡으면서 &lt;code&gt;getViewport({ scale: 1 })&lt;/code&gt; 기준으로 바뀌었고요(#2027). &lt;code&gt;originalHeight - y&lt;/code&gt;로 뒤집던 코드는 버전을 올리는 순간 B쪽에서 top이 &amp;minus;58, C쪽에서 &amp;minus;88이 되어 박스가 페이지 밖으로 나갑니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그린 것과 같은 viewport로 바꾸기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 행렬을 손으로 만들지 않는 게 답이었어요. 저장은 PDF 좌표로 하고, 그릴 때마다 &lt;b&gt;화면을 그린 것과 같은 viewport&lt;/b&gt;에게 바꿔 달라고 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1790140955403&quot; class=&quot;reasonml&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 저장 &amp;rarr; 화면: PDF 사각형 [x1, y1, x2, y2]를 CSS 상자로
function toScreenRect(page, scale, [x1, y1, x2, y2]) {
  const vp = page.getViewport({ scale }) // rotation을 안 넘기면 page.rotate를 씁니다
  const [ax, ay] = vp.convertToViewportPoint(x1, y1)
  const [bx, by] = vp.convertToViewportPoint(x2, y2)
  // 회전하면 꼭짓점 순서가 바뀌어서 min/abs로 정리합니다
  return {
    left: Math.min(ax, bx),
    top: Math.min(ay, by),
    width: Math.abs(bx - ax),
    height: Math.abs(by - ay),
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점 두 개를 따로 바꾸는 데는 이유가 있어요. pdfjs-dist 5.7에는 사각형을 한 번에 바꾸는 &lt;code&gt;convertToViewportRectangle&lt;/code&gt;이 있는데, pdf.js 6.4 데모 뷰어의 viewport에는 이 메서드가 없었어요. 점 변환은 두 버전 모두에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;같은 viewport&quot;가 중요한 이유는 회전 때문이에요. pdf.js의 &lt;code&gt;rotation&lt;/code&gt;도, react-pdf의 &lt;code&gt;rotate&lt;/code&gt;도 &lt;b&gt;더할 각도가 아니라 최종 각도&lt;/b&gt;입니다. &lt;code&gt;/Rotate 90&lt;/code&gt;인 쪽에 &lt;code&gt;rotate={90}&lt;/code&gt;을 주면 더 돌아가지 않고 파일에 적힌 그대로 보여요. 사용자가 돌리는 기능을 붙인다면 두 곳에 같은 값을 넘겨야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1790140955404&quot; class=&quot;processing&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const rotation = (page.rotate + userRotate) % 360

// 화면
&amp;lt;Page pageNumber={n} scale={scale} rotate={rotation} /&amp;gt;
// 좌표 변환
page.getViewport({ scale, rotation })&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;클릭은 canvas가 아니라 CSS 상자 기준으로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대 방향, 사용자가 누른 곳을 PDF 좌표로 바꿀 때도 함정이 있어요. react-pdf는 캔버스를 화면 배율(devicePixelRatio)만큼 크게 그리고 CSS로 줄여서 보여 주거든요.&lt;/p&gt;
&lt;pre id=&quot;code_1790140955405&quot; class=&quot;processing&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// react-pdf Page/Canvas.tsx (10.4.1)
page.getViewport({ scale: scale * devicePixelRatio, rotation: rotate }) // 캔버스를 그리는 viewport
canvas.width = renderViewport.width
canvas.style.width = `${Math.floor(viewport.width)}px`&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 쓴 PC는 Windows 배율 125%라서, scale 1인 레터 용지가 CSS로는 612px인데 &lt;code&gt;canvas.width&lt;/code&gt;는 765예요. &lt;code&gt;canvas.width&lt;/code&gt;로 배율을 계산하면 모든 박스가 1.25배 밀리고, 레티나 화면에서는 2배 밀립니다. 내 화면에서 멀쩡하던 박스가 동료 화면에서만 틀어진다면 여기를 먼저 의심해 보세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 클릭 위치는 화면에 보이는 CSS 상자를 기준으로 잽니다.&lt;/p&gt;
&lt;pre id=&quot;code_1790140955405&quot; class=&quot;reasonml&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 화면 &amp;rarr; 저장: 클릭한 점을 PDF 좌표로
function toPdfPoint(e, pageElement, page, scale) {
  const r = pageElement.getBoundingClientRect() // canvas.width가 아니라 CSS 상자
  return page
    .getViewport({ scale })
    .convertToPdfPoint(e.clientX - r.left, e.clientY - r.top)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PDF 좌표가 늘 정답은 아니에요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 PDF 좌표로 저장하는 게 모든 경우의 정답은 아닙니다. 형광펜처럼 &lt;b&gt;글자에 붙는 주석&lt;/b&gt;은 문서가 다시 만들어지면 좌표가 그대로여도 글자가 옮겨 가요. 그럴 때는 좌표 대신 인용한 문장과 앞뒤 문맥을 저장하는 방식(W3C Web Annotation의 TextQuoteSelector)이 더 버팁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 서명란, 도장 자리처럼 &lt;b&gt;종이의 위치&lt;/b&gt;에 붙는 표시는 PDF 좌표가 맞아요. pdf.js가 뽑아 주는 텍스트 좌표(&lt;code&gt;getTextContent&lt;/code&gt;의 &lt;code&gt;transform&lt;/code&gt;)도 PDF 좌표라서 맞춰 보기도 쉽고요. 두 방식을 한 화면에서 섞을 때 어느 쪽을 기준으로 둘지는 저도 아직 정하지 못했어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직접 해보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에도 최소 재현 저장소를 만들었어요. 1편과 같이 &lt;code&gt;next.config.ts&lt;/code&gt;는 비어 있고, 위의 세 쪽짜리 PDF에 박스 세 개를 얹어 두었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/danbom/pdf-overlay-coords&quot;&gt;github.com/danbom/pdf-overlay-coords&lt;/a&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;배율을 바꿔도 초록 박스만 파란 사각형에 붙어 있나요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;B쪽에서 주황 박스가 오른쪽으로 50만큼 비껴나나요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;C쪽에서 주황 박스의 가로세로가 초록과 반대인가요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;페이지를 누른 점의 PDF 좌표가 배율을 바꿔도 그대로인가요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;canvas.width &amp;divide; 쪽 폭&lt;/code&gt;이 내 화면의 &lt;code&gt;devicePixelRatio&lt;/code&gt;와 scale을 곱한 값인가요?&lt;/li&gt;
&lt;/ul&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 data-ke-size=&quot;size16&quot;&gt;박스 좌표를 저장할 때 단위가 CSS px인가요, PDF 포인트인가요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;테스트용 PDF 중에 CropBox 원점이 0이 아닌 파일과 &lt;code&gt;/Rotate&lt;/code&gt;가 있는 파일이 있나요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;좌표를 바꾸는 viewport와 화면을 그리는 viewport가 같은 scale, 같은 rotation으로 만들어지나요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;클릭 위치를 &lt;code&gt;canvas.width&lt;/code&gt; 기준으로 계산하고 있지는 않나요?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;나라면 이렇게 했을 텐데&quot; 싶은 지점이 있었다면, 그 얘기를 듣고 싶어요. 특히 글자에 붙는 주석과 종이에 붙는 표시를 한 저장소에 섞어 본 경험이 궁금합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 텍스트 레이어에서 드래그로 고른 글자 범위를 PDF 좌표로 저장하는 쪽을 다뤄 보려고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&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 data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/danbom/pdf-overlay-coords&quot;&gt;danbom/pdf-overlay-coords&lt;/a&gt; &amp;mdash; 이 글의 최소 재현 저장소&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;mozilla/pdf.js &amp;mdash; &lt;code&gt;PageViewport&lt;/code&gt;(&lt;code&gt;convertToViewportPoint&lt;/code&gt;, &lt;code&gt;convertToPdfPoint&lt;/code&gt;), &lt;code&gt;PixelsPerInch.PDF_TO_CSS_UNITS&lt;/code&gt;&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;wojtekmaj/react-pdf &amp;mdash; &lt;code&gt;Page/Canvas.tsx&lt;/code&gt;, &lt;code&gt;shared/utils.ts&lt;/code&gt;의 &lt;code&gt;makePageCallback&lt;/code&gt;, v10.3.0 릴리스 노트(#2027)&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;ISO 32000-1:2008 &amp;mdash; 14.11.2 Page Boundaries(MediaBox, CropBox), 7.7.3.3 Page Objects(&lt;code&gt;/Rotate&lt;/code&gt;)&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;W3C Web Annotation Data Model &amp;mdash; TextQuoteSelector&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>브라우저에서 문서 다루기</category>
      <author>Whiimsy</author>
      <guid isPermaLink="true">https://danbom425.tistory.com/117</guid>
      <comments>https://danbom425.tistory.com/117#entry117comment</comments>
      <pubDate>Wed, 23 Sep 2026 14:25:58 +0900</pubDate>
    </item>
    <item>
      <title>pdf.js를 Next.js에 붙이는 방법은 이미 반쯤 바뀌었다</title>
      <link>https://danbom425.tistory.com/116</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0AwLA/dJMb99OClw8/1Mk901F9w0fKCbMBnpkHY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0AwLA/dJMb99OClw8/1Mk901F9w0fKCbMBnpkHY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0AwLA/dJMb99OClw8/1Mk901F9w0fKCbMBnpkHY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0AwLA%2FdJMb99OClw8%2F1Mk901F9w0fKCbMBnpkHY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;브라우저에서 문서 다루기 #1 &amp;mdash; 검색해서 나오는 설정 네 가지 중 셋은 버려도 되고, 하나는 지금도 필요해요. 빈 next.config로 직접 확인해 볼게요.&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PDF를 웹에서 보여줘야 할 일이 생기면 보통 &lt;code&gt;react-pdf&lt;/code&gt;로 갑니다. 그리고 검색을 하면 거의 모든 글이 &lt;code&gt;next.config&lt;/code&gt;부터 고치라고 하는데요, 그대로 따라 했다가 오히려 한참 헤맸어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 그 처방들이 지금도 유효한지 하나씩 확인하고, &lt;b&gt;왜 그중 하나만 살아남는지&lt;/b&gt; 정리해 볼게요. 확인은 빈 설정으로 만든 최소 재현 저장소로 했습니다.&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 id=&quot;code_1790126889185&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ① 압축 끄기
module.exports = { swcMinify: false }

// ② canvas를 빈 모듈로 보내기
webpack: (config) =&amp;gt; { config.resolve.alias.canvas = false; return config }

// ③ 워커를 public/ 으로 복사하고 경로 직접 지정
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js'

// ④ 컴포넌트를 next/dynamic 으로 감싸고 ssr: false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넷 다 실제로 있었던 문제에서 나왔어요. 그래서 보통 한 묶음으로 취급됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;셋은 정말 없어졌어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-pdf &lt;b&gt;v9.2.0&lt;/b&gt; 릴리스 노트에 이렇게 적혀 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;This version updates PDF.js to 4.8.69, significantly simplifying setup in Next.js. &lt;b&gt;You no longer need to do any changes to Next.js config!&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말만 믿기엔 찜찜해서 직접 확인했어요. &lt;code&gt;next.config.ts&lt;/code&gt;를 &lt;b&gt;완전히 비운 채로&lt;/b&gt; Next.js 16.3.6 &amp;middot; Turbopack &amp;middot; pdfjs-dist 5.4.296에서 돌려봤습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;처방&lt;/b&gt;&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;① &lt;code&gt;swcMinify: false&lt;/code&gt;&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;없어도 정상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;② canvas 별칭&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Can't resolve 'canvas'&lt;/code&gt; 안 나옴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;③ 워커 복사&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;복사 안 해도 로드됨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 ③이 재미있는데요, 워커 경로를 화면에 찍어보니 이렇게 나왔어요.&lt;/p&gt;
&lt;pre id=&quot;code_1790126889187&quot; class=&quot;gradle&quot; data-ke-language=&quot;plain&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/_next/static/media/pdf.worker.min.2th4soq4xwzz7.mjs&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;public/&lt;/code&gt;에 아무것도 넣지 않았는데 &lt;b&gt;Turbopack이 워커를 직접 번들하고 해시까지 붙였습니다.&lt;/b&gt; 코드는 이 한 줄이 전부예요.&lt;/p&gt;
&lt;pre id=&quot;code_1790126889187&quot; class=&quot;stylus&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pdfjs.GlobalWorkerOptions.workerSrc = new URL(
  'pdfjs-dist/build/pdf.worker.min.mjs',
  import.meta.url,
).toString()&lt;/code&gt;&lt;/pre&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;새 버그를 만듭니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워커를 &lt;code&gt;public/&lt;/code&gt;에 복사하는 순간 그 파일은 그때 버전으로 고정돼요. 나중에 &lt;code&gt;pdfjs-dist&lt;/code&gt;를 올리면 본체만 올라가고 워커는 옛날 것이 남아서 이렇게 터집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1790126889188&quot; class=&quot;subunit&quot; data-ke-language=&quot;plain&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Error: The API version &quot;5.x.x&quot; does not match the Worker version &quot;4.x.x&quot;.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;new URL(..., import.meta.url)&lt;/code&gt;은 번들러가 &lt;b&gt;실제로 설치된 패키지&lt;/b&gt;를 해석하니까 어긋날 수가 없습니다. CDN 주소를 박는 방법도 같은 이유로 안 써요. 사내망이나 오프라인에서 죽고, 버전 고정을 사람이 기억해야 하거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건이 하나 있는데요, react-pdf README가 &lt;b&gt;workerSrc는 컴포넌트를 쓰는 같은 모듈에서 설정해야 한다&lt;/b&gt;고 못박고 있습니다. 설정 파일이나 공통 모듈에 몰아두면 기본값이 덮어쓸 수 있어요.&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;&lt;code&gt;'use client'&lt;/code&gt;를 붙였으니 브라우저에서만 돌겠거니 하고 그냥 import 했더니, 첫 요청이 &lt;b&gt;500&lt;/b&gt;으로 떨어졌습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1790126889188&quot; class=&quot;angelscript&quot; data-ke-language=&quot;plain&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ReferenceError: DOMMatrix is not defined
app/pdf-viewer.tsx (4:1) @ module evaluation
&amp;gt; 4 | import { Document, Page, pdfjs } from 'react-pdf'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택에 찍힌 경로가 원인을 그대로 말해줘요.&lt;/p&gt;
&lt;pre id=&quot;code_1790126889188&quot; class=&quot;awk&quot; data-ke-language=&quot;plain&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.next/dev/server/chunks/ssr/node_modules_...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;server/chunks/ssr.&lt;/b&gt; App Router에서 &lt;code&gt;'use client'&lt;/code&gt;는 &quot;서버에서 실행되지 않는다&quot;가 아니라 &lt;b&gt;&quot;클라이언트 번들에도 포함된다&quot;&lt;/b&gt;는 표시예요. 서버 렌더링은 그대로 한 번 일어납니다. 그래서 파일 맨 위 &lt;code&gt;import&lt;/code&gt; 줄이 Node에서 평가되고, 거기서 끝납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;npm run build&lt;/code&gt;를 돌리면 더 선명해져요. pdf.js 소스의 몇 번째 줄인지까지 나옵니다.&lt;/p&gt;
&lt;pre id=&quot;code_1790126889189&quot; class=&quot;subunit&quot; data-ke-language=&quot;plain&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Error occurred prerendering page &quot;/&quot;
ReferenceError: DOMMatrix is not defined
    at module evaluation (webpack://pdf.js/src/display/canvas.js:63:22)
&amp;gt; 63 | const SCALE_MATRIX = new DOMMatrix();
Export encountered an error on /page: /, exiting the build.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DOMMatrix&lt;/code&gt;는 브라우저 API인데 &lt;b&gt;모듈 최상단에서&lt;/b&gt; 객체를 하나 만들어 둡니다. 함수 안이 아니라 최상단이라, import 되는 순간 실행돼요. 조건부 렌더링으로는 못 막습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 ④는 살아 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1790126889189&quot; class=&quot;coffeescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const PdfViewer = dynamic(() =&amp;gt; import('./pdf-viewer'), { ssr: false })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 감싼 경로는 같은 조건에서 콘솔이 깨끗했어요.&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;&quot;이제 Next.js 설정 바꿀 필요 없다&quot;는 문장은 &lt;b&gt;①②③에 대해서만 참&lt;/b&gt;인데, 넷을 세트로 외운 사람은 ④까지 빼게 됩니다. 그리고 &lt;code&gt;dev&lt;/code&gt;에서 화면이 뜨기 때문에 한동안 모르다가 &lt;b&gt;빌드에서 처음 터져요.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고쳐진 것과 안 고쳐진 것이 같은 문단에 적혀 있으면, 읽는 사람은 둘을 구분하지 못합니다.&lt;/p&gt;
&lt;/blockquote&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모노레포&lt;/b&gt;에서 프로젝트 루트 밖 파일을 해석해야 하면 &lt;code&gt;turbopack.root&lt;/code&gt;를 지정해야 해요. Turbopack은 루트 밖을 기본적으로 안 봅니다.&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서버에서 PDF를 그려야 하면&lt;/b&gt;(썸네일 생성 같은 것) 이야기가 완전히 달라집니다. 그때 &lt;code&gt;canvas&lt;/code&gt;는 없애는 게 아니라 설치하는 쪽이에요. 빌드 로그에도 &lt;code&gt;Please use the legacy build in Node.js environments&lt;/code&gt; 경고가 같이 뜹니다.&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;react-pdf &lt;b&gt;v9.2.0 이전&lt;/b&gt;에 묶여 있다면 옛날 처방이 여전히 맞습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저도 아직 못 푼 게 있어요. 텍스트 레이어를 켜면 한글 PDF에서 선택 영역이 글자와 어긋나는 경우가 있는데, 원인을 아직 못 찾았습니다.&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;a href=&quot;https://github.com/danbom/nextjs-pdfjs-minimal&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub에 올려뒀어요&lt;/a&gt;. &lt;code&gt;next.config.ts&lt;/code&gt;가 비어 있는 게 핵심이고, 두 경로를 나란히 뒀습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/&lt;/code&gt; &amp;mdash; 감싸지 않은 쪽. &lt;b&gt;실패하도록 두었습니다&lt;/b&gt;&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/ssr-skipped&lt;/code&gt; &amp;mdash; &lt;code&gt;ssr:false&lt;/code&gt;로 감싼 쪽&lt;/li&gt;
&lt;/ul&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 data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/&lt;/code&gt;를 열면 콘솔에 500과 &lt;code&gt;DOMMatrix is not defined&lt;/code&gt;가 뜨나요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/ssr-skipped&lt;/code&gt;는 깨끗한가요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;npm run build&lt;/code&gt;가 &lt;code&gt;/&lt;/code&gt; 에서 멈추나요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;워커 경로가 &lt;code&gt;/_next/static/media/...&lt;/code&gt;로 찍히나요? &lt;code&gt;public/&lt;/code&gt;을 안 썼는데도요&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;콘솔에 &lt;code&gt;Can't resolve 'canvas'&lt;/code&gt;가 정말 없나요?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰고 계신 조합에서 다르게 나온다면 버전과 함께 알려주시면 저도 배우고 싶어요. 확인한 조합은 &lt;b&gt;Next.js 16.3.6 &amp;middot; react-pdf 10.x &amp;middot; pdfjs-dist 5.4.296 &amp;middot; Turbopack&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 &lt;b&gt;PDF 위에 그린 도형의 좌표가 확대&amp;middot;축소하면 왜 어긋나는지&lt;/b&gt;를 다뤄보려고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&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 data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/danbom/nextjs-pdfjs-minimal&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;danbom/nextjs-pdfjs-minimal&lt;/a&gt; &amp;mdash; 이 글의 최소 재현 저장소&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;wojtekmaj/react-pdf &amp;mdash; v9.2.0 릴리스 노트, README의 &lt;code&gt;workerSrc&lt;/code&gt; 설정 안내&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;Next.js 문서 &amp;mdash; &lt;code&gt;turbopack&lt;/code&gt; 설정 (&lt;code&gt;root&lt;/code&gt;, &lt;code&gt;resolveAlias&lt;/code&gt;, &lt;code&gt;rules&lt;/code&gt;)&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;mozilla/pdf.js &amp;mdash; &lt;code&gt;src/display/canvas.js&lt;/code&gt;의 &lt;code&gt;SCALE_MATRIX&lt;/code&gt;&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;vercel/next.js #64165 &amp;middot; #64657, wojtekmaj/react-pdf #1856 (모두 종료된 이슈입니다. 검색 상단에 남아 있으니 날짜를 보세요)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>브라우저에서 문서 다루기</category>
      <author>Whiimsy</author>
      <guid isPermaLink="true">https://danbom425.tistory.com/116</guid>
      <comments>https://danbom425.tistory.com/116#entry116comment</comments>
      <pubDate>Wed, 23 Sep 2026 10:29:23 +0900</pubDate>
    </item>
    <item>
      <title>우리는 우리 속도를 못 잰다</title>
      <link>https://danbom425.tistory.com/115</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnUL7A/dJMb99OCjt3/zWiz3xpW2VGjR5qjgBner0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnUL7A/dJMb99OCjt3/zWiz3xpW2VGjR5qjgBner0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnUL7A/dJMb99OCjt3/zWiz3xpW2VGjR5qjgBner0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnUL7A%2FdJMb99OCjt3%2FzWiz3xpW2VGjR5qjgBner0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;에이전트와 일하는 프런트엔드 #5 &amp;mdash; 19% 느려졌다는 그 연구가 1년 뒤 어떻게 됐는지, 그리고 왜 연구팀 본인이 결과를 물렸는지 정리해 볼게요.&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;AI 쓰면 개발자가 19% 느려진대.&quot; 이 문장을 올해 몇 번 보셨을 거예요. 출처는 METR이 2025년 7월에 낸 무작위 대조 실험인데요, 저도 한동안 이 숫자를 그대로 인용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 연구는 그 뒤로 두 번 더 움직였어요. 움직인 내용이 원래 결과보다 중요한데요, 이 글에서는 그 19%가 1년 뒤 어떻게 됐는지, 그리고 왜 연구팀이 자기 측정을 물렸는지 정리해 볼게요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;널리 인용되는 19%가 어디서 왔는지부터&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건을 먼저 보고 가는 게 좋겠어요. 나중에 이야기가 뒤집히는 지점이 전부 이 조건 안에 있거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;METR은 경험 많은 오픈소스 기여자 16명에게 자기가 평소 유지보수하는 저장소의 이슈 246개를 맡겼어요. 별 22,000개 이상, 코드 100만 줄 이상 되는 저장소들이었고, 과제 하나는 평균 두 시간짜리였습니다. 과제마다 AI 사용 허용과 금지를 무작위로 배정했어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구는 당시 최신이던 Cursor Pro에 Claude 3.5와 3.7 Sonnet이었고, 참가자들은 LLM을 이미 수십에서 수백 시간 써본 사람들이었어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 &lt;b&gt;AI를 쓴 과제가 19% 더 오래 걸렸다&lt;/b&gt;는 것이었습니다. 신뢰구간은 +2%에서 +39%였어요.&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;참가자들은 실험 &lt;b&gt;전에&lt;/b&gt; AI가 자기를 24% 빠르게 해줄 거라고 예상했어요. 그럴 수 있죠. 그런데 실험이 &lt;b&gt;끝난 뒤에도&lt;/b&gt;, 그러니까 자기가 실제로 그 과제들을 다 해본 다음에도, AI가 자기를 20% 빠르게 했다고 답했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 19% 느려졌는데요. &lt;b&gt;인식과 측정 사이에 40퍼센트포인트가 벌어진 거예요.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기가 방금 한 일인데도, 그게 얼마나 걸렸는지는 모릅니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈에서 계속 나오던 장면이에요. 2편에서는 AI가 쓴 코드의 40%에 취약점이 있었는데 정작 본인들은 자기 코드가 더 안전하다고 믿었고, 4편에서는 검토 도구가 판정을 내려주자 사람이 판정을 검토하는 대신 확인만 하게 됐어요. 속도도 같은 자리에 있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1년 뒤 같은 팀이 다시 쟀어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;METR은 2025년 후반에 규모를 키워 다시 측정했어요. 개발자 57명, 저장소 143개, 과제 800개 이상. 첫 연구의 3배가 넘는 규모입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자를 비교해 볼게요.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시기&lt;/b&gt;&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;표본&lt;/b&gt;&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;속도 변화&lt;/b&gt;&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;신뢰구간&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;2025 초 (원 연구)&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;16명 &amp;middot; 246과제&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;19% 느려짐&lt;/b&gt;&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;+2% ~ +39%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;2025 후반 &amp;middot; 기존 참가자&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;10명&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;18% 느려짐&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&amp;minus;38% ~ +9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;2025 후반 &amp;middot; 신규 참가자&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;47명&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;4% 느려짐&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;minus;15% ~ +9%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;느려지는 폭이 19%에서 4%로 줄었어요. 그런데 눈여겨볼 건 줄어든 숫자가 아니라 &lt;b&gt;오른쪽 칸&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 신뢰구간이 모두 0을 지나가요. 이건 &quot;효과가 없다&quot;는 뜻이 아니라 &lt;b&gt;&quot;이 연구로는 빨라졌는지 느려졌는지 말할 수 없다&quot;&lt;/b&gt;는 뜻이에요. 자주 헷갈리는 지점인데요, 유의하지 않다는 건 결론이 아니라 결론의 부재입니다.&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;2026년 2월, METR은 실험 설계를 바꾸겠다고 공개 발표했어요. 새 측정치가 신뢰할 만하지 않다고 스스로 판단했거든요. 이유가 세 가지였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫째, 참가를 거절하는 개발자가 늘었어요.&lt;/b&gt; AI 없이 일하는 조건 자체를 받아들이고 싶지 않다는 이유였습니다. 1년 전에는 그냥 실험 조건이던 것이 이제는 손해로 느껴지게 된 거예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둘째, 참가한 사람들도 과제를 골랐어요.&lt;/b&gt; 참가자의 30~50%가 &lt;b&gt;AI가 가장 도움될 것 같은 과제를 아예 제출하지 않았습니다.&lt;/b&gt; AI 금지 조건에 걸릴까 봐요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;셋째, 시급이 시간당 150달러에서 50달러로 내려갔어요.&lt;/b&gt; 누가 참가할지가 또 한 번 걸러졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지를 합치면 결론은 하나예요.&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI를 가장 잘 쓰는 사람과, AI가 가장 잘 먹히는 일이, 표본에서 체계적으로 빠졌습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;METR은 실제로는 그동안 생산성 향상이 생겼을 가능성이 높다고 보면서도, 자기들 데이터로는 그걸 잴 수 없다고 적었어요. 2026년 5월에 낸 설문에서는 기술직 349명이 AI 덕분에 일의 가치가 1.4~2배, 속도는 중앙값 3배가 됐다고 답했는데요, &lt;b&gt;같은 문서에서 설문 추정치는 현장 실험보다 늘 크게 나온다고 자기들이 먼저 적어놨습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 지금 상태는 이래요. 측정치는 방향을 말하지 못하고, 자기 보고는 믿을 수 없다고 측정한 쪽이 말하고 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;선택 편향은 임상에서 가장 오래된 적이에요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 일하는 쪽은 임상시험 시스템인데요, METR이 겪은 일에는 이미 이름이 다 붙어 있습니다. 100년 가까이 같은 문제와 싸워온 분야라서요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참가를 거절하는 사람이 특정 성향으로 쏠린다&lt;/b&gt; &amp;rarr; 선택 편향(selection bias). 그래서 배정을 무작위로 하고, 누가 왜 빠졌는지를 따로 보고합니다.&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참가자가 조건에 맞춰 행동을 바꾼다&lt;/b&gt; &amp;rarr; 비순응(non-adherence). 그래서 배정된 대로 분석해요(ITT). 중간에 이탈한 사람을 빼고 계산하면 남은 사람들만으로 좋은 숫자가 나오니까요.&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자기 보고가 실제와 다르다&lt;/b&gt; &amp;rarr; 그래서 가능하면 눈가림(blinding)을 하고, 1차 평가변수를 주관적 응답으로 두지 않습니다.&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과를 보고 나서 해석을 고른다&lt;/b&gt; &amp;rarr; 그래서 무엇을 성공으로 볼지 &lt;b&gt;시작 전에&lt;/b&gt; 프로토콜에 박고 사전 등록합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;METR이 지금 하고 있는 일, 그러니까 결과를 발표하는 대신 설계를 다시 짜는 건 이 분야에서는 정석이에요. 오히려 &lt;b&gt;신뢰구간이 0을 포함하는 결과를 &quot;AI는 효과 없음&quot;으로 발표하지 않은 게 이 연구팀의 가장 잘한 일&lt;/b&gt;이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 한 가지 더. 원 논문에도 이미 이렇게 적혀 있었어요. 이 결과는 AI가 대다수 개발자를 빠르게 하지 않는다는 증거가 아니라고요. 19%를 단정으로 옮긴 건 연구가 아니라 인용이었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 팀의 속도를 잴 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;AI 덕에 빨라졌나&quot;는 지금 답이 없는 질문이에요. 답이 없는 질문을 붙들고 있는 것보다, 답이 있는 질문으로 바꾸는 게 낫습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자기 보고를 1차 지표로 쓰지 않기.&lt;/b&gt; 회고에서 &quot;확실히 빨라진 것 같아요&quot;가 나오면 그건 데이터가 아니라 가설이에요. 40퍼센트포인트가 벌어지는 자리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;무엇이 빨라졌는지 쪼개기.&lt;/b&gt; 첫 커밋까지, 리뷰 통과까지, 배포까지는 서로 다르게 움직여요. AI는 첫 커밋을 크게 앞당기고 리뷰를 길게 만들 수 있는데, 셋을 하나로 뭉쳐 놓으면 그게 안 보입니다.&lt;/p&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;재기 전에 성공 기준을 적어두기.&lt;/b&gt; 숫자를 보고 나서 기준을 정하면 어떤 결과든 성공이 돼요. 이건 도구가 필요 없고 문서 한 줄이면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;작게, 자주, 같은 방식으로.&lt;/b&gt; 한 번 크게 재는 것보다 같은 방법으로 계속 재는 게 낫습니다. METR도 결국 규모를 키우다가 편향을 얻었어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이 다섯 개가 다 지켜지는 팀은 거의 없을 겁니다. 저도 못 하고 있어요. 특히 비교군은 제품 일정이 돌아가는 와중에 만들기가 정말 어렵고, 억지로 만들면 그 자체가 비용이 되죠. 다만 &lt;b&gt;네 번째, 재기 전에 성공 기준을 적어두는 것만큼은&lt;/b&gt; 도구도 예산도 필요 없어요. 하나만 고른다면 그것부터 하시길 권합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;적용해보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀에서 &quot;AI 덕에 빨라졌나&quot;를 이야기할 때 이 질문들을 먼저 던져보면 좋겠어요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;지금 근거로 삼는 숫자가 &lt;b&gt;자기 보고&lt;/b&gt;인가요, 시스템이 자동으로 남긴 기록인가요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&quot;빨라졌다&quot;에서 &lt;b&gt;무엇이&lt;/b&gt; 빨라졌나요 &amp;mdash; 첫 커밋까지? 리뷰 통과까지? 배포까지?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;비교 대상이 있나요? 없다면 그 숫자는 변화량이 아니라 현재값이에요.&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;AI를 가장 많이 쓰는 사람과 가장 적게 쓰는 사람이 &lt;b&gt;같은 종류의 일&lt;/b&gt;을 하고 있나요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;이 측정을 &lt;b&gt;시작하기 전에&lt;/b&gt; 무엇을 성공으로 볼지 적어뒀나요?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 하나 더요. 이 글의 숫자들도 2026년 9월 기준이고, METR은 지금 설계를 다시 짜는 중이에요. 그러니 이 글도 언젠가 정정될 겁니다. &lt;b&gt;그게 정상이라는 게 이 글의 요지이기도 하고요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 팀에서 AI 도입 효과를 실제로 재보신 분이 있다면, 어떤 지표를 쓰셨는지 듣고 싶어요. &quot;나라면 이렇게 쟀을 텐데&quot; 싶은 지점이 있었다면 그 얘기도요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 &lt;b&gt;판정과 측정이 남긴 기록이 나중에 증거가 될 수 있는지&lt;/b&gt;를 다뤄보려고 해요. 4편에서 사람이 판정한다고 했고 이번 편에서 그 판정을 어떻게 재느냐를 봤으니, 남는 건 그게 기록으로 어떻게 남느냐거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&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 data-ke-size=&quot;size16&quot;&gt;METR, &lt;i&gt;Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity&lt;/i&gt; (2025-07-10) &amp;mdash; 원 무작위 대조 실험&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;arXiv:2507.09089 &amp;mdash; 같은 연구의 논문본&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;METR, &lt;i&gt;We are Changing our Developer Productivity Experiment Design&lt;/i&gt; (2026-02-24) &amp;mdash; 재측정 결과와 선택 편향, 설계 변경&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;METR, &lt;i&gt;Measuring the Self-Reported Impact of Early-2026 AI on Technical Worker Productivity&lt;/i&gt; (2026-05-11) &amp;mdash; 기술직 349명 설문&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>에이전트와 일하는 프런트엔드</category>
      <author>Whiimsy</author>
      <guid isPermaLink="true">https://danbom425.tistory.com/115</guid>
      <comments>https://danbom425.tistory.com/115#entry115comment</comments>
      <pubDate>Wed, 23 Sep 2026 09:04:28 +0900</pubDate>
    </item>
    <item>
      <title>검토를 돕는 도구는 판정하지 않는다</title>
      <link>https://danbom425.tistory.com/114</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2FP89/dJMcadDoyuj/Bw2nVtZAFY36z10qswPNa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2FP89/dJMcadDoyuj/Bw2nVtZAFY36z10qswPNa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2FP89/dJMcadDoyuj/Bw2nVtZAFY36z10qswPNa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2FP89%2FdJMcadDoyuj%2FBw2nVtZAFY36z10qswPNa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;에이전트와 일하는 프런트엔드 #4 &amp;mdash; 검토를 돕는 도구가 판정까지 하면 왜 사람이 같이 틀리는지, 측정된 숫자 두 개로 이야기해 보려고 해요.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편은 이렇게 끝냈어요. &lt;b&gt;에이전트에게 구현은 맡길 수 있어도 판정 기준은 맡길 수 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 글은 에이전트를 &lt;i&gt;쓰는&lt;/i&gt; 쪽에서 쓴 거였는데요. 요즘 제가 하는 일은 반대쪽이에요. 사람이 문서를 검토하는 걸 도와주는 시스템을 만들고 있거든요. 그러니까 질문이 이렇게 바뀝니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;판정을 사람이 해야 한다면, 도구는 도대체 뭘 해야 하나?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답이 &quot;도구가 1차로 판정하고 사람이 확인한다&quot;인 것 같지만, 이 답에는 측정된 반박이 두 개 있어요. 이 글에서는 그 두 결과를 먼저 보고, 거기서 나오는 프런트엔드 설계 원칙 다섯 개를 정리해 볼게요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AI 교육 20시간으로도 자동화 편향은 안 막혀요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 NEJM AI에 실린 무작위 시험이에요. 의사 44명, 전원 &lt;b&gt;AI 리터러시 교육을 20시간 이수&lt;/b&gt;한 사람들이었어요. 임상 사례 여섯 개를 진단하게 하면서 한 그룹에는 정상적인 LLM 조언을, 다른 그룹에는 &lt;b&gt;절반의 사례에서 일부러 틀린 조언&lt;/b&gt;을 줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과부터 표로 볼게요.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정상 조언&lt;/b&gt;&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;틀린 조언&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;진단 추론 점수&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;84.9%&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;73.3%&lt;/b&gt; (조정 &amp;minus;14.0p)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;1순위 진단 정확도&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;90.5%&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;76.1%&lt;/b&gt; (&amp;minus;18.3%p)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;틀린 조언을 받은 쪽이 &lt;b&gt;1순위 진단에서 열 명 중 한 명 넘게 더 틀렸어요.&lt;/b&gt; 20시간 교육을 받고도요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저자들의 결론이 뼈아픕니다. &lt;b&gt;AI 리터러시 교육만으로는 자동화 편향을 막지 못한다.&lt;/b&gt; (Qazi et al., NEJM AI 2026)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 왜 도구 설계자에게 중요하냐면, 우리가 기본값으로 삼는 안전장치가 보통 이거라서예요. &quot;사람이 최종 확인합니다.&quot; &quot;사용자 교육을 하겠습니다.&quot; 20시간 교육받은 의사가 저 정도면, 반나절 온보딩 받은 사용자에게 기대할 수 있는 건 없다고 봐야 하죠.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다고 경보를 촘촘히 띄우면 90%가 무시돼요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 경고를 촘촘히 띄우면 되지 않나 싶은데요. 이것도 이미 해봤어요. 처방 시스템의 약물 상호작용(DDI, Drug-Drug Interaction) 경보를 모은 메타분석에서, 처방 57만 건&amp;middot;연구 11편 기준 &lt;b&gt;경보 무시율이 90%&lt;/b&gt;였어요. (95% CI 85.6&amp;ndash;95.0, Felisberto et al., 2024)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임계값을 조정해도 잘 안 내려갑니다. 사람이 게을러서가 아니라, 하루에 수십 번 울리는 신호는 정보가 아니라 소음이 되기 때문이에요.&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 data-ke-size=&quot;size16&quot;&gt;도구가 조용히 판정하면 &amp;rarr; &lt;b&gt;자동화 편향&lt;/b&gt;. 틀렸을 때 사람이 같이 틀려요.&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;도구가 시끄럽게 판정하면 &amp;rarr; &lt;b&gt;경보 피로&lt;/b&gt;. 맞았을 때도 사람이 안 봐요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간값을 잘 잡으면 되는 문제처럼 보이는데, 아니에요. &lt;b&gt;두 실패 모두 &quot;도구가 판정한다&quot;는 전제에서 나옵니다.&lt;/b&gt; 전제를 안 건드리면 둘 사이를 오갈 뿐이죠.&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;사람이 검토를 못 하는 이유는 판단력이 부족해서가 아니에요. &lt;b&gt;판단에 필요한 걸 모으는 비용이 너무 커서&lt;/b&gt;죠. 프로토콜 검토를 예로 들면, 판정 자체는 5초면 끝나요. 그 5초에 도달하기까지가 오래 걸립니다 &amp;mdash; 이 문장이 어느 버전에서 바뀌었는지, 원 규정 문구가 뭔지, 다른 섹션과 충돌하는지, 지난번엔 어떻게 처리했는지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구가 이걸 없애주면 사람의 판정은 빨라지면서 &lt;i&gt;사람의 것으로 남아요.&lt;/i&gt; 도구가 대신 판정하면 빨라지지만 위의 두 숫자가 따라오고요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구는 &quot;이건 틀렸습니다&quot;라고 말하지 않아요. &lt;b&gt;&quot;여기가 기준 문서의 이 부분과 다릅니다&quot;&lt;/b&gt;라고 말하고, 원문을 옆에 띄웁니다. 다른 게 틀린 건지는 사람이 정해요.&lt;/p&gt;
&lt;/blockquote&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;br /&gt;검토 지원 도구를 만들면 십중팔구 &quot;발견 사항 목록&quot;부터 만들게 되는데요. 그게 만들기 쉬우니까요. 그런데 목록은 판정의 결과물이에요. 실제로 사람이 오래 머무는 화면은 &lt;b&gt;좌우 대조&lt;/b&gt;입니다. 여기 붙은 문장과 저기 있는 원문. 목록은 대조 화면으로 가는 색인일 뿐이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 신뢰도 점수를 숫자로 주지 않아요.&lt;/b&gt;&lt;br /&gt;&quot;이 지적의 확신도 87%&quot; 같은 표시는 도움이 될 것 같지만, 자동화 편향을 &lt;i&gt;강화&lt;/i&gt;합니다. 숫자가 판정을 대신해버리거든요. 87%를 보고 사람이 할 수 있는 건 믿거나 안 믿거나 둘 중 하나고, 대개 믿죠. 같은 자리에 &lt;b&gt;근거 출처&lt;/b&gt;를 넣으면 사람이 할 일이 생겨요 &amp;mdash; 열어보는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 미리 채워주지 않아요.&lt;/b&gt;&lt;br /&gt;자동화 편향의 UI판이 기본값이에요. 폼에 AI가 값을 채워두면 그 값은 검토되지 않습니다. 빈칸으로 두고 옆에 후보를 제시하면, 채우는 행위가 사람의 것이 돼요. 클릭 한 번 차이인데 책임 주체가 바뀝니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. AI가 만든 것과 사람이 만든 것을 끝까지 구분해서 저장해요.&lt;/b&gt;&lt;br /&gt;화면에서만 구분하는 걸로는 부족해요. &lt;b&gt;데이터에 남아야&lt;/b&gt; 합니다. 이 값의 출처가 제안인지 입력인지, 사람이 그대로 받았는지 고쳤는지. 2편에서 쓴 감사 추적이 여기서 실물이 돼요. 나중에 &quot;이 판정 누가 했나&quot;를 물었을 때 대답할 수 있는 유일한 방법이거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 경보를 줄이는 법은 임계값 조정이 아니에요.&lt;/b&gt;&lt;br /&gt;90%가 무시당하는 이유는 경보가 많아서고, 경보가 많은 이유는 도구가 판정을 많이 해서예요. &lt;b&gt;판정을 안 하면 무시당할 경보도 없죠.&lt;/b&gt; 차이를 보여주는 것과 경고하는 것은 다릅니다. 전자는 쌓여도 참고 자료지만 후자는 쌓이면 소음이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이 다섯 개가 모든 경우의 정답은 아닙니다. 사용자가 하루에 수백 건을 처리해야 하는 화면이라면 &quot;전부 사람이 판정한다&quot;는 원칙이 그대로 처리량 문제가 되거든요. 저도 아직 이 지점은 못 풀었어요. 지금은 &lt;b&gt;판정이 아니라 정렬&lt;/b&gt;로 버티고 있습니다. 무엇이 틀렸는지는 말하지 않고, 무엇부터 볼지만 순서를 매기는 식으로요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;규제 쪽에서 봐도 같은 결론이 나와요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2편에서 정리한 방향 &amp;mdash; &lt;i&gt;위험에 비례해서, 사람이 책임지는 구조로&lt;/i&gt; &amp;mdash; 을 도구 설계 언어로 옮기면 그냥 이거예요. &lt;b&gt;도구는 중요 판정을 하지 않는다.&lt;/b&gt; 판정을 하지 않으면 그 도구는 애초에 높은 위험 등급을 받지 않고, 검증 부담도 그만큼 줄어듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재밌는 건 이게 규제 회피가 아니라는 점이에요. 판정하지 않는 도구가 실제로 더 낫습니다. 위의 두 결과가 그 얘기를 하고 있어요.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;이 화면에서 사용자가 가장 오래 머무는 곳이 &lt;b&gt;목록&lt;/b&gt;인가요, &lt;b&gt;대조 화면&lt;/b&gt;인가요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;우리가 띄우는 경고 중에, 사용자가 실제로 열어보는 비율을 세어본 적이 있나요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;AI가 채워 넣은 값과 사람이 입력한 값이 &lt;b&gt;데이터에서&lt;/b&gt; 구분되나요? 화면에서만 구분되는 건 아닌가요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&quot;사람이 최종 확인합니다&quot;를 안전장치로 쓰고 있다면, 그 사람이 틀린 조언을 받았을 때 걸러낸다는 근거가 있나요?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;네 편을 관통하는 한 문장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편은 에이전트가 읽을 저장소를 만드는 얘기였고, 2편은 그렇게 만든 코드의 책임이 누구에게 있는지, 3편은 맞고 틀림의 기준이 어디서 오는지였어요. 4편은 그 기준을 지키는 도구를 어떻게 만드는지였고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네 편이 결국 같은 말을 네 번 했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기계는 자료를 옮기고, 사람은 판정한다.&lt;/b&gt; 이 경계를 코드와 화면과 기록에 명시적으로 새겨넣은 만큼만, 에이전트를 안전하게 쓸 수 있어요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규제 산업이 20년간 문서로 해온 게 이 경계를 적어두는 일이었어요. 지금은 모든 산업이 같은 걸 코드로 적어야 하는 시기가 됐고요. 양식을 이미 갖고 있는 쪽이 유리하다고, 저는 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 읽으면서 &quot;나라면 이렇게 했을 텐데&quot; 싶은 지점이 있었다면, 그 얘기를 듣고 싶어요. 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 &amp;mdash; Qazi IA, Ali A, Khawaja AU, et al., &lt;i&gt;Automation Bias in Large Language Model&amp;ndash;Assisted Diagnostic Reasoning among Physicians Trained in AI Literacy: A Randomized Clinical Trial&lt;/i&gt;, NEJM AI 2026 (NCT06963957) &amp;middot; Felisberto M, et al., &lt;i&gt;Override rate of drug-drug interaction alerts in clinical decision support systems: a brief systematic review and meta-analysis&lt;/i&gt;, Health Informatics Journal, 2024&lt;/p&gt;</description>
      <category>에이전트와 일하는 프런트엔드</category>
      <author>Whiimsy</author>
      <guid isPermaLink="true">https://danbom425.tistory.com/114</guid>
      <comments>https://danbom425.tistory.com/114#entry114comment</comments>
      <pubDate>Tue, 22 Sep 2026 14:08:06 +0900</pubDate>
    </item>
    <item>
      <title>에이전트가 쓴 테스트는 무엇을 증명하나</title>
      <link>https://danbom425.tistory.com/113</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKp4BU/dJMcabseH72/ztiC1ZkjsQIT3x162QbqzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKp4BU/dJMcabseH72/ztiC1ZkjsQIT3x162QbqzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKp4BU/dJMcabseH72/ztiC1ZkjsQIT3x162QbqzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKp4BU%2FdJMcabseH72%2FztiC1ZkjsQIT3x162QbqzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;에이전트와 일하는 프런트엔드 #3 &amp;mdash; 에이전트에게 테스트를 맡기면 왜 동어반복이 나오는지, 측정된 결과와 규제 용어로 정리해 볼게요.&lt;/i&gt;&lt;/p&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안 돼요. 그리고 안 되는 이유가 취향 문제가 아니라 &lt;b&gt;측정되어 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;버그 있는 코드를 주면, 테스트가 버그를 정답으로 박제해요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토론토대 연구진이 올해 7월에 낸 결과예요. Defects4J v3.0의 실제 결함 233건, 함수 318개를 놓고, 모델 11개(Gemini &amp;middot; Claude &amp;middot; Grok &amp;middot; GPT &amp;middot; DeepSeek &amp;middot; Qwen 계열)에게 단위 테스트를 생성시켰습니다. 조건은 딱 하나만 달랐어요. &lt;b&gt;버그가 있는 코드를 주느냐, 고쳐진 코드를 주느냐.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과를 표로 먼저 볼게요.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;고쳐진 코드를 줬을 때&lt;/b&gt;&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;버그 있는 코드를 줬을 때&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;버그를 잡아내는 테스트&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;8.51%&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.98%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;버그를 정답으로 박제한 테스트&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;0.46%&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3.84%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버그를 잡는 능력이 3분의 1로 줄고, 버그를 &lt;i&gt;이게 맞는 동작입니다&lt;/i&gt;라고 단언하는 테스트가 8배로 늘었어요. 저자들은 이걸 &lt;b&gt;misguidance effect&lt;/b&gt;라고 불렀습니다. (Zhao, Zhou &amp;amp; Cohen, arXiv:2607.22883)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 건 실패율이 아니에요. &lt;b&gt;실패의 방향&lt;/b&gt;이죠. 에이전트가 만든 테스트는 통과합니다. 초록불이 켜지고, CI가 돌고, 커버리지가 올라가요. 그런데 그게 증명하는 건 코드가 맞다는 사실이 아니라 &lt;b&gt;코드가 지금 하고 있는 일&lt;/b&gt;이에요.&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;테스트에는 두 부분이 있어요. 무엇을 실행할지(입력)와 &lt;b&gt;결과가 맞는지 판정하는 기준&lt;/b&gt;(오라클). 입력은 코드를 보면 만들 수 있습니다. 판정 기준은 코드에서 나올 수 없어요. 코드에서 뽑아낸 기준은 정의상 코드와 항상 일치하니까요. 소프트웨어 공학에서 &lt;b&gt;오라클 문제(oracle problem)&lt;/b&gt;라고 부르는 그겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트에게 코드를 주고 테스트를 시키면, 에이전트가 참조할 수 있는 유일한 기준은 그 코드예요. 그래서 &lt;b&gt;동어반복&lt;/b&gt;이 나옵니다. 실력의 문제가 아니라 입력에 정답이 안 들어 있는 문제죠.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트에게 구현을 맡길 수는 있다. 판정 기준을 맡길 수는 없다. 우리가 안 주면 에이전트한테도 없다.&lt;/p&gt;
&lt;/blockquote&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Verification(검증)&lt;/b&gt; &amp;mdash; 만들어진 게 &lt;i&gt;설계 입력&lt;/i&gt;대로 되었는지 확인&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Validation(밸리데이션)&lt;/b&gt; &amp;mdash; 만들어진 게 &lt;i&gt;의도된 사용(intended use)&lt;/i&gt;을 충족하는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISO 13485의 7.3.6과 7.3.7이 이 둘을 따로 둡니다. 그리고 미국 FDA의 의료기기 품질시스템 규정은 올해 &lt;b&gt;2월 2일부로 QMSR로 바뀌면서 ISO 13485:2016을 그대로 인용&lt;/b&gt;해 들여왔어요. 즉 이 구분은 지금 미국&amp;middot;유럽 양쪽에서 같은 문장으로 살아 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GAMP의 V 모델도 같은 얘기를 그림으로 그린 거예요. 왼쪽 맨 위 사용자 요구사항(URS)은 오른쪽 맨 위 성능 적격성 평가(PQ)와 짝을 이룹니다. &lt;b&gt;코드와 짝을 이루는 게 아니에요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 에이전트가 코드를 보고 쓴 테스트는, 규제 용어로 옮기면 verification 흉내는 낼 수 있어도 validation은 구조적으로 못 해요. 기준선이 구현 쪽에 붙어 있기 때문이죠.&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;&lt;b&gt;스냅샷 테스트가 정확히 같은 함정이에요.&lt;/b&gt;&lt;br /&gt;toMatchSnapshot()은 편한데요. 이게 하는 일은 &lt;i&gt;지금 렌더된 결과를 정답으로 저장&lt;/i&gt;하는 것이에요. 처음 찍을 때 이미 틀려 있었다면, 그 테스트는 틀린 화면을 영구히 지킵니다. 그리고 진짜 문제는 그다음이에요. 화면이 바뀌어서 스냅샷이 깨지면 사람들은 확인하는 대신 -u를 칩니다. 에이전트한테 시키면 더 빠르게 치고요. 오라클이 매번 구현 쪽으로 다시 끌려가요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;우리 도메인에서 '정상값'이 뭔지는 코드에 안 적혀 있어요.&lt;/b&gt;&lt;br /&gt;임상 데이터에서 날짜는 자주 불완전합니다. 피험자가 &quot;재작년 여름쯤&quot;이라고 하면 그게 그대로 데이터예요. SDTM(Study Data Tabulation Model)은 이걸 버리지 않고 정밀도를 보존해서 적습니다. 뒤가 비면 잘라내고(2026-09, 2026), 가운데가 비면 자리를 하이픈으로 남겨요(2006----T03:34, SDTMIG 4.1.4.2).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 프런트엔드에 주는 결론은 단순해요. &lt;b&gt;new Date(value)로 넘기는 순간 틀립니다.&lt;/b&gt; 날짜 입력 컴포넌트가 연&amp;middot;월&amp;middot;일을 전부 요구하면 그건 버그예요. 정렬도, 기간 계산도, 표시 형식도 전부 부분 값이 들어올 걸 전제해야 합니다.&lt;/p&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;결측도 같아요. 대부분의 도메인에서 null은 예외지만 임상에서는 &lt;i&gt;정상&lt;/i&gt;이에요. &quot;측정 안 함&quot;과 &quot;측정했는데 값이 없음&quot;과 &quot;해당 없음&quot;이 다른 값이고, 이 셋을 화면에서 같게 그리면 그건 데이터를 잃은 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그래서 순서가 뒤집힙니다.&lt;/b&gt;&lt;br /&gt;테스트를 나중에 쓰면 그 테스트의 기준은 이미 코드예요. 에이전트를 쓰든 안 쓰든 그렇지만, 에이전트를 쓰면 그 순서가 훨씬 빨리 굳습니다. 판정 기준을 먼저 고정해두면 그때부터 에이전트의 속도는 순수한 이득이 되고요. &lt;b&gt;기준이 없는 상태에서의 속도는 나중에 갚을 빚이라는 게 지난 글 결론이었는데, 여기가 그 빚이 정확히 생기는 지점이에요.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;제가 인용했던 수치 하나를 정정합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서 사람의 검토 능력이 떨어진다는 얘기를 하면서 스탠퍼드 연구(Perry et al., CCS 2023)를 인용했는데요. 같은 맥락에서 자주 인용되는 게 METR의 2025년 실험이에요. 숙련 개발자가 AI를 쓸 때 오히려 &lt;b&gt;19% 느려졌는데 본인들은 20% 빨라졌다고 느꼈다&lt;/b&gt;는 그 결과요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 METR이 올해 2월에 스스로 단서를 달았습니다. 후속 실험(개발자 57명, 과제 800건 이상)에서 &lt;b&gt;선택 효과&lt;/b&gt;가 드러났다는 거예요. 참가자의 30~50%가 &quot;AI를 쓰면 훨씬 빠를 것 같은&quot; 과제를 아예 제출하지 않았습니다. 재추정치는 기존 참가자 &amp;minus;18%(신뢰구간 &amp;minus;38%~+9%), 신규 참가자 &amp;minus;4%(&amp;minus;15%~+9%)로, &lt;b&gt;0을 포함해요.&lt;/b&gt; METR 자신도 실제 이득이 측정치보다 높을 가능성이 크다고 적었고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 &quot;AI가 개발자를 느리게 만든다&quot;는 문장은 지금 근거가 약합니다. 인용하려면 이 단서까지 같이 인용해야 한다고 봐요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 &lt;b&gt;무너지지 않은 부분&lt;/b&gt;이 있어요. 속도가 얼마였든, 참가자들이 자기 속도를 잘못 추정했다는 관찰은 그대로입니다. 그리고 이 글의 논지에 필요한 건 속도가 아니라 그쪽이에요. &lt;b&gt;사람이 자기 판단의 정확도를 스스로 못 재는 상황에서, 사람의 검토를 최종 안전망으로 두는 설계는 위험합니다.&lt;/b&gt; 그게 판정 기준을 사람 머릿속이 아니라 저장소 안에 놔야 하는 이유죠.&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;넘겨도 되는 것&lt;/b&gt;&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;넘기면 안 되는 것&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;구현&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;무엇이 맞는지의 기준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;테스트 &lt;i&gt;케이스&lt;/i&gt;를 늘리기 (기준이 이미 있을 때)&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;테스트 &lt;i&gt;기준&lt;/i&gt;을 코드에서 역산하기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;리팩터링 &amp;mdash; 동작이 같아야 한다는 기준이 이미 테스트로 있을 때&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;동작 변경 &amp;mdash; 새 기준이 필요한 일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;반복 작업, 변환, 보일러플레이트&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;도메인 규칙이 들어가는 판정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;b&gt;적을 수 있는데 안 적어둔 것&lt;/b&gt;과 &lt;b&gt;원래 못 적는 것&lt;/b&gt;은 구분해야 한다고 생각해요.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;지금 우리 테스트의 판정 기준은 &lt;b&gt;코드에서&lt;/b&gt; 왔나요, &lt;b&gt;요구사항에서&lt;/b&gt; 왔나요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;스냅샷이 깨졌을 때 우리 팀은 확인하나요, -u를 치나요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;우리 도메인에서 &quot;정상값&quot;의 정의가 저장소 안 어딘가에 문장으로 적혀 있나요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;에이전트가 그 문장을 읽을 수 있는 자리에 있나요, 아니면 사람 머릿속에만 있나요?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편에서 &lt;b&gt;에이전트가 읽을 저장소&lt;/b&gt; 얘기를 했었는데요. 그때는 구조와 문서 얘기였는데, 세 편을 쓰고 나니 같은 결론에 다른 이름이 붙네요. 저장소에 적어둬야 하는 건 코드가 어떻게 생겼는지가 아니라 &lt;b&gt;무엇이 맞는지&lt;/b&gt;예요. 그게 적혀 있는 만큼만 에이전트에게 넘길 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 반대쪽 얘기를 해볼게요. 판정을 사람이 해야 한다면, 그럼 도구는 뭘 해야 하는지요. 감사합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;참고 &amp;mdash; Zhao, Zhou &amp;amp; Cohen, &lt;i&gt;Evaluating and Mitigating the Misguidance Effect of Buggy Code in LLM-Generated Unit Tests&lt;/i&gt;, arXiv:2607.22883 (2026.7) &amp;middot; METR, &lt;i&gt;We are Changing our Developer Productivity Experiment Design&lt;/i&gt; (2026.2.24) &amp;middot; Perry et al., &lt;i&gt;Do Users Write More Insecure Code with AI Assistants?&lt;/i&gt;, ACM CCS 2023 &amp;middot; FDA, Quality Management System Regulation (21 CFR 820), 2026.2.2 발효 &amp;middot; ISO 13485:2016 &amp;sect;7.3.6&amp;ndash;7.3.7 &amp;middot; CDISC SDTMIG &amp;sect;4.1.4.2&lt;/p&gt;</description>
      <category>에이전트와 일하는 프런트엔드</category>
      <author>Whiimsy</author>
      <guid isPermaLink="true">https://danbom425.tistory.com/113</guid>
      <comments>https://danbom425.tistory.com/113#entry113comment</comments>
      <pubDate>Tue, 22 Sep 2026 09:52:34 +0900</pubDate>
    </item>
    <item>
      <title>AI가 쓴 코드, 누가 책임지나</title>
      <link>https://danbom425.tistory.com/112</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZ1YHV/dJMcagUEsob/WOBKgAB8pMh280iJFo79wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZ1YHV/dJMcagUEsob/WOBKgAB8pMh280iJFo79wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZ1YHV/dJMcagUEsob/WOBKgAB8pMh280iJFo79wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZ1YHV%2FdJMcagUEsob%2FWOBKgAB8pMh280iJFo79wk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;에이전트와 일하는 프런트엔드 #2 &amp;mdash; AI가 쓴 코드의 책임을 규제는 어떻게 다루고 있는지, 적용 범위를 뭉개지 않고 정리해 볼게요.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유럽 GMP 부속서 초안에 이런 취지의 문장이 들어갔어요. &lt;b&gt;GMP에 중요한 결정은 &quot;정적이고 결정론적인(static, deterministic)&quot; 모델만 내릴 수 있다. 동적 모델, 생성형 AI, 대규모 언어 모델은 중요 용도에서 제외한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM을 아예 금지한 건 아닌데요. 일탈 보고서 요약, SOP 검색, 정비 메모 초안 같은 &lt;i&gt;중요하지 않은&lt;/i&gt; 작업에는 쓸 수 있어요. 단 조건이 붙습니다. 자격 있는 사람이 루프 안에 있어야 하고, 그 사람이 출력물을 검토하며, &lt;b&gt;문서화된 책임을 집니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규제기관이 &quot;AI 잘 쓰세요&quot; 같은 덕담이 아니라 규정 문안으로 선을 그은 건 이게 처음이에요. 그리고 이 선은 개발자에게 꽤 구체적인 질문을 던집니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;질문이 바뀌었어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 코딩 도구 얘기는 보통 &quot;얼마나 잘 짜느냐&quot;로 흐르죠. 벤치마크 점수, 생산성 몇 배, 어떤 모델이 더 낫나.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 규제 산업에서 일해보면 그 질문이 애초에 2순위라는 걸 알게 돼요. 1순위는 이겁니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드가 맞다는 걸, 누가, 어떻게 확인했고, 그 확인을 어떻게 증명하나?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임상시험 시스템은 감사를 받아요. 심사관이 와서 &quot;이 기능 검증 기록 보여주세요&quot;라고 하면 내놓을 게 있어야 합니다. 이때 &quot;Claude가 짰는데 잘 돌아갑니다&quot;는 답이 아니에요. 답이 아닌 이유는 AI를 못 믿어서가 아니라, &lt;b&gt;책임 주체가 특정되지 않아서&lt;/b&gt;죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 AI 코딩 도구가 규제 산업에 들어올 때 진짜 병목은 모델 성능이 아니라 &lt;b&gt;증명 가능성&lt;/b&gt;이 됩니다. 이건 프롬프트를 잘 쓴다고 해결되지 않아요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;40%에 취약점, 그런데 본인은 더 안전하다고 믿었어요&lt;/h2&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;하나.&lt;/b&gt; NYU 연구진이 Copilot에게 MITRE Top 25 취약점 시나리오 89개를 주고 프로그램 1,689개를 생성시켰습니다. &lt;b&gt;약 40%에 취약점이 있었어요.&lt;/b&gt; (Pearce et al., IEEE S&amp;amp;P 2022)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둘.&lt;/b&gt; 이게 더 중요한데요. 스탠퍼드 연구진이 참가자 47명을 AI 도우미 그룹과 대조군으로 나눠 코딩시켰어요. AI를 쓴 쪽이 &lt;b&gt;유의미하게 덜 안전한 코드를 썼습니다.&lt;/b&gt; 그런데 동시에 &lt;b&gt;자기 코드가 안전하다고 더 강하게 믿었어요.&lt;/b&gt; (Perry et al., ACM CCS 2023)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째가 왜 더 중요하냐면, 모든 규제 프레임워크가 최후의 안전망으로 &lt;b&gt;사람의 검토&lt;/b&gt;를 상정하고 있기 때문이에요. 그런데 이 연구가 말하는 건 AI를 끼고 일할 때 사람의 검토 능력 자체가 떨어진다는 겁니다. 안전망에 구멍이 뚫린 채로 안전망을 믿는 구조가 되는 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 연구에 힌트도 있어요. &lt;b&gt;AI를 덜 믿고 프롬프트를 더 붙들고 씨름한 참가자일수록 취약점이 적었습니다.&lt;/b&gt; 즉 문제는 도구가 아니라 도구와의 거리예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;규제는 이미 답을 정해놨어요 (다만 우리 얘기는 아직 아니에요)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 솔직하게 짚고 갈 게 있는데요. 요즘 &quot;FDA가 AI 가이드라인을 냈다&quot;는 식의 글이 많은데, &lt;b&gt;대부분 적용 범위를 뭉갭니다.&lt;/b&gt; 표로 정확히 정리해 볼게요.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문서&lt;/b&gt;&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상태&lt;/b&gt;&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;적용 범위&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;FDA CSA 가이던스&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최종&lt;/b&gt; (2025.9, docket FDA-2022-D-0795)&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;생산&amp;middot;품질시스템 소프트웨어(QSR). 임상 EDC 아님&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;EU GMP Annex 22 (AI)&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;초안&amp;middot;의견수렴 종료, 2026 확정 예상&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GMP&lt;/b&gt;(제조). GCP(임상) 아님&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;EMA 리플렉션 페이퍼&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;채택 (2024.9.9)&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;의약품 전 주기. &lt;b&gt;구속력 없음&lt;/b&gt; &amp;mdash; EMA 가이던스 중 가장 낮은 단계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;FDA AI 가이던스&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;아직 초안&lt;/b&gt; (2025.1 발행, 17개월째)&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;규제 의사결정 지원용 AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;ISPE GAMP AI 가이드&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;발행 (2025.7)&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;GxP 영역, 업계 표준(규제 아님)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 &lt;b&gt;&quot;AI가 짠 코드를 임상시험 시스템에 쓰지 마라&quot;고 직접 말하는 규정은 아직 없어요.&lt;/b&gt; Annex 22는 제조 얘기고, CSA는 품질시스템 얘기입니다. 이걸 임상에 그대로 갖다 붙이면 틀린 말이 돼요.&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 data-ke-size=&quot;size16&quot;&gt;EMA: &lt;i&gt;&quot;인간 중심 접근이 AI/ML의 모든 개발과 배포를 이끌어야 한다&quot;&lt;/i&gt;&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;FDA 초안: 위험도를 &lt;b&gt;모델 영향력 &amp;times; 결정 결과&lt;/b&gt;로 계산하고, 그에 비례해 신뢰성 입증 계획을 세워라 (7단계 프레임워크)&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;Annex 22: 중요 결정은 재현 가능한 모델만. LLM은 사람이 책임질 때만&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통점은 &quot;AI를 쓰지 마라&quot;가 아니라 &lt;b&gt;&quot;위험에 비례해서, 사람이 책임지는 구조로 써라&quot;&lt;/b&gt;예요. 임상 쪽에 같은 취지의 문서가 내려오는 건 시간 문제에 가깝다고 봅니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CSA가 진짜 바꾼 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSA에서 제일 오해받는 대목을 짚고 싶어요. CSA를 &quot;검증을 덜 해도 된다&quot;로 읽는 분이 많은데, 가이던스 본문에 이런 문장이 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비정형 테스트(unscripted testing)가 &lt;b&gt;문서화하지 않는다는 뜻은 아니다&lt;/b&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSA가 줄이라는 건 &lt;b&gt;종이&lt;/b&gt;지 &lt;b&gt;확신&lt;/b&gt;이 아니에요. 오히려 이렇게 하라고 합니다 &amp;mdash; 스크린샷 찍고 결과를 손으로 옮겨적는 대신, &lt;b&gt;시스템이 이미 만들어내는 로그와 감사 추적(audit trail)을 증거로 쓰라&lt;/b&gt;고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 개발자에게는 꽤 실용적인 지시예요. 검증 산출물을 나중에 사람이 만드는 게 아니라, &lt;b&gt;시스템이 돌아가면서 스스로 남기게 설계하라&lt;/b&gt;는 뜻이니까요. 21 CFR Part 11이 20년 넘게 요구해온 것(&amp;sect;11.10(e) 감사 추적, &amp;sect;11.10(d) 접근 통제, &amp;sect;11.10(a) 시스템 검증)이 여기서 검증 증거로 재활용됩니다.&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;&lt;b&gt;1. 에이전트에게 맡길 것과 아닌 것을 코드 구조로 갈라둡니다.&lt;/b&gt;&lt;br /&gt;Annex 22의 &quot;중요/비중요&quot; 구분은 사실 좋은 설계 원칙이에요. 값을 계산하고 판정하는 로직과, 그걸 화면에 옮기는 코드는 위험도가 다릅니다. 후자는 에이전트에게 넘기기 훨씬 편해요. 문제는 대부분의 코드베이스가 이 둘을 안 갈라놓는다는 거고, 안 갈라놨으면 &lt;b&gt;전체가 높은 위험도로 취급&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. &quot;돌아간다&quot;와 &quot;맞다&quot;를 구분하는 테스트를 먼저 써요.&lt;/b&gt;&lt;br /&gt;LLM 코드의 특징적인 실패는 컴파일 에러가 아니에요. 해피 패스는 완벽하고, 경계값에서 조용히 틀립니다. 빈 입력, null, 범위 밖 값, 로케일&amp;middot;날짜. 임상 데이터는 결측과 경계값이 &lt;i&gt;정상&lt;/i&gt;인 도메인이라 이게 치명적이죠. 에이전트에게 코드를 시키기 전에 &lt;b&gt;판정 기준을 먼저 테스트로 고정&lt;/b&gt;해두면, 에이전트가 만든 게 통과하는지로 검증이 자동으로 남아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 의존성은 사람이 봅니다.&lt;/b&gt;&lt;br /&gt;슬롭스쿼팅(slopsquatting)이라는 이름이 붙은 공격이 있어요. LLM이 없는 패키지 이름을 그럴듯하게 지어내면, 공격자가 그 이름을 npm&amp;middot;PyPI에 &lt;b&gt;선점 등록&lt;/b&gt;해두는 겁니다. 에이전트가 제안한 설치 명령을 그대로 실행하면 공격자 코드를 받게 돼요. 코드 리뷰에서 로직만 보고 package.json 변경은 대충 넘기는 습관이 여기서 깨집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 검토했다는 사실을 사람이 아니라 저장소가 증명하게 해요.&lt;/b&gt;&lt;br /&gt;&quot;제가 봤습니다&quot;는 감사에서 약합니다. 누가 언제 무엇을 승인했는지가 PR&amp;middot;커밋&amp;middot;CI 기록으로 남아 있으면 그건 감사 추적이에요. 이미 다 있는 도구고요. 안 쓰고 있을 뿐이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이 네 가지가 감사 대응을 대신해주지는 않습니다. 실제 감사에서는 이 기록들을 &lt;i&gt;어떤 절차로&lt;/i&gt; 만들었는지를 또 묻거든요. 다만 기록 자체가 없으면 그 질문에 도달하지도 못해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1편과 이어지는 지점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서 &lt;b&gt;에이전트가 읽을 저장소&lt;/b&gt;를 어떻게 만드는지 썼는데요. 이번 글은 그 반대편이에요. 에이전트가 읽기 좋게 만들어놨으면, 이제 &lt;b&gt;에이전트가 쓴 걸 어떻게 확인할 것인가&lt;/b&gt;가 남습니다.&lt;/p&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;규제 산업은 이 빚을 먼저 떠안아본 동네죠. 20년 전부터 &quot;누가 책임지나&quot;를 문서로 증명해왔으니까요. 지금 모든 산업이 AI 때문에 같은 질문을 받기 시작했는데, 임상 쪽 사람들은 이미 답안지 양식을 갖고 있는 셈이에요. 재밌는 역전입니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;우리 코드베이스에서 &lt;b&gt;판정하는 코드&lt;/b&gt;와 &lt;b&gt;옮기는 코드&lt;/b&gt;가 구분되어 있나요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&quot;사람이 리뷰했다&quot;를 증명해야 한다면, 지금 기록으로 증명이 되나요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;에이전트가 추가한 의존성을 마지막으로 직접 확인한 게 언제인가요?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 그 &quot;검증 구조&quot;를 에이전트한테 맡기면 어떻게 되는지 써볼게요. 감사합니다.&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 data-ke-size=&quot;size16&quot;&gt;FDA, &lt;i&gt;Computer Software Assurance for Production and Quality System Software&lt;/i&gt; (Final, 2025.9, FDA-2022-D-0795)&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;EU GMP Annex 22 (AI) 초안 &amp;mdash; European Commission GMP/GDP Inspectors Working Group&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;EMA, &lt;i&gt;Reflection paper on the use of AI in the medicinal product lifecycle&lt;/i&gt; (2024.9.9)&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;FDA, &lt;i&gt;Considerations for the Use of AI to Support Regulatory Decision-Making&amp;hellip;&lt;/i&gt; (Draft, 2025.1)&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;ISPE, &lt;i&gt;GAMP Guide: Artificial Intelligence&lt;/i&gt; (2025.7) / GAMP 5 2nd Ed. Appendix M12&amp;middot;D11&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;Pearce et al., &lt;i&gt;Asleep at the Keyboard? Assessing the Security of GitHub Copilot's Code Contributions&lt;/i&gt;, IEEE S&amp;amp;P 2022&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;Perry et al., &lt;i&gt;Do Users Write More Insecure Code with AI Assistants?&lt;/i&gt;, ACM CCS 2023&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>에이전트와 일하는 프런트엔드</category>
      <author>Whiimsy</author>
      <guid isPermaLink="true">https://danbom425.tistory.com/112</guid>
      <comments>https://danbom425.tistory.com/112#entry112comment</comments>
      <pubDate>Mon, 21 Sep 2026 15:40:25 +0900</pubDate>
    </item>
    <item>
      <title>에이전트가 읽을 저장소</title>
      <link>https://danbom425.tistory.com/111</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0ViLS/dJMcajcHiQT/xYfGx3q2tUkBADHjC8jTLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0ViLS/dJMcajcHiQT/xYfGx3q2tUkBADHjC8jTLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0ViLS/dJMcajcHiQT/xYfGx3q2tUkBADHjC8jTLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0ViLS%2FdJMcajcHiQT%2FxYfGx3q2tUkBADHjC8jTLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;에이전트와 일하는 프런트엔드 #1 &amp;mdash; 주석 207개를 지우고, 문서 12종을 1종으로 합치고, 규칙을 파일에 박기까지. 저장소를 에이전트가 읽을 수 있게 만든 한 주의 기록이에요.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 임상시험 시스템을 만드는 Frontend Developer 민은영입니다. 앞으로 네 편에 걸쳐 &lt;b&gt;에이전트와 일하면서 바뀐 것들&lt;/b&gt;을 정리해 보려고 하는데요. 첫 편은 코드를 짜기 전에 저장소부터 손봐야 했던 이야기예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트에게 일을 시키다 보면 이상한 순간이 옵니다. 같은 요청인데 어떤 날은 정확하게 고치고, 어떤 날은 3개월 전에 폐기한 방식으로 되돌려 놓아요. 처음에는 모델 탓을 했습니다. 몇 번 반복되고 나서야 원인이 다른 데 있다는 걸 알았어요. &lt;b&gt;저장소가 에이전트에게 거짓말을 하고 있었거든요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람은 저장소의 거짓말에 면역이 있죠. 주석에 적힌 날짜가 낡았다는 걸 알고, 플랜 문서가 세 개면 슬랙에서 최신본을 물어보고, // 임시라고 쓰인 코드가 2년째 프로덕션에 있다는 것도 알고요. 에이전트는 그 면역이 없습니다. 읽은 대로 믿어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 한 주를 코드가 아니라 &lt;b&gt;저장소를 읽히게 만드는 데&lt;/b&gt; 썼습니다. 이 글은 그때 지운 것과 남긴 것에 대한 기록이에요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 주석은 코드보다 빨리 썩어요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 손댄 건 주석이었는데요. 프로덕션 코드의 주석을 전수로 읽고 분류했더니 이렇게 나왔어요.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;종류&lt;/b&gt;&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;건수&lt;/b&gt;&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;묘비 (tombstone)&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;6&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;이미 지운 함수가 &quot;여기 있었다&quot;고 알려주는 주석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;이력 조각&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;18&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;&quot;2024-11 A방식 &amp;rarr; 2025-03 B방식으로 변경&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;동어반복&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;183&lt;/td&gt;
&lt;td data-ke-size=&quot;size16&quot;&gt;// 사용자 이름을 반환한다 바로 아래 return user.name&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합쳐서 &lt;b&gt;207건&lt;/b&gt;이에요. 압도적으로 많은 건 동어반복이었고요. 여기에 주석 안에 박혀 있던 날짜 44건(28개 파일)을 더 걷어냈습니다. 프로덕션 주석이 2,202줄에서 2,092줄로 줄었어요.&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;b&gt;다시 쌓이지 않게 하는 것&lt;/b&gt;이었어요. 그래서 규칙 하나를 문서에 박았습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결정의 시점과 경위는 커밋 로그와 이슈에서 본다. 주석에는 쓰지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이력을 주석에 적고 싶어지는 이유는 대개 &quot;나중에 왜 이렇게 했는지 기억 안 날까 봐&quot;죠. 그런데 그 정보는 이미 커밋과 이슈에 있어요. 주석에 복사해 두면 원본이 바뀔 때 사본만 낡습니다. 사본을 지우는 게 아니라 &lt;b&gt;사본을 만들지 않는 것&lt;/b&gt;이 규칙이어야 해요.&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;다음은 문서였는데요. 기능별로 흩어져 있던 플랜&amp;middot;잔여 작업 문서가 12종이었고, 그중 6묶음이 서로 내용이 겹쳤어요. 겹친 것들끼리도 미묘하게 달랐고요. 어떤 문서는 이미 끝난 항목을 미완으로 두고 있었고, 어떤 문서는 폐기한 방식을 그대로 남겨 뒀습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람이 이 상태를 견딜 수 있는 건 &quot;아 그건 옛날 거예요&quot;라고 말해 줄 동료가 있기 때문이에요. 에이전트에게는 그 동료가 없습니다. 12종을 전부 읽고 &lt;b&gt;모순되는 지시를 동시에 따르려고 해요.&lt;/b&gt; 결과물이 이상해지는 건 당연하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복을 제거하고 하나의 정본 파일로 합쳤습니다. 그 과정에서 &quot;이미 끝났는데 문서에만 남아 있던 항목&quot; 11건이 정리됐어요. 문서를 합친 게 아니라 &lt;b&gt;사실을 한 곳에 모은 것&lt;/b&gt;에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정본을 하나로 만들 때 지킨 것 하나 &amp;mdash; 합친 뒤 옛 문서를 남겨 두지 않았어요. &quot;혹시 몰라서&quot; 남긴 파일이 다음 달에 다시 정본 행세를 하거든요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 참조 번호가 어디를 가리키는지 아무도 몰라요&lt;/h2&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;어느 문서의 무엇인지 아는 사람이 저 하나&lt;/b&gt;였다는 거예요. 새로 온 사람도 못 찾고, 에이전트는 더 못 찾습니다. 번호만 보고 지레짐작해서 엉뚱한 결론을 내요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 색인 문서를 하나 만들었어요. 번호 체계마다 &quot;이건 무엇이고, 원문은 어디에 있고, 형식은 이렇다&quot;를 적은 한 장짜리 표입니다. 만드는 데 반나절 걸렸고, 그 뒤로 &quot;이 번호 뭐예요?&quot;라는 질문이 사라졌어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부수 효과가 하나 있었는데요. 색인을 만들면서 &lt;b&gt;더 이상 존재하지 않는 요구사항을 가리키는 주석 89줄&lt;/b&gt;이 드러났습니다. 색인은 그 자체로 죽은 참조를 찾는 도구였어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 규칙은 리뷰가 아니라 파일에 둡니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 세 가지를 정리하고 나서, 정리한 내용이 유지되게 만들 자리가 필요했어요. 저장소 루트의 에이전트 규칙 파일(AGENTS.md)에 다음을 명문화했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;주석 규칙 &amp;mdash; 결정 시점은 커밋 로그와 이슈에서 본다&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;폴더 규약 &amp;mdash; 훅과 스토어는 model, 순수 함수는 lib. 예외 경로도 함께 명시&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;UI 텍스트 표기 규칙 &amp;mdash; 대소문자 기준, 단복수 처리, 목록 단위 명사&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 배운 게 하나 있어요. &lt;b&gt;예외를 같이 적지 않으면 규칙은 지켜지지 않습니다.&lt;/b&gt; &quot;훅은 model에&quot;만 적으면 쇼케이스 라우트처럼 규칙 밖에 있어야 하는 디렉터리에서 에이전트가 규칙을 억지로 적용해요. 예외를 명시한 뒤로 그 실수가 없어졌습니다.&lt;/p&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;리뷰에서 두 번 이상 나온 말만&lt;/b&gt; 올리는 기준으로 버티고 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 그래서 무엇이 달라졌나&lt;/h2&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;폐기된 방식으로 되돌아가는 일이 사라졌어요.&lt;/b&gt; 원인이 이력 주석과 중복 문서였다는 게 사후에 분명해졌고요.&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컨텍스트에 넣을 파일을 고르는 시간이 줄었어요.&lt;/b&gt; 정본이 하나면 고를 게 없으니까요.&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;온보딩 질문이 줄었어요.&lt;/b&gt; 색인과 &quot;첫날 읽을 순서&quot;를 넣은 뒤 사람에게서도 같은 효과가 났습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 항목이 이 작업의 진짜 성격을 말해 줘요. 저는 에이전트를 위해 저장소를 정리했다고 생각했는데, 결과물은 &lt;b&gt;사람이 읽기에도 더 나은 저장소&lt;/b&gt;였습니다. 에이전트는 우리 문서가 실제로 얼마나 엉망인지를 아프게 드러내는 센서에 가까웠어요.&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동어반복 주석부터 지웁니다.&lt;/b&gt; 가장 많고, 지워도 아무도 다치지 않아요. 리스크 0의 워밍업이죠.&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중복 문서를 찾습니다.&lt;/b&gt; 같은 주제 파일이 두 개 이상이면 하나로 합치고 나머지는 지워요. 남기지 않습니다.&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;규칙 파일을 만듭니다.&lt;/b&gt; 처음부터 완성할 필요 없어요. 리뷰에서 두 번 이상 한 말을 한 줄씩 옮기면 됩니다.&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예외를 같이 적습니다.&lt;/b&gt; 규칙보다 예외가 규칙을 살려요.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주석 하나 지우는 건 커밋 한 줄이지만, 207개를 지우는 건 결정이에요. 이 결정은 스프린트 중간에 곁다리로 못 합니다. 저는 실기능이 일단락된 주를 통째로 여기에 썼고, 그 판단이 이 작업에서 제일 잘한 부분이었다고 생각해요.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;같은 주제를 다루는 문서가 &lt;b&gt;두 개 이상&lt;/b&gt; 있나요? 그중 어느 게 정본인지 파일만 보고 알 수 있나요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;주석에 적힌 날짜 중 올해 것이 몇 개인가요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;코드에 박힌 사내 참조 번호를 해석할 수 있는 사람이 몇 명인가요?&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;리뷰에서 세 번 이상 반복한 지적이 있다면, 그건 지금 어디에 적혀 있나요?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 이렇게 정리한 저장소에 에이전트가 코드를 쓰기 시작했을 때, &lt;b&gt;그 코드의 책임을 누가 지는지&lt;/b&gt;를 써볼게요. 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이 글은 특정 제품&amp;middot;조직의 세부를 뺀 일반화된 기록입니다. 수치는 실제 작업분이고, 사례는 제 환경에서의 관찰이에요.&lt;/i&gt;&lt;/p&gt;</description>
      <category>에이전트와 일하는 프런트엔드</category>
      <author>Whiimsy</author>
      <guid isPermaLink="true">https://danbom425.tistory.com/111</guid>
      <comments>https://danbom425.tistory.com/111#entry111comment</comments>
      <pubDate>Mon, 21 Sep 2026 11:01:47 +0900</pubDate>
    </item>
    <item>
      <title>개인정보처리방침</title>
      <link>https://danbom425.tistory.com/pages/privacy</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;본 블로그(danbom425.tistory.com, 이하 &quot;블로그&quot;)는 방문자의 개인정보를 소중히 다루며, 아래와 같이 개인정보의 처리 방침을 안내합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 수집하는 개인정보&lt;/h3&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;방문 일시, 접속 IP, 이용 기록, 브라우저 및 기기 정보&lt;/li&gt;
&lt;li&gt;댓글 또는 방명록 작성 시 이용자가 입력한 닉네임, 비밀번호, 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글&amp;middot;방명록은 티스토리(주식회사 카카오)의 서비스 기능을 통해 처리되며, 해당 정보의 보관과 관리는 티스토리의 개인정보처리방침을 따릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 수집 목적&lt;/h3&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;댓글&amp;middot;방명록을 통한 이용자와의 소통&lt;/li&gt;
&lt;li&gt;부정 이용 및 스팸 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 보유 및 이용 기간&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 수집 정보는 티스토리의 정책에 따라 보관되며, 목적이 달성되면 파기됩니다. 댓글&amp;middot;방명록은 작성자가 삭제하거나 블로그 운영자가 삭제할 때까지 보관됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 광고 및 쿠키&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 블로그는 제3자 광고 서비스를 이용할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Google을 포함한 제3자 광고 사업자는 쿠키를 사용하여 이용자의 이전 방문 기록을 바탕으로 광고를 게재할 수 있습니다.&lt;/li&gt;
&lt;li&gt;이용자는 Google 광고 설정(https://adssettings.google.com)에서 맞춤 광고를 해제할 수 있습니다.&lt;/li&gt;
&lt;li&gt;제3자 광고 사업자의 쿠키 사용에 대해서는 https://policies.google.com/technologies/ads 에서 확인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;브라우저 설정을 통해 쿠키 저장을 거부할 수 있으며, 이 경우 일부 기능 이용에 제한이 있을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 통계 도구&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방문 통계 확인을 위해 티스토리 자체 통계 및 Google Analytics 등의 분석 도구를 사용할 수 있습니다. 이 도구들은 개인을 식별할 수 없는 형태의 정보를 수집합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 제3자 제공&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그는 수집된 정보를 제3자에게 판매하거나 제공하지 않습니다. 다만 법령에 따라 요구되는 경우에는 관련 절차에 따라 제공할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 이용자의 권리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이용자는 언제든지 본인이 작성한 댓글&amp;middot;방명록의 열람, 수정, 삭제를 요청할 수 있습니다. 요청은 아래 연락처로 보내주시면 확인 후 처리하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 문의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bear0369@naver.com&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 시행일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 방침은 2026년 9월 21일부터 시행됩니다. 내용이 변경될 경우 이 페이지를 통해 공지합니다.&lt;/p&gt;</description>
      <author>Whiimsy</author>
      <guid isPermaLink="true">https://danbom425.tistory.com/pages/privacy</guid>
      <pubDate>Mon, 21 Sep 2026 10:52:55 +0900</pubDate>
    </item>
    <item>
      <title>소개</title>
      <link>https://danbom425.tistory.com/pages/about</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 프론트엔드 개발자 Whiimsy입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 블로그는 제가 일하면서 부딪힌 문제와 그때 내린 판단을 기록해 두는 곳입니다. 정리된 지식을 옮겨 적기보다, 실제로 막혔던 지점과 그걸 어떻게 풀었는지를 남기려고 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주로 쓰는 것&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프론트엔드 실무 &amp;mdash; React, Next.js, TypeScript&lt;/li&gt;
&lt;li&gt;디자인 시스템과 UI 구현&lt;/li&gt;
&lt;li&gt;AI 에이전트를 실무 코드베이스에 들여놓으면서 바뀐 것들&lt;/li&gt;
&lt;li&gt;읽은 책과 문서 정리&lt;/li&gt;
&lt;li&gt;개발자로 지내면서 남기는 일기&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;글을 쓰는 기준&lt;/h3&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;/li&gt;
&lt;li&gt;회사 업무와 관련된 내용은 조직&amp;middot;제품을 특정할 수 있는 부분을 모두 덜어내고, 기술적인 판단만 일반화해서 씁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;연락&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bear0369@naver.com&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글에 대한 의견이나 잘못된 부분 지적은 언제든 환영합니다. 댓글이나 메일로 알려주시면 확인하고 수정하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;운영&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;글 100여 편 &amp;middot; 누적 방문 27,000여 회 (2026년 9월 기준)&lt;/li&gt;
&lt;li&gt;이 블로그의 모든 글은 직접 작성했습니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>Whiimsy</author>
      <guid isPermaLink="true">https://danbom425.tistory.com/pages/about</guid>
      <pubDate>Mon, 21 Sep 2026 10:50:42 +0900</pubDate>
    </item>
    <item>
      <title>효과적인 코드 리뷰를 위해서</title>
      <link>https://danbom425.tistory.com/108</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://engineering.linecorp.com/ko/blog/effective-codereview#enhance-skill&quot;&gt;https://engineering.linecorp.com/ko/blog/effective-codereview#enhance-skill&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1723088516494&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;효과적인 코드 리뷰를 위해서&quot; data-og-description=&quot;종종 팀 내에서 코드 품질이 이슈가 됩니다. 그리고 유닛 테스트와 코드 커버리지를 향상시키는 방법에 대해 모두가 한 마디씩 던집니다. 하지만 그리 오래가진 못합니다. 모두들 다시 바빠지면&quot; data-og-host=&quot;engineering.linecorp.com&quot; data-og-source-url=&quot;https://engineering.linecorp.com/ko/blog/effective-codereview#enhance-skill&quot; data-og-url=&quot;https://engineering.linecorp.com/ko/blog/effective-codereview&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/e5bYt/hyWKFdQXeA/RL7aR9FRz8J43PBgE2PoW0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/b1QJci/hyWKzdEivk/j6j2c88266wyL770vjT2R1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bQvM8a/hyWKGw5lBC/Ls5xOo1m13VROsFpay2IM1/img.png?width=1389&amp;amp;height=1482&amp;amp;face=0_0_1389_1482&quot;&gt;&lt;a href=&quot;https://engineering.linecorp.com/ko/blog/effective-codereview#enhance-skill&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://engineering.linecorp.com/ko/blog/effective-codereview#enhance-skill&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/e5bYt/hyWKFdQXeA/RL7aR9FRz8J43PBgE2PoW0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/b1QJci/hyWKzdEivk/j6j2c88266wyL770vjT2R1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bQvM8a/hyWKGw5lBC/Ls5xOo1m13VROsFpay2IM1/img.png?width=1389&amp;amp;height=1482&amp;amp;face=0_0_1389_1482');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;효과적인 코드 리뷰를 위해서&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;종종 팀 내에서 코드 품질이 이슈가 됩니다. 그리고 유닛 테스트와 코드 커버리지를 향상시키는 방법에 대해 모두가 한 마디씩 던집니다. 하지만 그리 오래가진 못합니다. 모두들 다시 바빠지면&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;engineering.linecorp.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리뷰어를 희귀한 자원으로 다루는 것이 중요하다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 리뷰 목표&lt;/h3&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;/li&gt;
&lt;li&gt;리뷰를 위해 최대한 빨리 PR을 보내자&lt;/li&gt;
&lt;li&gt;의미 있는 PR을 만들기에 충분한 정보를 제공하자&lt;/li&gt;
&lt;li&gt;코드 분석툴을 활용하고 코드 스타일을 확인하자&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1시간에 300줄 정도 리뷰하는 것이 적당&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PR 내용엔&lt;/h3&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;/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;가 명확하게 담겨 있어야 한다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PR 템플릿 예시&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Why need this change? / Root case:&lt;/li&gt;
&lt;li&gt;Changes made:&lt;/li&gt;
&lt;li&gt;Test Scope / Change impact:&lt;/li&gt;
&lt;li&gt;Veriried Screenshots (optional)&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;코드 리뷰에서 중요한 부분 중 하나는 개발자들의 성장과 노력에 대해 보상하는 것. 최대한 많이 칭찬~&lt;/blockquote&gt;</description>
      <category>개발 메모</category>
      <author>Whiimsy</author>
      <guid isPermaLink="true">https://danbom425.tistory.com/108</guid>
      <comments>https://danbom425.tistory.com/108#entry108comment</comments>
      <pubDate>Thu, 8 Aug 2024 12:43:02 +0900</pubDate>
    </item>
  </channel>
</rss>