<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>열정을 기록하기</title>
    <link>https://huncozyboy.tistory.com/</link>
    <description>이지훈</description>
    <language>ko</language>
    <pubDate>Wed, 8 Apr 2026 17:00:42 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>huncozyboy</managingEditor>
    <image>
      <title>열정을 기록하기</title>
      <url>https://tistory1.daumcdn.net/tistory/7531964/attach/c00cc4ad454d44e888a801d45fba8d1c</url>
      <link>https://huncozyboy.tistory.com</link>
    </image>
    <item>
      <title>Claude Code Skills로 코드리뷰 Skill 커스텀 해보기</title>
      <link>https://huncozyboy.tistory.com/68</link>
      <description>&lt;h2 data-end=&quot;413&quot; data-start=&quot;406&quot; data-section-id=&quot;1fudykl&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-end=&quot;705&quot; data-start=&quot;415&quot; data-ke-size=&quot;size16&quot;&gt;Claude Code의 Skills는 Claude가 특정 작업을 더 잘 수행하도록 지침, 스크립트, 리소스를 폴더 단위로 묶어두는 방식이다.&lt;br /&gt;쉽게 말하면, 비슷한 역할을 수행하는 기능들을 하나의 묶음으로 구성해 재사용 가능하게 만든 구조라고 이해하면 된다&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에서는 Skills를 이렇게 설명한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Skills extend what Claude can do.&lt;br /&gt;Create a SKILL.md file with instructions, and Claude adds it to its toolkit.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;SKILL.md&lt;/span&gt; 파일 하나만 만들어줘서 Claude의 &amp;ldquo;능력&amp;rdquo; 자체를 확장할 수 있다는 의미다&lt;/p&gt;
&lt;p data-end=&quot;705&quot; data-start=&quot;415&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://code.claude.com/docs/en/slash-commands&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://code.claude.com/docs/en/slash-commands&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774253476683&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Extend Claude with skills - Claude Code Docs&quot; data-og-description=&quot;Create, manage, and share skills to extend Claude's capabilities in Claude Code. Includes custom commands and bundled skills.&quot; data-og-host=&quot;code.claude.com&quot; data-og-source-url=&quot;https://code.claude.com/docs/en/slash-commands&quot; data-og-url=&quot;https://code.claude.com/docs/en/skills&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gZ4Cf/dJMb9lk6yuC/l08hcnh5UTY8C8vqG1PbgK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/uoijO/dJMb9efdmeI/pSsf8lIJ9LU9ITEA3k3kP1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://code.claude.com/docs/en/slash-commands&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://code.claude.com/docs/en/slash-commands&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gZ4Cf/dJMb9lk6yuC/l08hcnh5UTY8C8vqG1PbgK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/uoijO/dJMb9efdmeI/pSsf8lIJ9LU9ITEA3k3kP1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&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;Extend Claude with skills - Claude Code Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Create, manage, and share skills to extend Claude's capabilities in Claude Code. Includes custom commands and bundled skills.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;code.claude.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-end=&quot;705&quot; data-start=&quot;415&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;SKILL.md를 중심으로 동작하고, 필요하면 /skill-name 형태로 직접 호출할 수도 있다. Claude 내부 context에 로드시켜서 특정 명령어를 입력한다면, Claude가 작업 문맥에 맞춰 자동으로 불러오기도 한다. 만약에 반복해서 하는 작업들이 있다면 재사용이 가능하도록, 리소스를 줄일 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;705&quot; data-start=&quot;415&quot; data-ke-size=&quot;size16&quot;&gt;해당 기능을 사용해보면서, 개발자가 아니여도 PDF, PPT, Word, Excel 같은 것도 원하는대로 커스텀하여 제작 시간을 줄일 수도 있겠다라는 생각이 들기도 하였다&lt;br /&gt;&lt;br /&gt;Anthropic에서 공식으로 Skills 마켓플레이스를 제공해주고 있으니까, 여러가지 내용들을 둘러보고 필요한 내용들은 활용해준다면 좋을거같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/anthropics/skills&quot;&gt;https://github.com/anthropics/skills&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774253768344&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - anthropics/skills: Public repository for Agent Skills&quot; data-og-description=&quot;Public repository for Agent Skills. Contribute to anthropics/skills development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/anthropics/skills&quot; data-og-url=&quot;https://github.com/anthropics/skills&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nAUQ8/dJMb9frESeS/FSk9tJagdDNLYjZw6KkYU1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/i03qe/dJMb9fZuJSy/cWpaun3kx4JfVhKClCtUq1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/anthropics/skills&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/anthropics/skills&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nAUQ8/dJMb9frESeS/FSk9tJagdDNLYjZw6KkYU1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/i03qe/dJMb9fZuJSy/cWpaun3kx4JfVhKClCtUq1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - anthropics/skills: Public repository for Agent Skills&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Public repository for Agent Skills. Contribute to anthropics/skills development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서 기준으로 Skill은 아래의 구조처럼 생성된다고 설명한다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Skills 구조 이해하기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ekGPMT/dJMcadOQNxB/URi23XtF8UvGVGk7uXO6a1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ekGPMT/dJMcadOQNxB/URi23XtF8UvGVGk7uXO6a1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ekGPMT/dJMcadOQNxB/URi23XtF8UvGVGk7uXO6a1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FekGPMT%2FdJMcadOQNxB%2FURi23XtF8UvGVGk7uXO6a1%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;300&quot; height=&quot;288&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위는 예시이고, 여기서 &lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;SKILL.md&lt;/span&gt;를&lt;span&gt; &lt;/span&gt;커스텀한다고&lt;span&gt; &lt;/span&gt;생각해주면&lt;span&gt; &lt;/span&gt;된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;name은 레거시 코드 내용 팔로업으로 네이밍하고, description에서는 간단하게 코드 팔로업용 Skill과 아래의 내용들을 작성해놓을 수도 있다&lt;br /&gt;코드를 설명시, 아래 내용 포함 :&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;1.&lt;/span&gt; 비유&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;2.&lt;/span&gt; 다이어그램&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;3.&lt;/span&gt; API 요청&lt;span&gt; - &lt;/span&gt;응답 플로우&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. Skill은 언제 실행될까?&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Claude uses skills when relevant, or you can invoke one directly with /skill-name.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 보면 자동 실행 (문맥 기반)과 앞서 서론에서 짧게 설명한&amp;nbsp;&lt;span&gt;수동 실행 (&lt;/span&gt;/skill-name&lt;span&gt;) 방식을 의미한다 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;좀 더 자세히 설명해보면, 자동 실행은 Claude가 현재 요청의 의도를 해석한 뒤 가장 적절한 Skill을 스스로 선택해 적용하는 방식이다. 예시로 테스트 코드 작성이나 깃허브 PR, 이슈 등등의 문서처럼 반복적인 컨벤션이 있는 작업에서는 사용자가 명시하지 않아도 별도의 호출 없이 관련 Skill이 자연스럽게 적용될 수도 있다&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;br /&gt;해당 이유에서 수동 실행의 세부적인 커스텀이 중요하다. 결론부터 먼저 얘기하자면 수동 실행을 잘쓰기 위해선 &lt;/span&gt;&lt;span&gt;description&lt;/span&gt;을 잘 써야 한다. 이유는 description이 모호하면 의도하지 않은대로 자동 실행이 작동할 수도 있고, 만약에 Skill을 사용하여 수동으로 호출하더라도 의도한 방식으로 동작하지 않을 가능성이 높기때문이다&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CJ40h/dJMcaaklWf1/CKwB2uXKtOkJPEENSo1qH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CJ40h/dJMcaaklWf1/CKwB2uXKtOkJPEENSo1qH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CJ40h/dJMcaaklWf1/CKwB2uXKtOkJPEENSo1qH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCJ40h%2FdJMcaaklWf1%2FCKwB2uXKtOkJPEENSo1qH1%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;700&quot; height=&quot;371&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 /skill-name 을 설정해두면 편리한 이유는 로고, 폰트, 색상 같은 브랜딩 규칙이나 특정 팀의 문서 양식, 리뷰 기준, 출력 형식을&lt;span&gt; 클로드에게 반복해서 설명하지 않아도 된다는 점이다. 한번 Skill로 정의해두면 Claude가 해당 컨텍스트를 일관되게 재사용하기 때문에, 기준이 흔들리지 않고 원하는 품질에 가깝게 결과를 유지할 수 있다&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 본격적으로 &lt;span&gt;코드리뷰&lt;/span&gt; &lt;span&gt;관련&lt;/span&gt; Skills를 만드는 예시를 설명해보려한다&lt;br /&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 초기 세팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;skill-creator는 document-skills가 아니다. 이게 무슨 말이냐면, &lt;span&gt;document-skills&lt;/span&gt;는 이름 그대로 문서 생성 계열 Skill 묶음일 뿐이고 코드리뷰 Skill을 만들고 싶다면 &lt;span&gt;document-skills&lt;/span&gt;만 설치해서는 부족하다는 뜻이다. 따라서 추가적인 설치가 필요하다는 뜻이다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;br /&gt;/plugin marketplace add anthropics/skills&lt;br /&gt;&lt;br /&gt;/plugin install document-skills@anthropic-agent-skills&lt;br /&gt;&lt;br /&gt;/plugin install example-skills@anthropic-agent-skills&lt;br /&gt;&lt;br /&gt;/reload-plugins&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 문서 생성만 하고싶다면 두번째 줄 플러그인만 설치해도 충분하지만, 코드 리뷰용 Skill을 만들기 위해서는 해당 플러그인들이 모두 필요하다고 이해해주면 된다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 코드 리뷰 가이드 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 먼저 Claude에게 가이드를 주고 시작하는 것이 좀 더 완성도가 높았던거같다. 예를 들면 아래처럼 시작할 수 있다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;코드리뷰 관련 skills를 작성하고 싶은데, 먼저 코드리뷰 best practice를 찾아서 학습해봐. 특히 아키텍처 경계, 예외 처리, 테스트 가능성, 성능, 보안, 가독성 관점으로 정리해줘.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. skill-creator로 코드리뷰 Skill 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기준 정리가 끝나면 이제 실제 Skill 생성을 시킬 수 있는데, 아래는 예시니까 참고만 해주면 좋을거같다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이제 skill-creator skills를 써서&lt;br /&gt;&lt;br /&gt;방금 학습한 코드리뷰 철학으로 코드리뷰 Skills를 만들어줘.&lt;br /&gt;&lt;br /&gt;Compaction이 일어나도 중요한 상태를 유지할 수 있게&lt;br /&gt;&lt;br /&gt;reference 파일, 체크리스트, 중간 산출물 구조도 함께 고려해줘.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 서론에 설명한거처럼 Skills.md가 생성되는데, 레퍼런스 파일들도 함께 생성됩니다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWENkG/dJMcahqfC4R/Dhx1rNNuXS8xFyYIs1evqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWENkG/dJMcahqfC4R/Dhx1rNNuXS8xFyYIs1evqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWENkG/dJMcahqfC4R/Dhx1rNNuXS8xFyYIs1evqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWENkG%2FdJMcahqfC4R%2FDhx1rNNuXS8xFyYIs1evqk%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;400&quot; height=&quot;389&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;span&gt;SKILL.md&lt;/span&gt;의 frontmatter는 최대한 자세하게 적어주는게 좋다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Compaction까지 고려해서 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점이 하나 있는데, Skill 자체가 대화 상태를 영구 저장해주는 것은 아니다. Claude Code는 컨텍스트가 차면 자동으로 또는 &lt;span&gt;/compact&lt;/span&gt; 명령으로 대화를 요약해 공간을 확보할 수 있는데, 이 과정에서 세부 맥락이 일부 사라질 수도 있다. 공식 문서도 compaction 이후 중요한 문맥이 손실될 수 있다고 설명하면서, &lt;span&gt;SessionStart&lt;/span&gt; hook의 &lt;span&gt;compact&lt;/span&gt; matcher를 이용해 핵심 컨텍스트를 다시 주입하는 예시를 제공해주기도 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;따라서 아래의 내용처럼 설계해주면, 커스텀한 내용들이 휘발되지 않도록 복구하는 플로우를 생성해줄 수도 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, &lt;span&gt;SKILL.md&lt;/span&gt;에는 핵심 workflow만 두고 상세 기준은 &lt;span&gt;references/&lt;/span&gt;로 분리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 리뷰 중간 결론이나 체크포인트는 &lt;span&gt;.claude/state/code-review-summary.md&lt;/span&gt; 같은 파일에 따로 남긴다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋째, compaction 이후 이 파일을 다시 읽게 만드는 hook을 둔다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 해당 방식의 장점은 명확하다고 생각한다&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 한 번 잘 만들어둔 작업 방식을 계속 재사용할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;span&gt;document-skills&lt;/span&gt;와 결합하면, 코드리뷰 이후 결과를 Word나 PPT 요약본 등으로 시각화 할 수도 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Anthropic에서 제공해주는 공식 플러그인이라 그런지 퀄리티는 보장해주는거같다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dFsvJW/dJMcadBiBVd/fX9rOsozg85Nz7fRbtmSKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dFsvJW/dJMcadBiBVd/fX9rOsozg85Nz7fRbtmSKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dFsvJW/dJMcadBiBVd/fX9rOsozg85Nz7fRbtmSKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdFsvJW%2FdJMcadBiBVd%2FfX9rOsozg85Nz7fRbtmSKk%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;600&quot; height=&quot;307&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;570&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>AI/Claude Code</category>
      <category>anthropic</category>
      <category>claude code</category>
      <category>document-skills</category>
      <category>example-skills</category>
      <category>skill-creator</category>
      <category>Skills</category>
      <author>huncozyboy</author>
      <guid isPermaLink="true">https://huncozyboy.tistory.com/68</guid>
      <comments>https://huncozyboy.tistory.com/68#entry68comment</comments>
      <pubDate>Mon, 23 Mar 2026 18:20:39 +0900</pubDate>
    </item>
    <item>
      <title>클로드 코드 개발자 Boris Cherny의 클로드 코드 사용법 (Claude Code)</title>
      <link>https://huncozyboy.tistory.com/67</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;요즘 클로드 코드(Claude Code)가 정말 빠른 속도로 발전하고 있다고 생각한다. 얼마전에 &lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;1000개 이상의 쿼리를 단기간에 마이그레이션 하기위해서 &lt;b&gt;Copilot Agent Mode &lt;/b&gt;를 활용했던거 처럼, 단순 AI 발전 말고도 개발자의 더 명확한 판단 능력, 검증을 설계하는 능력, 작업을 자동화하는 능력 등등이 더 중요해지고 있다고 판단했다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;a href=&quot;https://huncozyboy.tistory.com/66&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://huncozyboy.tistory.com/66&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770457525510&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;AI Copilot Agent Mode로 SQL 쿼리 마이그레이션 (Copilot)&quot; data-og-description=&quot;들어가며CUBRID 데이터베이스 환경에서 돌아가던 SQL 쿼리들을 MySQL 8.x로 마이그레이션하는 작업을 한 내용으로 포스팅을 해보려고 한다.AI Copilot(Agent Mode)을 활용해 1000개가 넘는 쿼리 변환을 성공&quot; data-og-host=&quot;huncozyboy.tistory.com&quot; data-og-source-url=&quot;https://huncozyboy.tistory.com/66&quot; data-og-url=&quot;https://huncozyboy.tistory.com/66&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/SkEsA/dJMb83Sep3u/4ipmROKJ6qU84sTO52KPDk/img.jpg?width=300&amp;amp;height=168&amp;amp;face=0_0_300_168,https://scrap.kakaocdn.net/dn/zWz0e/dJMb83koxEx/SSDkTsXPIsbvD68klC9ud0/img.jpg?width=300&amp;amp;height=168&amp;amp;face=0_0_300_168,https://scrap.kakaocdn.net/dn/qrURv/dJMb81GSL6j/7meZ3mYvdEcPtDxDAz2foK/img.png?width=1166&amp;amp;height=750&amp;amp;face=0_0_1166_750&quot;&gt;&lt;a href=&quot;https://huncozyboy.tistory.com/66&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://huncozyboy.tistory.com/66&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/SkEsA/dJMb83Sep3u/4ipmROKJ6qU84sTO52KPDk/img.jpg?width=300&amp;amp;height=168&amp;amp;face=0_0_300_168,https://scrap.kakaocdn.net/dn/zWz0e/dJMb83koxEx/SSDkTsXPIsbvD68klC9ud0/img.jpg?width=300&amp;amp;height=168&amp;amp;face=0_0_300_168,https://scrap.kakaocdn.net/dn/qrURv/dJMb81GSL6j/7meZ3mYvdEcPtDxDAz2foK/img.png?width=1166&amp;amp;height=750&amp;amp;face=0_0_1166_750');&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;AI Copilot Agent Mode로 SQL 쿼리 마이그레이션 (Copilot)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며CUBRID 데이터베이스 환경에서 돌아가던 SQL 쿼리들을 MySQL 8.x로 마이그레이션하는 작업을 한 내용으로 포스팅을 해보려고 한다.AI Copilot(Agent Mode)을 활용해 1000개가 넘는 쿼리 변환을 성공&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;huncozyboy.tistory.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;p data-end=&quot;420&quot; data-start=&quot;263&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;이번 글은 클로드 코드를 만든 사람, 보리스(Boris Cherny)가 직접 공유한 사용법을 인용해서 정리해보았다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;1288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nK31U/dJMcahcd6pQ/ACk5jcZs82WBFvcavytnVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nK31U/dJMcahcd6pQ/ACk5jcZs82WBFvcavytnVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nK31U/dJMcahcd6pQ/ACk5jcZs82WBFvcavytnVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnK31U%2FdJMcahcd6pQ%2FACk5jcZs82WBFvcavytnVK%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;600&quot; height=&quot;662&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;1288&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;420&quot; data-start=&quot;263&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style7&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) Claude 프로세스 5개를 동시에 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 터미널에서 Claude 프로세스 5개를 동시에 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탭에 1~5번까지 번호를 매기고, 시스템 알림을 사용하여 Claude 프로세스가 입력을 필요로 할 때를 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 추가적으로 설명하자면 1번 프로세스는 특정 기능 구현, 2번 프로세스는 테스트 코드 작성, 3번 프로세스는 작업한 내용 문서화,&amp;nbsp;&lt;br /&gt;4번 프로세스는 PR 내용 작성 등등으로 한가지의 큰 task가 아닌 여러가지의 task들을 동시에 돌리는 것들도 가능하다는 얘기이다.&lt;br /&gt;(한 세션에서 복잡해진 대화가 다른 작업을 오염시키지 않는다는 점이다. 대신 같은 파일을 여러 세션이 동시에 건드리면 충돌이 날 수 있으니, 보통은 큰 API에 대한 작업이라면&amp;nbsp;&lt;span&gt;&lt;b&gt;작업 범위를 파일/도메인 단위로 분리&lt;/b&gt;&lt;/span&gt;해서 맡기기도 한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://code.claude.com/docs/en/terminal-config#iterm-2-system-notifications&quot;&gt;https://code.claude.com/docs/en/terminal-config#iterm-2-system-notifications&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvOimd/dJMcaivpCox/kNikjQfXFVfZjCKdaxDnWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvOimd/dJMcaivpCox/kNikjQfXFVfZjCKdaxDnWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvOimd/dJMcaivpCox/kNikjQfXFVfZjCKdaxDnWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvOimd%2FdJMcaivpCox%2FkNikjQfXFVfZjCKdaxDnWk%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;680&quot; height=&quot;446&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 로컬 Claude 세션 병렬 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 로컬 Claude 세션과 병렬로 &lt;a href=&quot;https://claude.ai/code&quot;&gt;https://claude.ai/code&lt;/a&gt; 서버에서 5~10개의 Claude 세션을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 코딩할 때, 종종 로컬 세션을 웹 서버로 전환(&amp;amp; 사용)하거나 Chrome에서 수동으로 세션을 시작하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;Boris는&lt;/span&gt; --teleport 명령어를 사용하여 터미널과 웹 서버를 오가기도 하면서, 매일 아침과 하루 동안 휴대폰(Claude iOS 앱)에서 몇 개의 세션을 시작하고 나중에 확인한다고 말했다. (즉, 터미널만 사용한다기보다는 로컬 세션을 웹/모바일로 이어받거나, 웹에서 시작한 세션을 터미널로 가져오는 식의 작업들도 가능하다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTgbOJ/dJMcac22A5B/KoAKxqtLVwOkcpqY3e2Nz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTgbOJ/dJMcac22A5B/KoAKxqtLVwOkcpqY3e2Nz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTgbOJ/dJMcac22A5B/KoAKxqtLVwOkcpqY3e2Nz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTgbOJ%2FdJMcac22A5B%2FKoAKxqtLVwOkcpqY3e2Nz1%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;680&quot; height=&quot;690&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) Opus 4.5 모델 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델은 모든 것에 사고 기능을 갖춘 Opus 4.5를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 사용해 본 코딩 모델 중 최고이며, Sonnet보다 크고 느리긴 하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 조작할 필요가 적고 도구 활용 능력이 뛰어나기 때문에 결과적으로는 더 작은 모델을 사용하는 것보다 거의 항상 더 빠르다고 한다.&lt;br /&gt;(+ 2026년 3월 기준으로는 opusplan을 사용하여 &lt;b&gt;plan mode에서는 Opus를 쓰고 실행 단계에서는 Sonnet을 사용&amp;nbsp;&lt;/b&gt;하는거처럼 유동적으로도 가능하다)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) Claude Code md 파일 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;Boris의&amp;nbsp;&lt;/span&gt;&lt;/span&gt;팀은 Claude Code 저장소에 대해 하나의 &lt;a href=&quot;https://code.claude.com/docs/&quot;&gt;CLAUDE.md&lt;/a&gt; 공유한다. 변경사항을 git에 커밋하고, 팀 전체가 일주일에 여러 번 기여한다. Claude가 잘못된 작업을 수행하는 것을 발견할 때마다 CLAUDE.md 에 추가하여 Claude가 다음번에 같은 실수를 하기 위함이기 때문에, 다른 팀들은 자체적인 CLAUDE.md '를 유지한다. 각 팀은 자신의 '를 최신 상태로 유지해야 할 책임이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아키텍처 원칙&lt;/li&gt;
&lt;li&gt;테스트 코드 컨벤션, 실행 순서&lt;/li&gt;
&lt;li&gt;수정하면 안되는 파일&lt;/li&gt;
&lt;li&gt;코드 스타일&lt;/li&gt;
&lt;li&gt;DB/도메인 규칙&lt;/li&gt;
&lt;li&gt;N+1 위험이 있는 쿼리 패턴 지양&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5) PR에 @.claude를 태그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 리뷰 중에 &lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;Boris는&lt;/span&gt; 종종 동료들의 PR에 @.claude를 태그하여 PR의 일부로 CLAUDE.md 에 무언가를 추가한다고 한다. 이를 위해 Claude Code GitHub 액션 (/install-github-action)을 사용한다.&amp;nbsp;해당 내용은 @danshipper 의 Compounding Engineering과 유사한 기능을 의미한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6) 계획 모드 -&amp;gt; 승인 모드 전환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 세션은 계획 모드(Shift+Tab 두 번 누르기)에서 시작한다. 만약에 풀 리퀘스트를 작성하는 것이 목표라면 계획 모드를 사용하고, 마음에 드는 계획이 나올 때까지 Claude와 의견을 주고받고, 계획이 완성되면 편집 자동 승인 모드로 전환하고, Claude는 보통 한 번에 풀 리퀘스트를 승인하는 과정을 가진다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(개인적으로 클로드를 다른 AI와 구분하여, 제일 선호하는 이유이기도 하다. Claude가 변경 대상 파일, 영향 범위, 위험 지점, 테스트 전략 등등을 제안하며 수준 높은 바이브 코딩이 가능하다고 생각한다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2TI5S/dJMcacPx1A6/WfthVZ5FM41hoHlCflhYBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2TI5S/dJMcacPx1A6/WfthVZ5FM41hoHlCflhYBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2TI5S/dJMcacPx1A6/WfthVZ5FM41hoHlCflhYBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2TI5S%2FdJMcacPx1A6%2FWfthVZ5FM41hoHlCflhYBk%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;680&quot; height=&quot;208&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;208&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7) 내부 루프 워크플로에 슬래시 명령어를 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하루에도 여러 번 반복해서 수행하는 모든 내부 루프 워크플로에 슬래시 명령어를 사용한다고 한다. 이렇게 하면 반복적인 프롬프트 표시를 피할 수 있고, Claude도 이러한 워크플로를 사용할 수 있으며, 명령어는 Git에 커밋되어 .claude/commands/ 디렉토리에 저장되어 관리된다. 예를 들어, Claude와 매일 수십 번씩 /commit-push-pr 슬래시 명령어를 사용하는데, 해당 명령어는 인라인 bash를 사용하여 git 상태와 몇 가지 다른 정보를 미리 계산하여 명령어가 빠르게 실행되고 모델과의 반복적인 상호 작용을 방지할 수 있는 효과가 있다.(&lt;a href=&quot;https://code.claude.com/docs/en/slash-commands#bash-command-execution&quot;&gt;https://code.claude.com/docs/en/slash-commands#bash-command-execution&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cM6uBP/dJMcacWhIP9/IgZSGg57UPlKJyiTEuOCA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cM6uBP/dJMcacWhIP9/IgZSGg57UPlKJyiTEuOCA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cM6uBP/dJMcacWhIP9/IgZSGg57UPlKJyiTEuOCA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcM6uBP%2FdJMcacWhIP9%2FIgZSGg57UPlKJyiTEuOCA0%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;680&quot; height=&quot;166&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8) 몇 가지 서브에이전트의 정기적으로 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;code-simplifier는 Claude가 작업을 완료한 후 코드를 간소화하고, verify-app은 Claude 코드의 엔드 투 엔드 테스트에 대한 자세한 지침을 제공하는 등의 기능을 해준다. &lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;Boris는&amp;nbsp;&lt;/span&gt;슬래시 명령어와 마찬가지로 서브에이전트는 대부분의 PR에서 수행하는 가장 일반적인 워크플로를 자동화하는 도구라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://code.claude.com/docs/en/sub-agents&quot;&gt;https://code.claude.com/docs/en/sub-agents&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;462&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bg4Avp/dJMcaiWue8m/siAUl7g8NigWBcRSVi1PU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bg4Avp/dJMcaiWue8m/siAUl7g8NigWBcRSVi1PU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg4Avp/dJMcaiWue8m/siAUl7g8NigWBcRSVi1PU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbg4Avp%2FdJMcaiWue8m%2FsiAUl7g8NigWBcRSVi1PU1%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;462&quot; height=&quot;318&quot; data-origin-width=&quot;462&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;code-simplifier.md&lt;span&gt;: 구현 끝난 뒤 복잡도 낮추기, 가독성 높이기 등등&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;verify-app.md&lt;/span&gt;: 테스트, 브라우저, E2E 검증&lt;/li&gt;
&lt;li&gt;oncall-guide.md&lt;span&gt;: 장애 대응 점검 절차&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;db-reviewer.md&lt;/span&gt;: 쿼리, 인덱스, N+1 점검&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 퇴근시에 background로 작업한 내용들이 궁금하다면 session-summary.md&lt;span&gt;&lt;span&gt; 등의 파일을 활용하여 작업에 대한 원하는 양식으로 요약을 담당할 수도 있고, &lt;/span&gt;&lt;/span&gt;Notification hook 등의 md 파일을 관리하여 작업한 내용에 대한 알림을 받을 수도 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9) PostToolUse 훅을 사용하여 Claude의 코드 형식을 지정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude는 일반적으로 기본적으로 형식이 잘 지정된 코드를 생성하며, 이 훅은 나중에 CI에서 형식 오류가 발생하지 않도록 나머지 10%를 처리한다. hook은 &lt;span&gt;CLAUDE.md&lt;/span&gt;와 다르게 &lt;span&gt;&lt;b&gt;결정론적으로 실행되는 자동화&lt;/b&gt;&lt;/span&gt;다. 공식 문서도 &amp;ldquo;CLAUDE.md는 advisory, hooks는 deterministic&amp;rdquo;이라고 설명한다. 그래서 Claude가 파일을 수정할 때마다 아래 명령어 들을 ostToolUse hook으로 돌리면, 사람이 마지막에 검증시에 시간을 덜 쓰게 된다. 이 설정은 .claude/settings.json 또는 /hooks 메뉴로 관리할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;spotlessApply&lt;br /&gt;Gradle에 붙인 Spotless 포맷터를 실행해서, 프로젝트에 설정된 Java/Gradle/Kotlin 등의 코드 스타일 위반을 자동으로 수정해준다. (Java라고 가정시에 17, 21 버전 등등에 맞게 설정 가능)&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;br /&gt;./gradlew ktlintFormat&lt;br /&gt;Kotlin 프로젝트에서 ktlint 규칙에 맞게 .kt, Kotlin script 파일들을 자동 포맷한다. ktlint Gradle plugin 공식 문서에 잘 나와있으며,&amp;nbsp; ktlintFormat을 Kotlin 파일과 Kotlin script를 코드 스타일에 맞게 format&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P0JhO/dJMcahXChRx/WcWXMXheVBg2AnKZN8MbUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P0JhO/dJMcahXChRx/WcWXMXheVBg2AnKZN8MbUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P0JhO/dJMcahXChRx/WcWXMXheVBg2AnKZN8MbUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP0JhO%2FdJMcahXChRx%2FWcWXMXheVBg2AnKZN8MbUk%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;680&quot; height=&quot;322&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10)&amp;nbsp;--dangerously-skip-permissions&lt;span&gt; 옵션 미사용&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--dangerously-skip-permissions 옵션을 사용하는대신 &lt;span&gt;/permissions&lt;/span&gt; 명령어를 사용하여 제 환경에서 안전하다고 판단되는 일반적인 bash 명령어를 미리 허용함으로써 불필요한 권한 요청 메시지를 방지한다. 해당 설정 대부분은 &lt;span&gt;.claude/settings.json&lt;/span&gt; 파일에 저장되어 팀과 공유된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhSGYX/dJMcaflbQUd/sOFRy9HnOIuMvEAOy5S7VK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhSGYX/dJMcaflbQUd/sOFRy9HnOIuMvEAOy5S7VK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhSGYX/dJMcaflbQUd/sOFRy9HnOIuMvEAOy5S7VK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdhSGYX%2FdJMcaflbQUd%2FsOFRy9HnOIuMvEAOy5S7VK%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;680&quot; height=&quot;566&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11) Claude Code에서도 사용하는 모든 도구를 활용 (MCP)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;Boris는&amp;nbsp;&lt;/span&gt;MCP 서버를 통해 Slack에 검색 및 게시물을 올리고, bq CLI를 사용하여 BigQuery 쿼리를 실행하여 분석 관련 질문에 답변하고, Sentry에서 오류 로그를 가져오는 등의 작업을 수행한다고 한다. 또한 Slack MCP 구성은 .mcp.json 파일에 커밋되어 팀과 공유된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;MCP란 ? &lt;br /&gt;Claude Code에 외부 도구, 데이터베이스, API를 붙이는 표준 방식을 말한다. 공식 문서도 MCP를 통해 수백 개의 외부 도구와 데이터 소스에 연결할 수 있다고 설명한다. 보통 팀 실무에서는 &lt;span&gt;.mcp.json&lt;/span&gt;을 저장소에 커밋해 두고, 프로젝트 공통 도구를 함께 쓰게 만들어주기도 한다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlZ7AT/dJMcab39wzz/MIuwwAOJ76u5dGFePRoTw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlZ7AT/dJMcab39wzz/MIuwwAOJ76u5dGFePRoTw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlZ7AT/dJMcab39wzz/MIuwwAOJ76u5dGFePRoTw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdlZ7AT%2FdJMcab39wzz%2FMIuwwAOJ76u5dGFePRoTw1%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;680&quot; height=&quot;255&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;255&quot;/&gt;&lt;/span&gt;&lt;/figure&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;(a) 작업이 완료되면 백그라운드 에이전트를 사용하여 Claude가 작업을 확인하도록 메시지를 표시하거나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(b) 에이전트 Stop 후크를 사용하여 보다 확정적으로 확인하거나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(c) ralph-wiggum 플러그인(원래 @GeoffreyHuntley 이 구상함)을 사용한다. 또한 샌드박스 환경에서 --permission-mode=dontAsk 또는 --dangerously-skip-permissions 옵션을 사용하여 세션에 대한 권한 프롬프트를 방지함으로써 Claude가 제 작업으로 인해 차단되지 않고 작업을 수행할 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/anthropics/claude-plugins-official/tree/main/plugins/ralph-wiggum&quot;&gt;https://github.com/anthropics/claude-plugins-official/tree/main/plugins/ralph-wiggum&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://code.claude.com/docs/en/hooks-guide&quot;&gt;https://code.claude.com/docs/en/hooks-guide&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctvlc9/dJMcai90BnL/p8xTRdkpdjWdbUoxx3ZTO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctvlc9/dJMcai90BnL/p8xTRdkpdjWdbUoxx3ZTO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctvlc9/dJMcai90BnL/p8xTRdkpdjWdbUoxx3ZTO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fctvlc9%2FdJMcai90BnL%2Fp8xTRdkpdjWdbUoxx3ZTO1%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;680&quot; height=&quot;150&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;150&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13)&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Claude에게 작업 검증 방법을 제공&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code -- 에서 훌륭한 결과를 얻기 위한 가장 중요한 것은 Claude에게 작업 검증 방법을 제공하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude가 이러한 피드백 루프를 갖추면 최종 결과의 품질이 2~3배 향상될 것이라고 &lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;Boris는 말한다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 테스트, 빌드, 시뮬레이터, 브라우저 검증 같은 건 Claude Code가 background bash로 돌릴 수 있고, subagent도 background로 운영할 수 있다.&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 구체적으로는 Claude는 Claude Chrome 확장 프로그램을 사용하여 &lt;a href=&quot;https://claude.ai/code&quot;&gt;https://claude.ai/code&lt;/a&gt; 에 적용하는 모든 변경 사항을 테스트한 다음, 브라우저를 열고 UI를 테스트한 다음 코드가 작동하고 UX가 만족스러울 때까지 반복한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 도메인마다 검증 방식이 다르고, 간단한 bash 명령어 실행부터 테스트 스위트 실행, 브라우저나 모바일 시뮬레이터에서의 앱 테스트까지 다양한 방법이 있을 수 있다. 무엇보다 중요한 것은 이러한 검증 과정을 철저하게 구축하는 것이라고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;느낀점&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;최근에 클로드를 동시에 여러개 돌리면서, 확실히 발전이 체감이 되는거같다. 예전 같으면 몇 주 걸렸을 일들이 며칠 안에 정리되는 적도 있었다. 동시에 드는 생각은 구현 속도가 빨라진 만큼 잘못된 방향으로 가는 속도도 같이 빨라진다. 그래서 이제는 타이핑을 누가 더 빨리 하냐보다, 그 폭발적인 속도의 AI를 올바른 방향으로 가도록 결정하는 개발자의 판단이 더 중요해지는 것 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고한 자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://code.claude.com/docs/en/chrome&quot;&gt;https://code.claude.com/docs/en/chrome&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://x.com/bcherny/status/2007179833990885678?s=12&quot;&gt;https://x.com/bcherny/status/2007179833990885678?s=12&lt;/a&gt;&lt;/p&gt;</description>
      <category>AI/Claude Code</category>
      <category>Boris Cherny</category>
      <category>Claude</category>
      <category>claude code</category>
      <category>mcp</category>
      <category>PostToolUse</category>
      <author>huncozyboy</author>
      <guid isPermaLink="true">https://huncozyboy.tistory.com/67</guid>
      <comments>https://huncozyboy.tistory.com/67#entry67comment</comments>
      <pubDate>Sat, 7 Feb 2026 19:56:56 +0900</pubDate>
    </item>
    <item>
      <title>AI Copilot Agent Mode로 SQL 쿼리 마이그레이션 (Copilot)</title>
      <link>https://huncozyboy.tistory.com/66</link>
      <description>&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CUBRID 데이터베이스 환경에서 돌아가던 SQL 쿼리들을 &lt;b&gt;MySQL 8.x&lt;/b&gt;로 마이그레이션하는 작업을 한 내용으로 포스팅을 해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI Copilot(Agent Mode)을 활용해 1000개가 넘는 쿼리 변환을 성공적으로 마칠 수 있었고, 최근 AI 기술 발전에 관심 있는 개발자분들에게 해당 경험 공유가 도움이 되었으면 좋겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFCyhL/dJMcahpJyrj/NnZu7ERrp56dP3yv0BX0Vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFCyhL/dJMcahpJyrj/NnZu7ERrp56dP3yv0BX0Vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFCyhL/dJMcahpJyrj/NnZu7ERrp56dP3yv0BX0Vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFCyhL%2FdJMcahpJyrj%2FNnZu7ERrp56dP3yv0BX0Vk%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;800&quot; height=&quot;515&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MySQL 8.x 로 마이그레이션을 결정한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 기존의 CUBRID 시스템에서 &lt;b&gt;MySQL 8.x&lt;/b&gt; 로 옮기게된 이유부터 설명하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RJmnz/dJMcabXk4Nn/ggVNkStTI9siCTkTPyh2Lk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RJmnz/dJMcabXk4Nn/ggVNkStTI9siCTkTPyh2Lk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RJmnz/dJMcabXk4Nn/ggVNkStTI9siCTkTPyh2Lk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRJmnz%2FdJMcabXk4Nn%2FggVNkStTI9siCTkTPyh2Lk%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;500&quot; height=&quot;155&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫번째, 잦은 Lock 발생&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RANGE 조건이 들어간 쿼리가 특정 시간대에 몰리면, 인덱스 범위 스캔 과정에서 &lt;b&gt;잠금이 생각보다 넓게, 많이 잡히는&lt;/b&gt; 순간이 종종 있었다. 특히 업데이트 + 삭제처럼 쓰기 작업이 섞이면, 한 건이 느린 게 아니라 &lt;b&gt;대기열이 길어지며 다른 트랜잭션까지 줄줄이 밀리는&lt;/b&gt; 형태로 체감 성능이 급격히 떨어졌다. MySQL(InnoDB)은 기본적으로 &lt;b&gt;인덱스 레코드 단위로 잠금을 잡는 구조&lt;/b&gt;라 같은 범위 조건이라도 인덱스 + 쿼리 전략을 잘 짜줘서 영향 범위를 더 잘 쪼개줘서 버틸 수 있다고 알고있었지만, CUBRID 쪽은 특정 패턴에서 락이 누적되며 운영이 빡세다고 생각했었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두번째, 대용량 동시성 처리에서 비효율&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 락이 많이 걸리는 순간은 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;데이터가 커지고 동시 요청이 늘면서 더 곤란해졌다. CUBRID는 락을 너무 많이 관리하게 되면 &lt;b&gt;상위 단위로 락을 격상&lt;/b&gt;을 수행할 수 있는데, 이게 발생하면 개별 행 단위로 버티던 흐름이 &lt;b&gt;더 거친 단위의 락&lt;/b&gt;으로 바뀌면서 동시 처리 효율이 확 떨어질 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; 반면 MySQL(InnoDB)은 기본적으로 인덱스 레코드 기반 row-level locking을 중심으로 동시성을 유지하는 설계라 특정 트랜잭션이 과도하게 커질때에도 버티는 비용이 CUBRID 대비 많이 좋다고 판단했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 서론이고, 그렇다면 1000개 이상의 쿼리를 단기간에 변환하기 위한 효율적인 방법으로 Copilot을 어떻게 활용했을까 ?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;마이그레이션에 Copilot 활용&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;Copilot Agent Mode&lt;/b&gt;였다. Claude 4 기반 모델에 agent-prompt.md 를 컨텍스트로 주입해 변환 규칙을 고정하고, Agent가 쿼리를 읽어 &lt;b&gt;변환 &amp;rarr; 누락 체크 &amp;rarr; 재수정&lt;/b&gt;까지 한 흐름으로 반복 처리하도록 구성해주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y4M0U/dJMcaaD8x3l/rN393jpvXoeAzAqYiKoDQk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y4M0U/dJMcaaD8x3l/rN393jpvXoeAzAqYiKoDQk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y4M0U/dJMcaaD8x3l/rN393jpvXoeAzAqYiKoDQk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY4M0U%2FdJMcaaD8x3l%2FrN393jpvXoeAzAqYiKoDQk%2Fimg.jpg&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;500&quot; height=&quot;280&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;168&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;프롬프트 작성 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 1000개라는 양의 쿼리를 한번에 바꾸기보다는, 프롬프트를 점차적으로 완성하는 방식으로 진행했다.&lt;br /&gt;간단한 쿼리부터 점차적으로 변환을 시켜보고, &lt;b&gt;Copilot &lt;/b&gt;이 놓치거나 틀린 부분을 발견하면 특정 규칙을 agent-prompt.md 파일에 추가하는 방식으로 반복했다. (기억에 남는 내용들은 아래와 같았다.)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2420&quot; data-start=&quot;2398&quot;&gt;OUTER JOIN (+) 변환 누락&lt;/li&gt;
&lt;li data-end=&quot;2461&quot; data-start=&quot;2421&quot;&gt;GROUP BY가 ONLY_FULL_GROUP_BY에서 터지게 변환됨&lt;/li&gt;
&lt;li data-end=&quot;2480&quot; data-start=&quot;2462&quot;&gt;긴 WHERE 조건 일부 누락&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;agent-prompt.md&lt;span&gt; 커스터마이징&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;272&quot; data-start=&quot;131&quot;&gt;&lt;b&gt;Alias 규칙 고정&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;a,b,c 금지. 테이블명 그대로 alias 사용&lt;/span&gt;&lt;br /&gt;-&amp;gt; 쿼리만 봐도 어떤 테이블 컬럼인지 즉시 추적 가능해서 디버깅과 리뷰 시간이 줄어듦. 조인 늘어날수록 실수도 줄고, 팀 내 스타일도 자동으로 통일할 수 있었다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li data-end=&quot;446&quot; data-start=&quot;274&quot;&gt;&lt;b&gt;CUBRID Outer Join(+) ANSI JOIN으로 강제 변환&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;(+)는 WHERE에 섞여 있어 조건 누락/방향 실수가 자주 발생하다고 판단&lt;/span&gt;&lt;br /&gt;-&amp;gt; LEFT/RIGHT JOIN으로 명시하면 조인 의도가 드러나고, ON/WHERE 분리가 되어 유지보수와 성능 튜닝과 인덱스 확인이 쉬워졌다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li data-end=&quot;613&quot; data-start=&quot;448&quot;&gt;&lt;b&gt;DB 힌트 제거&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;MySQL에서 의미 없는 힌트는 혼란만 준다고 판단&lt;/span&gt; &lt;br /&gt;-&amp;gt; CUBRID 힌트가 남아 있으면 무시되거나 오해를 유발할 수 있었다. 실행계획 분석 시 방해만 되니, MySQL 기준으로 깔끔한 쿼리만 남기는 게 안전하독 생각하였다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li data-end=&quot;840&quot; data-start=&quot;615&quot;&gt;&lt;b&gt;ONLY_FULL_GROUP_BY 대응은 원칙적으로 하도록&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;ANY_VALUE() 금지(정합성 보장 불가). ROW_NUMBER() + rn=1 패턴 사용&lt;/span&gt;&lt;br /&gt;-&amp;gt; ANY_VALUE는 같은 그룹에서도 어떤 행이 선택될지 보장되지 않아 장애/데이터 불일치로 이어질 수 있기때문에, ROW_NUMBER로 대표 행 선택 기준을 고정하여 결과가 항상 동일해져 테스트가 용이해졌다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li data-end=&quot;1015&quot; data-start=&quot;842&quot;&gt;&lt;b&gt;예약어 컬럼 처리&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;user, level 같은 애들 안전하게 백틱 처리&lt;/span&gt;&lt;br /&gt;-&amp;gt; 환경과 버전에 따라 예약어 해석이 달라지면 배포 후 갑자기 쿼리가 깨질 수 있다고 판단하였다. 초기부터 백틱으로 고정해두어 신규 컬럼 추가 시에도 병목이 없도록 설정해주었다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li data-end=&quot;1247&quot; data-start=&quot;1017&quot;&gt;&lt;b&gt;재귀 쿼리 변환: CONNECT BY &amp;rarr; WITH RECURSIVE&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;DISTINCT 금지 제약 인지 , 임의 depth 제한 X&lt;/span&gt;&lt;br /&gt;-&amp;gt; MySQL 재귀 CTE는 제약이 있어서 임의 depth 제한은 데이터 누락을 만들기 쉬울 수 있다. 따라서 중복 제거가 필요하면 재귀 밖에서 처리해 원본 계층 구조를 그대로 살려주었다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/myru0/dJMcad1R9l9/HlVutGMxxfSfKSQh4pUKSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/myru0/dJMcad1R9l9/HlVutGMxxfSfKSQh4pUKSK/img.png&quot; data-alt=&quot;agent-prompt.md&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/myru0/dJMcad1R9l9/HlVutGMxxfSfKSQh4pUKSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmyru0%2FdJMcad1R9l9%2FHlVutGMxxfSfKSQh4pUKSK%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;600&quot; height=&quot;319&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;526&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;agent-prompt.md&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;CUBRID -&amp;gt; MySQL 8 쿼리 변환 일부&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pNLNt/dJMcabiLdsh/V5LlddvHJ9KtoKM7Jqlcp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pNLNt/dJMcabiLdsh/V5LlddvHJ9KtoKM7Jqlcp0/img.png&quot; data-alt=&quot;GROUP BY 변환 Before: CUBRID&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pNLNt/dJMcabiLdsh/V5LlddvHJ9KtoKM7Jqlcp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpNLNt%2FdJMcabiLdsh%2FV5LlddvHJ9KtoKM7Jqlcp0%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;600&quot; height=&quot;283&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GROUP BY 변환 Before: CUBRID&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DLQds/dJMcadgxSO2/AEWnwGlLvIN7YeL6iSsZm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DLQds/dJMcadgxSO2/AEWnwGlLvIN7YeL6iSsZm1/img.png&quot; data-alt=&quot;GROUP BY 변환 After: MySQL 8&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DLQds/dJMcadgxSO2/AEWnwGlLvIN7YeL6iSsZm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDLQds%2FdJMcadgxSO2%2FAEWnwGlLvIN7YeL6iSsZm1%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;600&quot; height=&quot;362&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;744&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GROUP BY 변환 After: MySQL 8&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;576&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bloDuE/dJMcachErH8/mH1s1PGiUMwhEgWomkgOlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bloDuE/dJMcachErH8/mH1s1PGiUMwhEgWomkgOlk/img.png&quot; data-alt=&quot;Outer Join(+) 변환 Before: CUBRID&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bloDuE/dJMcachErH8/mH1s1PGiUMwhEgWomkgOlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbloDuE%2FdJMcachErH8%2FmH1s1PGiUMwhEgWomkgOlk%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;576&quot; height=&quot;346&quot; data-origin-width=&quot;576&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Outer Join(+) 변환 Before: CUBRID&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2th4M/dJMcahQNjJE/sVnTWYI0jTWw5zeCg2amq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2th4M/dJMcahQNjJE/sVnTWYI0jTWw5zeCg2amq0/img.png&quot; data-alt=&quot;Outer Join(+) 변환 Before: MySQL 8&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2th4M/dJMcahQNjJE/sVnTWYI0jTWw5zeCg2amq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2th4M%2FdJMcahQNjJE%2FsVnTWYI0jTWw5zeCg2amq0%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;600&quot; height=&quot;279&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Outer Join(+) 변환 Before: MySQL 8&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t8lDY/dJMcai3bB1w/orRARvqa3gbYN3EBENCS8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t8lDY/dJMcai3bB1w/orRARvqa3gbYN3EBENCS8K/img.png&quot; data-alt=&quot;재귀 변환 Before: CUBRID&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t8lDY/dJMcai3bB1w/orRARvqa3gbYN3EBENCS8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft8lDY%2FdJMcai3bB1w%2ForRARvqa3gbYN3EBENCS8K%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;600&quot; height=&quot;165&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;298&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;재귀 변환 Before: CUBRID&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1224&quot; data-origin-height=&quot;1042&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zVhUH/dJMcadOj0BF/Is81EMPxhDr3sOkZxFeVj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zVhUH/dJMcadOj0BF/Is81EMPxhDr3sOkZxFeVj1/img.png&quot; data-alt=&quot;재귀 변환 After: MySQL 8&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zVhUH/dJMcadOj0BF/Is81EMPxhDr3sOkZxFeVj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzVhUH%2FdJMcadOj0BF%2FIs81EMPxhDr3sOkZxFeVj1%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;600&quot; height=&quot;511&quot; data-origin-width=&quot;1224&quot; data-origin-height=&quot;1042&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;재귀 변환 After: MySQL 8&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 변환 쿼리들은&amp;nbsp;&lt;b&gt;무조건 실제 DB에서 실행&amp;nbsp;&lt;/b&gt;하여서 오류 발생을 예방하였다. 또한 긴 쿼리 분할은 AI가 한번에 처리하도록 하기보다는, 여러 개의 작은 단위로 나누어 변환을 요청했을 때 더 안정적이고 정확한 결과를 얻을 수 있었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style7&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에&lt;span&gt; AI&lt;/span&gt;가&lt;span&gt; &lt;/span&gt;빠른&lt;span&gt; &lt;/span&gt;속도로&lt;span&gt; &lt;/span&gt;발전하면서&lt;span&gt;, AI를 활용한 문서화나 자동화에 관심이 많이 생기게 되었다. 그중에서도 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Claude 4는 이런 부분에서 선두에 있다고 개인적으로 생각한다. 물론&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;agent-prompt.md 를 반복적으로 커스텀한 이유처럼, AI한테 맡기되 맹신하기 보다는 결정적인 책임은 사람이 무조건 해야한다고 생각하고 있다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아무리&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;agent-prompt.md&lt;span&gt;&lt;span&gt;&lt;span&gt; 를 명확하게 작성하여 지시한다고 하여도, 결과물에 대한 꼼꼼한 검증이 필수적이라고 생각하였다. 이번 글에서는 방대한 양의 쿼리를 처리해야했기에 더더욱&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고한 자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;#&amp;nbsp;MySQL:&amp;nbsp;ONLY_FULL_GROUP_BY&amp;nbsp;/&amp;nbsp;GROUP&amp;nbsp;BY&amp;nbsp;처리&lt;br /&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.3/en/group-by-handling.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/refman/8.3/en/group-by-handling.html&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://dev.mysql.com/doc/en/sql-mode.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/en/sql-mode.html&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;#&amp;nbsp;MySQL:&amp;nbsp;Window&amp;nbsp;Function&amp;nbsp;(ROW_NUMBER&amp;nbsp;등)&lt;br /&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/window-functions-usage.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/refman/8.0/en/window-functions-usage.html&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;#&amp;nbsp;MySQL:&amp;nbsp;WITH&amp;nbsp;/&amp;nbsp;WITH&amp;nbsp;RECURSIVE&amp;nbsp;(CTE)&lt;br /&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/en/with.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/refman/en/with.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI/Copilot</category>
      <category>Claude</category>
      <category>claude 4</category>
      <category>Copilot</category>
      <category>Copilot Agent Mode</category>
      <category>cubrid</category>
      <category>group by</category>
      <category>mysql</category>
      <category>ONLY_FULL_GROUP_BY</category>
      <category>SQL</category>
      <author>huncozyboy</author>
      <guid isPermaLink="true">https://huncozyboy.tistory.com/66</guid>
      <comments>https://huncozyboy.tistory.com/66#entry66comment</comments>
      <pubDate>Fri, 30 Jan 2026 22:15:12 +0900</pubDate>
    </item>
    <item>
      <title>스웨거 typescript-api로 자동화 하는법 (Swagger)</title>
      <link>https://huncozyboy.tistory.com/64</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;409&quot; data-origin-height=&quot;118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CdodA/dJMcadG8uhq/HNdtiG36m1GMKNyIXl6niK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CdodA/dJMcadG8uhq/HNdtiG36m1GMKNyIXl6niK/img.jpg&quot; data-alt=&quot;Swagger 공식문서 이미지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CdodA/dJMcadG8uhq/HNdtiG36m1GMKNyIXl6niK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCdodA%2FdJMcadG8uhq%2FHNdtiG36m1GMKNyIXl6niK%2Fimg.jpg&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;409&quot; height=&quot;118&quot; data-origin-width=&quot;409&quot; data-origin-height=&quot;118&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Swagger 공식문서 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Sopt에서 Lococo를 개발하면서 스웨거가 단순히 문서화 툴일 뿐만 아니라, 클라쪽에서 자동화로 뽑아서 코드를 작성해줄 수 있는 툴이 있음을 알게되었었다. 그래서&lt;span&gt;&amp;nbsp;&lt;/span&gt;NestJS의 공식 문서를 보다가, 흥미로운 내용이 있어서 포스팅을 작성하게 되었다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;@nestjs/swagger&lt;/span&gt;, FastAPI, Springdoc-openapi 같은 것들이 대표적인데, 이런 프레임워크들은 컨트롤러/라우트 정의를 읽어서 OpenAPI 문서를 뽑아주기까지 할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/openapi/introduction&quot;&gt;https://docs.nestjs.com/openapi/introduction&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763588930275&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Documentation | NestJS - A progressive Node.js framework&quot; data-og-description=&quot;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&quot; data-og-host=&quot;docs.nestjs.com&quot; data-og-source-url=&quot;https://docs.nestjs.com/openapi/introduction&quot; data-og-url=&quot;https://docs.nestjs.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/UCGTg/hyZNB6WlrW/eKjIcxfoWwnb5gFkrfNMvk/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/openapi/introduction&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.nestjs.com/openapi/introduction&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/UCGTg/hyZNB6WlrW/eKjIcxfoWwnb5gFkrfNMvk/img.png?width=820&amp;amp;height=429&amp;amp;face=0_0_820_429');&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;Documentation | NestJS - A progressive Node.js framework&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.nestjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style7&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;본문&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIFRcM/dJMcafLLxhM/BRajRhIYE7BKDSqHLChZqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIFRcM/dJMcafLLxhM/BRajRhIYE7BKDSqHLChZqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIFRcM/dJMcafLLxhM/BRajRhIYE7BKDSqHLChZqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIFRcM%2FdJMcafLLxhM%2FBRajRhIYE7BKDSqHLChZqk%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;800&quot; height=&quot;195&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;372&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 위처럼 타입 정의를 뽑아서 쓸 수 있다. &lt;br /&gt;&lt;span&gt;--no-client&lt;/span&gt;: API 클래스는 생성하지 않겠다는 의미이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;--union-enums&lt;/span&gt;: enum을 실제 TypeScript 유니온(&lt;span&gt;&quot;A&quot; | &quot;B&quot;&lt;/span&gt;)으로 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들어진 &lt;span&gt;marketplace.d.ts&lt;/span&gt;는 프로젝트 어디에서나 타입으로만 소비할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이 방식은 &amp;ldquo;&lt;span&gt;&lt;b&gt;타입 동기화&lt;/b&gt;&lt;/span&gt;&amp;rdquo; 측면에서만 도움을 줄 수 있고, 결국 &lt;span&gt;&lt;b&gt;API 호출부는 매번 손으로 짜야 하는 &lt;/b&gt;방식이다&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;swagger-typescript-api 활용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;swagger-typescript-api&lt;/span&gt;가 제공하는 &lt;span&gt;&lt;b&gt;두 가지 커스터마이징 포인트&lt;/b&gt;&lt;/span&gt;를 이용하면, 위 문제를 꽤 깔끔하게 풀 수 있다.&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;템플릿(ejs) 커스터마이징&lt;/li&gt;
&lt;li&gt;&lt;b&gt;훅(hooks) 기반 플러그인 스타일 커스터마이징&lt;/b&gt;&lt;/li&gt;
&lt;/ul&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;b&gt;1단계 &amp;ndash; 기본 템플릿 뽑아서 레포에 복사&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1586&quot; data-origin-height=&quot;1238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/evej7K/dJMcaiIrAkB/6nyQuO44sNTM1ENf2xpgW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/evej7K/dJMcaiIrAkB/6nyQuO44sNTM1ENf2xpgW1/img.png&quot; data-alt=&quot;https://fig.io/manual/swagger-typescript-api/generate-templates&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/evej7K/dJMcaiIrAkB/6nyQuO44sNTM1ENf2xpgW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fevej7K%2FdJMcaiIrAkB%2F6nyQuO44sNTM1ENf2xpgW1%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;600&quot; height=&quot;468&quot; data-origin-width=&quot;1586&quot; data-origin-height=&quot;1238&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://fig.io/manual/swagger-typescript-api/generate-templates&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 공식 CLI가 제공하는 &lt;span&gt;generate-templates&lt;/span&gt; 서브커맨드로 &lt;span&gt;&lt;b&gt;현재 버전의 기본 템플릿&lt;/b&gt;&lt;/span&gt;을 로컬에 복사한다&lt;span&gt;&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1070&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZJJgM/dJMcaihqeQM/RlBfbm8n82A2SVQisIsoyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZJJgM/dJMcaihqeQM/RlBfbm8n82A2SVQisIsoyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZJJgM/dJMcaihqeQM/RlBfbm8n82A2SVQisIsoyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZJJgM%2FdJMcaihqeQM%2FRlBfbm8n82A2SVQisIsoyK%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;800&quot; height=&quot;221&quot; data-origin-width=&quot;1070&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 &lt;span&gt;./tools/swagger-templates&lt;/span&gt; 아래에 대략 이런 파일들이 생성된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;http-client.ejs&lt;/li&gt;
&lt;li&gt;data-contracts.ejs&lt;/li&gt;
&lt;li&gt;api.ejs&lt;/li&gt;
&lt;li&gt;(기타 헬퍼 템플릿들)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2단계&amp;nbsp;&amp;ndash;&amp;nbsp;http-client&amp;nbsp;템플릿에서&amp;nbsp;axios&amp;nbsp;인스턴스&amp;nbsp;주입&amp;nbsp;구조로&amp;nbsp;변경&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;http-client.ejs&lt;/span&gt;를 열어서 대략 아래와 비슷한 형태의 코드를 확인한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rpksa/dJMcafZiLie/TsYFOKZv7K9xJJgDElO9z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rpksa/dJMcafZiLie/TsYFOKZv7K9xJJgDElO9z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rpksa/dJMcafZiLie/TsYFOKZv7K9xJJgDElO9z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frpksa%2FdJMcafZiLie%2FTsYFOKZv7K9xJJgDElO9z1%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;800&quot; height=&quot;482&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;서론에서 얘기했듯이, &amp;ldquo;&lt;/span&gt;&lt;b&gt;이미 만들어 둔 axios 인스턴스를 주입해서 쓰는 형태&lt;/b&gt;&lt;span&gt;&amp;rdquo;로 활용하기 위해서 템플릿을 살짝 바꿔준다&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;1116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baKdEE/dJMcaf54nQB/1PWKnFjHRHenaAGBVHxb61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baKdEE/dJMcaf54nQB/1PWKnFjHRHenaAGBVHxb61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baKdEE/dJMcaf54nQB/1PWKnFjHRHenaAGBVHxb61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaKdEE%2FdJMcaf54nQB%2F1PWKnFjHRHenaAGBVHxb61%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;800&quot; height=&quot;612&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;1116&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3단계&amp;nbsp;&amp;ndash;&amp;nbsp;API&amp;nbsp;템플릿에서&amp;nbsp;메서드&amp;nbsp;이름과&amp;nbsp;반환&amp;nbsp;타입&amp;nbsp;컨벤션&amp;nbsp;정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음으로 &lt;/span&gt;&lt;span&gt;api.ejs&lt;/span&gt;&lt;span&gt; 쪽에서 &lt;/span&gt;&lt;b&gt;메서드 이름과 반환 타입을 팀 컨벤션에 맞게&lt;/b&gt;&lt;span&gt; 정리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어, 기본 템플릿은 &lt;/span&gt;GET /v1/tickets&lt;span&gt; 같은 엔드포인트를 &lt;/span&gt;getTickets&lt;span&gt; 대신 &lt;/span&gt;ticketsControllerControllerListTickets&lt;span&gt; 같은 장문의 이름으로 만들 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;태그(&lt;span&gt;tag&lt;/span&gt;) + HTTP 메서드 + 경로 일부 조합으로 깔끔하게 축약&lt;/li&gt;
&lt;li&gt;&lt;span&gt;operationId&lt;/span&gt;를 우선적으로 사용해서 &lt;span&gt;&lt;b&gt;백엔드에서 이름을 명시적으로 관리&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4단계 &amp;ndash; hooks 로 스키마/타입 수정하여 플러그인처럼 사용&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;1190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brMWsm/dJMcai2KlvI/ziTgvrXrO8HTFKbvEgba81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brMWsm/dJMcai2KlvI/ziTgvrXrO8HTFKbvEgba81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brMWsm/dJMcai2KlvI/ziTgvrXrO8HTFKbvEgba81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrMWsm%2FdJMcai2KlvI%2FziTgvrXrO8HTFKbvEgba81%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;800&quot; height=&quot;624&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;1190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 소스코드처럼, onParseSchema&lt;span&gt;, &lt;/span&gt;onCreateRouteName&lt;span&gt; 등 다양한 훅을 조합하면&amp;nbsp;&lt;/span&gt;enum, 날짜 타입, 파일 업로드 타입 등을 &lt;span&gt;&lt;b&gt;우리 코드베이스에서 쓰는 타입&lt;/b&gt;&lt;/span&gt;으로 일괄 매핑 가능하다. 사실상 &amp;ldquo;젠 체인 앞단에서 동작하는 플러그인&amp;rdquo;처럼 사용이 가능해진다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 전체 플로우를 정리하며 글을 마무리 해보려고 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;서버에서 OpenAPI 스펙 자동 생성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NestJS, Spring, FastAPI 등에서 &lt;span&gt;/v3/api-docs&lt;/span&gt;, &lt;span&gt;/openapi.json&lt;/span&gt; 등으로 노출&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CI에서 swagger-typescript-api 실행&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;템플릿 + hooks를 이용해 타입 + axios 클라이언트 코드 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프론트/공유 라이브러리가 그 코드를 그대로 import&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레포 간에는 git submodule, 패키지 배포 등으로 공유&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 &lt;b&gt;스웨거 스펙을 최신 상태로 유지해주면,&amp;nbsp;&lt;/b&gt;타입 + axios 기반 클라이언트 코드가 함께 갱신되고 프론트/다른 서비스에서는 &lt;span&gt;MarketplaceClient&lt;/span&gt;만 가져다 쓰면 되는 자동화 구조를 만들 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2260&quot; data-origin-height=&quot;1586&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NJbaq/dJMcaiobB0y/AguIystYyi9dxTXDCgEuv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NJbaq/dJMcaiobB0y/AguIystYyi9dxTXDCgEuv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NJbaq/dJMcaiobB0y/AguIystYyi9dxTXDCgEuv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNJbaq%2FdJMcaiobB0y%2FAguIystYyi9dxTXDCgEuv0%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;700&quot; height=&quot;491&quot; data-origin-width=&quot;2260&quot; data-origin-height=&quot;1586&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고 &amp;amp; 공식문서 링크&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OpenAPI 공식 스펙 (3.1 기준)&lt;br /&gt;&lt;a href=&quot;https://swagger.io/specification/&quot;&gt;https://swagger.io/specification/&lt;/a&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;OpenAPI 3.0 Paths &amp;amp; Operations 설명&lt;br /&gt;&lt;a href=&quot;https://swagger.io/docs/specification/v3_0/paths-and-operations/&quot;&gt;https://swagger.io/docs/specification/v3_0/paths-and-operations/&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;swagger-typescript-api GitHub 리포지토리&lt;span&gt;&lt;/span&gt;&lt;br /&gt;&lt;a href=&quot;https://github.com/acacode/swagger-typescript-api&quot;&gt;https://github.com/acacode/swagger-typescript-api&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring/Swagger</category>
      <category>axios</category>
      <category>hooks</category>
      <category>nestjs</category>
      <category>OpenAPI</category>
      <category>Spring</category>
      <category>swagger-typescript-api</category>
      <author>huncozyboy</author>
      <guid isPermaLink="true">https://huncozyboy.tistory.com/64</guid>
      <comments>https://huncozyboy.tistory.com/64#entry64comment</comments>
      <pubDate>Thu, 20 Nov 2025 07:12:34 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot 4.0 RC1 릴리즈 (Spring)</title>
      <link>https://huncozyboy.tistory.com/63</link>
      <description>&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크드인을 살펴보다가 &lt;span&gt;&lt;b&gt;2025년 10월 24일&lt;/b&gt;&lt;/span&gt;에 Spring Boot 버전이 메이저로 올라간 사실을 알게됐다. 정확히는 Spring Boot 4.0.0‑RC1이 릴리즈 되었다는 소식이었고, LTS(장기지원) 버전은 아니지만 메이저 버전 변경이라는 점에서 관심을 가지고 찾아보게 되었다. 이번 포스팅은 버전 4.0에서 주목할 만한 몇 가지 변경사항을 추려보고&amp;nbsp; 앞으로 프로젝트 진행 시 참고할 만한 기능이 있는지, 그리고 Spring Boot가 어떤 방향으로 나아갈거같은지, 공유하고자 작성하게 되었다. &lt;s&gt;(물론&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; RC 버전이라 확정이 아님을 감안해주면 좋을거같다&lt;/span&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xv1i9/dJMcahJswuT/c5tK2jkaEJGRWLKPzrdWW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xv1i9/dJMcahJswuT/c5tK2jkaEJGRWLKPzrdWW1/img.png&quot; data-alt=&quot;Spring Boot 4.0.0‑RC1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xv1i9/dJMcahJswuT/c5tK2jkaEJGRWLKPzrdWW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXv1i9%2FdJMcahJswuT%2Fc5tK2jkaEJGRWLKPzrdWW1%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;432&quot; height=&quot;614&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Spring Boot 4.0.0‑RC1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style7&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;본론&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 10월 23일(미국 기준)에 Spring 공식 블로그에는 &amp;ldquo;Spring Boot 4.0.0‑RC1 available now&amp;rdquo;라는 글이 올라왔고, 해당 버전이 Maven Central에 등록되었다는 안내가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://spring.io/blog/2025/10/23/spring-boot-4-0-0-RC1-available-now?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://spring.io/blog/2025/10/23/spring-boot-4-0-0-RC1-available-now?utm_source=chatgpt.com&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1761730591484&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Spring Boot 4.0.0-RC1 available now&quot; data-og-description=&quot;&quot; data-og-host=&quot;spring.io&quot; data-og-source-url=&quot;https://spring.io/blog/2025/10/23/spring-boot-4-0-0-RC1-available-now?utm_source=chatgpt.com&quot; data-og-url=&quot;https://spring.io/blog/2025/10/23/spring-boot-4-0-0-RC1-available-now&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cvtBUJ/hyZMW3BrNF/rmri1REZG3Ddx3hYjOYJ31/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/AyKEu/hyZMgbxbyp/9mMl2yr2hbCSw18XG72sAk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://spring.io/blog/2025/10/23/spring-boot-4-0-0-RC1-available-now?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://spring.io/blog/2025/10/23/spring-boot-4-0-0-RC1-available-now?utm_source=chatgpt.com&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cvtBUJ/hyZMW3BrNF/rmri1REZG3Ddx3hYjOYJ31/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/AyKEu/hyZMgbxbyp/9mMl2yr2hbCSw18XG72sAk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;Spring Boot 4.0.0-RC1 available now&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;spring.io&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;p data-ke-size=&quot;size16&quot;&gt;이 릴리즈에는 176건의 기능 향상, 문서 개선, 의존성 업그레이드, 버그 수정 등이 포함되어 있다고 밝히면서, &amp;ldquo;현재까지의 관행을 바꾸겠다&amp;rdquo;, 혹은 &amp;ldquo;새로운 표준을 정하겠다&amp;rdquo; 정도의 의지가 담겨 있다고 많은 사람들이 해석하고 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;주요 변경 사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 구체적인 예시 코드들을 보면서 함께, 4.0 버전부터 바뀌는 주요 업데이트 내용을 알아보자&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;RestClientTest 추가&lt;/h4&gt;
&lt;pre id=&quot;code_1761731328483&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.boot.test.autoconfigure.web.client.RestTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.junit.jupiter.api.Test;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerTest {

    @Autowired
    private RestTestClient restTestClient;

    @Test
    void getUser_returnsUser() {
        restTestClient.get()
            .uri(&quot;/api/users&quot;)
            .exchange()
            .expectStatus().isOk()
            .expectBody()
            .jsonPath(&quot;$.id&quot;).isEqualTo(1)
            .jsonPath(&quot;$.name&quot;).isEqualTo(&quot;jihun&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저&amp;nbsp; 기존 버전(3.x 등)에서는 WebTestClient(주로 WebFlux 환경)나 RestTemplate 등을 이용해 컨트롤러/서비스 테스트를 작성하곤 했고, 또 MockMvc 등 MVC 환경용 테스트 도구가 존재했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 4.0 RC1에서는 &lt;span&gt;&lt;b&gt;RestTestClient &lt;/b&gt;&lt;/span&gt;라는 테스트 도구가 새롭게 소개되어, MVC와 WebFlux 양쪽 프레임워크 모두에서 통합 테스트를 보다 수월하게 진행할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 업데이트가 시사하는 바로는, 이제 프레임워크 종류에 따라 테스트 도구를 나누지 않아도 되며, 하나의 테스트 클라이언트 API로 통합 가능하다는 점이 흥미로웠다. &amp;ldquo;컨트롤러와 서비스 사이 통신 흐름을 통합적으로 검증하고 싶을 때&amp;rdquo;, 아니면 &amp;ldquo;MVC &amp;rarr; WebFlux 마이그레이션 중인 프로젝트&amp;rdquo;같은 경우에 특히 유용할 수 있겠다는 개인적인 생각도 들었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;신규 어노테이션 @ObservationKeyValue&lt;/h4&gt;
&lt;pre id=&quot;code_1761731393234&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class OrderCreateService {

    @ObservationKeyValue(key = &quot;order.created&quot;, value = &quot;true&quot;)
    public Order createOrder(Customer customer, Item item) {
        Order order = new Order(customer, item);
        return order;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 내용은 본론을 시작하며 위에서 첨부했던 스프링 공식문서에도 담겨있었던 내용으로, &lt;span&gt;&lt;b&gt;관측성(Observability)&lt;/b&gt;&lt;/span&gt; 에 대한 변화에 집중하겠다는걸 보여준 예시이기도 한거같다. @ObservationKeyValue 어노테이션을 통해 메트릭에 대한 키‑값을 보다 직관적으로 정의할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 리서치 해본 결과, &lt;span style=&quot;color: #ee2323;&quot;&gt;key와 value가 반대로 들어간다든지, 내부 fallback 처리로 메서드 반환값이 value에 들어간다든지 하는 현상&lt;/span&gt;이 있다는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;버그가 있다는 &lt;/span&gt;점은 참고하고, 정식 버전이 아니라는 점도 감안해주어야 할거같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 개인적으로 재밌었던 점은, &lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47); color: oklch(0.3039 0.04 213.68); text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Spring Boot 3.x 버전에서도 메서드 파라미터를 기반으로 메트릭에 동적 태그를 추가하는 기능으로, @Timed&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47); color: oklch(0.3039 0.04 213.68); text-align: start;&quot;&gt;나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;@Counted&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47); color: oklch(0.3039 0.04 213.68); text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;어노테이션과 함께 메서드 파라미터에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;@MeterTag&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47); color: oklch(0.3039 0.04 213.68); text-align: start;&quot;&gt;를 사용하여 동적으로 태그를 추가하는게&lt;/span&gt; 이미 존재했었다는 점이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47); color: oklch(0.3039 0.04 213.68); text-align: start;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47); color: oklch(0.3039 0.04 213.68); text-align: start;&quot;&gt;그럼에도 &lt;/span&gt;@ObservationKeyValue 어노테이션이 나온 이유는, @Observed&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47); color: oklch(0.3039 0.04 213.68); text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;어노테이션과 함께 사용할 수 있어, 메트릭과 트레이싱 모두에 동적 태그를 일관되게 추가할 수 있기때문이 아닐까라는 개인적인 생각인 생각이 들었다. 마찬가지로 &lt;b&gt;&quot;관측성&quot;&lt;/b&gt; 측면에 집중한 업데이트 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761731822316&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Spring Boot 3.x의 복잡한 방식
@Observed(name = &quot;order.created&quot;)
public Order createOrder(String orderId) {
    Observation observation = registry.getCurrentObservation();
    if (observation != null) {
        observation.highCardinalityKeyValue(&quot;orderId&quot;, orderId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47); color: oklch(0.3039 0.04 213.68); text-align: start;&quot;&gt;그리고 첫번째 첨부한 예시 코드랑 비교했을때, 두번째 첨부한 바로 위의 코드가 가독성도 조금 떨어진다고 생각하기에, 개인적으로 좋다고 생각하는 업데이트이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관측성 측면에서는 또 Micrometer나 Micrometer Tracing 등과의 연계, 분산 추적(Distributed Tracing) 기본 내장 등이 강화된 것으로 보이는데, 릴리즈 노트에서는 &amp;ldquo;Redis 관측성 개선(Redis observability improvements)&amp;rdquo; 등의 항목이 나와있어서 추후에 발표되는 내용들을 주의깊게 봐봐야할거같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;직렬화/역직렬화 및 라이브러리 변화: Jackson 3 검토&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 Jackson 버전 3 도입 검토가 진행 중이라고 한다. 현재 Spring Boot는 Jackson 2.x 계열을 사용하고 있고, 이를 Jackson 3로 업그레이드하는 작업이 내부적으로 검토되고 있다고 한다. &amp;ldquo;Record 타입 직렬화/역직렬화&amp;rdquo;을 기대하셨는데, 실제로 Preview 분석 글에서도 Java Record 지원 강화가 언급되어 있다.&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 4.0.0‑M1 릴리즈 노트에서는 의존성 업그레이드 항목으로 Jackson 3 미리보기 버전 가능성이 언급되어 있어서, 해당 부분도 참고해주면 좋을거같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로도 불변 객체(Immutable Object), Record 타입을 많이 쓰시는 경험이 있었어서 이 변화가 반갑기도 했었다. 앞으로 클래스 + Lombok 방식 대신 Record 방식 + 제대로 된 직렬화/역직렬화 지원이 가능해지면 구조 설계 측면에서도 선택지가 넓어질 것 같다는 생각도 들었었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;마치며&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 굳이 불변성을 지닌 객체에 대해서는 Lombok을 적용하는 것을 선호하지는 않았어서, 최대한 Lombok 적용을 고민해 보는편이였다. 그래서 Java Record의 개념은 앞서 얘기했듯이 내가 좋아하고 애용도 많이하지만, ObjectMapper가 직렬화/역직렬화를 제대로 지원하지 않아 Class로 변경하고 Lombok을 사용했던 경험도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 RestTestClient도 RestClient는 도입해봤던 경험이 많지만, 결국 테스트는 WebTestClient나 RestTemplate을 사용하여 진행하였는데 이를 통합한 테스트 객체가 생겨서 너무 좋다고도 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 글에는 설명하지 않았지만 Virtual Threads 동시성 처리 개선,AOT 컴파일 최적화로 시작 속도 향상, 분산 추적 기본 내장와 같은 보완된 내용들이 있으니 참고하면 좋을 것 같다.&lt;/p&gt;</description>
      <category>Spring/CS 개념</category>
      <category>@ObservationKeyValue</category>
      <category>Jackson 3</category>
      <category>RestClientTest</category>
      <category>Spring</category>
      <category>Spring 4.0</category>
      <category>Spring Boot 4.0</category>
      <category>역직렬화</category>
      <category>직렬화</category>
      <author>huncozyboy</author>
      <guid isPermaLink="true">https://huncozyboy.tistory.com/63</guid>
      <comments>https://huncozyboy.tistory.com/63#entry63comment</comments>
      <pubDate>Wed, 29 Oct 2025 18:57:20 +0900</pubDate>
    </item>
    <item>
      <title>Conflict를 깃허브 웹에서 해결시 주의사항 (Github)</title>
      <link>https://huncozyboy.tistory.com/62</link>
      <description>&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 현업에서나 개인, 팀 플젝에서 GitHub를 활용해서 활발하게 PR(Pull Request) 기반 협업을 진행하고있다. 해당 과정에서 작업 브랜치와 동일한 브랜치에서 다른 작업들이 병합이 되면서 충돌(Merge Conflict)이 발생하는 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 IntelliJ&amp;nbsp;IDEA나 vs&amp;nbsp;code를 활용하여 개발 환경 도구를 사용할 수도 있지만, 이러한 충돌을 웹 UI 상에서 간편하게 해결할 수 있다는 점이 GitHub의 장점이라고 필자도 생각하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만 이번 포스팅은 해당 &lt;span style=&quot;color: #ee2323;&quot;&gt;GitHub 웹에서 Conflict를 해결할때의 주의사항&lt;/span&gt;에 대해서 다루어보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 웹 UI를 통한 충돌 해결 방식이 어떻게 동작하는지, 어떤 주의사항이 있는지, 그리고 충돌 해결 시 권장되는 방식에 대해서 다루어보려고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;Github 웹에서 컨플릭트 해결하는 방법 &amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 컨플릭트에 대해 가벼운 설명이나, 컨플릭트를 최소화 하고싶은 방법이 궁금하다면, 아래 포스팅의 &lt;b&gt;개발 전 고려사항&lt;/b&gt; 부분을 봐주어도 좋을거같다. 가치택시 프로젝트를 개발하며 컨플릭트와 관련해서 팀원들과 고민했던 과정이 담겨있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://huncozyboy.tistory.com/7#%EA%B0%9C%EB%B0%9C%20%EC%A0%84%20%EA%B3%A0%EB%A0%A4%EC%82%AC%ED%95%AD-1-4&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://huncozyboy.tistory.com/7#%EA%B0%9C%EB%B0%9C%20%EC%A0%84%20%EA%B3%A0%EB%A0%A4%EC%82%AC%ED%95%AD-1-4&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1761726202878&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;가치택시(매칭 알고리즘, Spring)&quot; data-og-description=&quot;서론방학이 시작되고 Leets 4기에서 프로젝트를 시작하게 되었고, 내가 참여하여 시작한 프로젝트의 주제는 &amp;quot; 교내 학생들의 택시 이용시,&amp;nbsp;요금과 시간의 부담을 줄여줄 수 있는 택시 매칭 서비&quot; data-og-host=&quot;huncozyboy.tistory.com&quot; data-og-source-url=&quot;https://huncozyboy.tistory.com/7#%EA%B0%9C%EB%B0%9C%20%EC%A0%84%20%EA%B3%A0%EB%A0%A4%EC%82%AC%ED%95%AD-1-4&quot; data-og-url=&quot;https://huncozyboy.tistory.com/7&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cfASFS/hyZMOdpqtm/SpHj941rKqR5PVCPPiKhMk/img.png?width=800&amp;amp;height=1084&amp;amp;face=0_0_800_1084,https://scrap.kakaocdn.net/dn/jr5HQ/hyZMpzxJCK/g9NH38UXOZGH8HNeYKYAN1/img.png?width=800&amp;amp;height=1084&amp;amp;face=0_0_800_1084,https://scrap.kakaocdn.net/dn/uiJc3/hyZMXnSfZn/btVSXbVqc0HsVEK4NJ7VZ1/img.png?width=1056&amp;amp;height=1432&amp;amp;face=0_0_1056_1432&quot;&gt;&lt;a href=&quot;https://huncozyboy.tistory.com/7#%EA%B0%9C%EB%B0%9C%20%EC%A0%84%20%EA%B3%A0%EB%A0%A4%EC%82%AC%ED%95%AD-1-4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://huncozyboy.tistory.com/7#%EA%B0%9C%EB%B0%9C%20%EC%A0%84%20%EA%B3%A0%EB%A0%A4%EC%82%AC%ED%95%AD-1-4&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cfASFS/hyZMOdpqtm/SpHj941rKqR5PVCPPiKhMk/img.png?width=800&amp;amp;height=1084&amp;amp;face=0_0_800_1084,https://scrap.kakaocdn.net/dn/jr5HQ/hyZMpzxJCK/g9NH38UXOZGH8HNeYKYAN1/img.png?width=800&amp;amp;height=1084&amp;amp;face=0_0_800_1084,https://scrap.kakaocdn.net/dn/uiJc3/hyZMXnSfZn/btVSXbVqc0HsVEK4NJ7VZ1/img.png?width=1056&amp;amp;height=1432&amp;amp;face=0_0_1056_1432');&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;가치택시(매칭 알고리즘, Spring)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론방학이 시작되고 Leets 4기에서 프로젝트를 시작하게 되었고, 내가 참여하여 시작한 프로젝트의 주제는 &quot; 교내 학생들의 택시 이용시,&amp;nbsp;요금과 시간의 부담을 줄여줄 수 있는 택시 매칭 서비&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;huncozyboy.tistory.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;p data-ke-size=&quot;size16&quot;&gt;다시 돌아와서, 구체적인 컨플릭트 과정부터 살펴보자&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;공식 GitHub Webhook 설명서&lt;br /&gt;&quot; 동일한 브랜치를 기준으로 다른 브랜치에서 PR을 올렸을 때 같은 파일의 동일한 줄(line) 을 두 브랜치가 수정한 경우에 한해 웹 UI에서 충돌을 해결할 수 있는 옵션이 나옵니다&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.github.com/articles/about-merge-conflicts?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.github.com/articles/about-merge-conflicts?utm_source=chatgpt.com&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1761726624948&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;병합 충돌 정보 - GitHub Docs&quot; data-og-description=&quot;병합 충돌은 경합 커밋이 있는 분기를 병합할 때 발생하며 Git가 최종 병합에 통합할 변경 내용을 결정하는 데 사용자의 도움이 필요합니다.&quot; data-og-host=&quot;docs.github.com&quot; data-og-source-url=&quot;https://docs.github.com/articles/about-merge-conflicts?utm_source=chatgpt.com&quot; data-og-url=&quot;https://docs-internal.github.com/ko/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/about-merge-conflicts?utm_source=chatgpt.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/baMeDZ/hyZMVwPpHb/BUEWFJB7dCzvEaVEz2WVjk/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/bE93gA/hyZMitEaKg/aWPVK0HVCPsNgHynnmnHV0/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628&quot;&gt;&lt;a href=&quot;https://docs.github.com/articles/about-merge-conflicts?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.github.com/articles/about-merge-conflicts?utm_source=chatgpt.com&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/baMeDZ/hyZMVwPpHb/BUEWFJB7dCzvEaVEz2WVjk/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/bE93gA/hyZMitEaKg/aWPVK0HVCPsNgHynnmnHV0/img.png?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628');&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;병합 충돌 정보 - GitHub Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;병합 충돌은 경합 커밋이 있는 분기를 병합할 때 발생하며 Git가 최종 병합에 통합할 변경 내용을 결정하는 데 사용자의 도움이 필요합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.github.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;p data-ke-size=&quot;size16&quot;&gt;위 공식문서 내용들을 보면 알 수 있듯이, 서론에서도 설명했듯이 간단하게 컨플릭트를 해결할 수 있다. 좀 더 구체적으론 아래처럼 할 수 있을거같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uf0WP/dJMcadAh4KX/MNWTwknUD82kGwD9oNyOHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uf0WP/dJMcadAh4KX/MNWTwknUD82kGwD9oNyOHK/img.png&quot; data-alt=&quot;README.md 라는 간단한 컨플릭트 예시 (아마 예시와 달리 여러개의 파일이 동시에 컨플릭트가 일어나는 현상이 대부분일거다)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uf0WP/dJMcadAh4KX/MNWTwknUD82kGwD9oNyOHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuf0WP%2FdJMcadAh4KX%2FMNWTwknUD82kGwD9oNyOHK%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;844&quot; height=&quot;182&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;README.md 라는 간단한 컨플릭트 예시 (아마 예시와 달리 여러개의 파일이 동시에 컨플릭트가 일어나는 현상이 대부분일거다)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;PR 페이지에서 &amp;ldquo;This pull request has conflicts that must be resolved&amp;rdquo; 등의 안내가 뜹니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Resolve conflicts&amp;rdquo; 버튼을 클릭하면 충돌이 난 파일별로 편집 화면이 나옵니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&amp;lt; &amp;lt; &amp;lt; &amp;lt; &amp;lt; HEAD, =======, &amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; branch-name 등의 충돌 마커(conflict markers)가 보이고, 원하는 코드만 남기고 마커를 삭제합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;모든 파일 충돌을 해결하고 나면 &amp;ldquo;Mark as resolved&amp;rdquo; &amp;rarr; &amp;ldquo;Commit merge&amp;rdquo; 등의 버튼이 활성화됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가장 많이 사용하는 깃허브 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 귀찮거나 실수가 나올 수도 있는 컨플릭트 상황을 조금이라도 줄이거나, 충돌이 발생했을 때 효율적으로 대응하기 위해서는 브랜치 전략이 굉장히 중요하다고 생각하는데, 필자가 가장 많이 사용하는 깃허브 전략은 github flow이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMNusH/dJMcaelFdO5/c575VSkdLwK83PtOvV0zKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMNusH/dJMcaelFdO5/c575VSkdLwK83PtOvV0zKk/img.png&quot; data-alt=&quot;GitHub Flow 전략에 대한 간단한 설명&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMNusH/dJMcaelFdO5/c575VSkdLwK83PtOvV0zKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMNusH%2FdJMcaelFdO5%2Fc575VSkdLwK83PtOvV0zKk%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;600&quot; height=&quot;305&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GitHub Flow 전략에 대한 간단한 설명&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 설명했을때, 아래 플로우로 진행이 된다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;기능 별로 브랜치 생성 feat #1 회원가입 기능 구현&lt;/li&gt;
&lt;li&gt;브랜치로 이동 git checkout -b&lt;/li&gt;
&lt;li&gt;작업 (새로 생성한 브랜치에서) git add, git commit, git push,&lt;/li&gt;
&lt;li&gt;Pull Request (PR) 생성 간단하게 서로 리뷰&lt;/li&gt;
&lt;li&gt;메인에다가 머지 리뷰가 끝나면 머지&lt;/li&gt;
&lt;li&gt;다시 Main 브랜치로 이동 git pull origin main&lt;/li&gt;
&lt;li&gt;또 새로운 작업 들어가면 1번 반복&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Flow는 비교적 간단하고 직관적인 구조를 가지고 있어서 협업이나 리뷰가 자주 이뤄지는 사이드 프로젝트에서 특히 유용하다고 느꼈다 (개인적인 생각)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style7&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;본론&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서론이 좀 길었는데, 그러면 이 포스팅의 제목인 깃허브 웹 UI를 활용해서 컨플릭트를 해결할시, 어떤 점을 주의해야할까 ?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;base 브랜치 전체가 병합될 수 있다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub 웹 UI에서 Merge Conflict를 해결하면, 단순히 충돌된 파일만 병합되는 것이 아니라 PR의 base 브랜치 전체가 head(compare) 브랜치에 병합된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbZdD0/dJMcaaKkJWc/tMdk15K1HKYszbk9V27igK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbZdD0/dJMcaaKkJWc/tMdk15K1HKYszbk9V27igK/img.png&quot; data-alt=&quot;main과 feature/login 브랜치의 컨플릭트 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbZdD0/dJMcaaKkJWc/tMdk15K1HKYszbk9V27igK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbZdD0%2FdJMcaaKkJWc%2FtMdk15K1HKYszbk9V27igK%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;805&quot; height=&quot;207&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;207&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;main과 feature/login 브랜치의 컨플릭트 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, main 브랜치를 기준으로 feature/login 브랜치에서 PR을 만들었고 충돌이 생긴 경우, 웹에서 충돌을 해결하면 main 전체가 feature/login에 병합되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 우리가 웹 UI를 사용하지 않고 직접 충돌을 해결할 때를 생각하면 이해가 쉬운데, 기본적으로 웹 UI에서는 head(compare) 브랜치가 base 브랜치의 최신 변경사항을 반영한 뒤 병합되는 방식으로, UI상에서는 &amp;ldquo;버튼 클릭&amp;rdquo;으로 간편해 보이기 때문에 이 과정이 눈에 잘 띄지 않을 수 있다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통은 흔하게 아래 두가지 두가지 방법을 사용한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. PR을 올린 반대 방향으로 merge하는 케이스&lt;br /&gt;base 브랜치의 변경 사항이 head 브랜치에 반영됩니다. 웹 UI의 동작과 동일합니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;2. 작업 브랜치를 rebase하는 케이스&lt;br /&gt;마찬가지로 base 브랜치의 변경 사항이 head 브랜치에 반영됩니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때는 우리가 직접 merge나 rebase를 하면서 base 브랜치의 최신 내용을 반영한다는 사실을 자연스레 인지하는데, 웹 UI에서는 버튼 몇 번으로 간단하게 해결할 수 있다보니 이를 쉽게 간과할 수 있는 것 같다고 개인적으로 생각했다. 그리고 결론적으론 다음과 같은 상황에서는 더 주의해주어야한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;base 브랜치의 최신 내용을 반영할 시에 주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 필자는 아래 3가지 case를 생각해보았다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;head 브랜치가 배포 대상인 브랜치일 때&lt;/li&gt;
&lt;li&gt;충돌 해결 후 커밋 내용이 그대로 유지되어야 하는 경우&lt;/li&gt;
&lt;li&gt;외부 브랜치나 포크 저장소에서 작업 중일 때&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 3가지의 케이스에서는 단순하게 GitHub 웹 UI에서 Merge Conflict 해결 방식을 사용할때 주의해야된다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 case, &lt;span style=&quot;color: #ee2323;&quot;&gt;배포 브랜치에 직접적인 변경 이력이 남게 되는 위험&lt;/span&gt;이 존재한다.&lt;br /&gt;웹 UI에서 병합을 진행하면 Merge commit이 자동 생성되는데, 이는 CI/CD 파이프라인이 트리거되어 의도치 않게 배포가 재실행될 수도 있다. 또한 병합 과정에서 테스트나 빌드 검증이 충분히 이루어지지 않은 상태로 main이나 release 브랜치에 반영될 수 있다는 점에서도 주의가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;857&quot; data-start=&quot;599&quot; data-ke-size=&quot;size16&quot;&gt;3번 case, &lt;span style=&quot;color: #ee2323;&quot;&gt;포크 저장소나 외부 협업 브랜치에서 권한 문제&lt;/span&gt;가 발생할 수 있다.&lt;br /&gt;GitHub 웹 UI에서 병합을 시도하면, 저장소 권한이 제한된 경우 자동 병합이 불가능하거나, 원격 브랜치에 대한 push 권한 부족으로 인해 로컬 변경 사항이 반영되지 않는 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;857&quot; data-start=&quot;599&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1134&quot; data-start=&quot;859&quot; data-ke-size=&quot;size16&quot;&gt;2번 case, 웹 UI 방식에서는 &lt;span style=&quot;color: #ee2323;&quot;&gt;자동으로 병합 커밋이 생성되거나, 커밋 메시지가 시스템 기본값으로 작성&lt;/span&gt;되는 경우가 많다.&lt;br /&gt;즉, 히스토리를 깨끗하게 관리해야 하는 프로젝트(예: git log를 통해 변경 흐름을 명확히 추적하거나, git bisect로 디버깅 가능한 상태를 유지해야 하는 경우)라면 웹 UI 방식을 사용하기보다는 로컬에서 명시적으로 커밋 메시지를 작성하고, 필요에 따라 squash 또는 rebase를 통해 이력을 정돈하는 방식을 추천한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;권장하는 충돌 해결 방식 및 브랜치 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub 웹에서 충돌을 해결하기 전, 다음과 같은 점들을 고려하는 것을 권장합니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;1. 중요한 브랜치는 보호된 브랜치로 설정하기&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;191&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/edp2LS/dJMcahCGUVC/1JbK4rEvBC1wftEV4Wo5C1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/edp2LS/dJMcahCGUVC/1JbK4rEvBC1wftEV4Wo5C1/img.png&quot; data-alt=&quot;Github 자체에서도 많은 기능을 제공해주기에, 직접 들어가서 보는걸 추천한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/edp2LS/dJMcahCGUVC/1JbK4rEvBC1wftEV4Wo5C1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fedp2LS%2FdJMcahCGUVC%2F1JbK4rEvBC1wftEV4Wo5C1%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;1101&quot; height=&quot;191&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;191&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Github 자체에서도 많은 기능을 제공해주기에, 직접 들어가서 보는걸 추천한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;main&lt;/span&gt;, &lt;span&gt;release/*&lt;/span&gt; 등 주요 브랜치에 대해 보호 규칙(Protected Branches)을 걸어두는 것이 좋다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직접 push 금지&lt;/li&gt;
&lt;li&gt;force‑push 금지&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;span style=&quot;color: #ee2323;&quot;&gt;2. &amp;ldquo;이 브랜치에 base 브랜치를 통째로 병합해도 괜찮은가?&amp;rdquo; 사전 검토&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;head 브랜치에 base 브랜치 전체를 반영해도 문제가 없는가?&lt;br /&gt;&amp;rarr; 만약 head 브랜치가 배포 대상 브랜치이거나, 커밋 히스토리를 깔끔히 유지해야 하는 경우라면 앞서 짧게 설명했듯이 별도로 브랜치를 파서 충돌을 해결하는 편이 안전.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FL1qK/dJMcahvVh4s/iYiOU9J5PwZRR0F4s0diS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FL1qK/dJMcahvVh4s/iYiOU9J5PwZRR0F4s0diS0/img.png&quot; data-alt=&quot;main-merge 라는 별도의 브랜치를 생성해서 안전하게 머지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FL1qK/dJMcahvVh4s/iYiOU9J5PwZRR0F4s0diS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFL1qK%2FdJMcahvVh4s%2FiYiOU9J5PwZRR0F4s0diS0%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;484&quot; height=&quot;225&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;main-merge 라는 별도의 브랜치를 생성해서 안전하게 머지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;커밋 히스토리가 깔끔하게 유지되어야 하는가?&lt;br /&gt;&amp;rarr; 그렇다면 로컬에서 rebase 혹은 merge --no‑ff 방식으로 해결하는 것이 바람직함.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;마치며&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;간단히 정리하자면, GitHub 웹 UI를 통한 충돌 해결 기능은 &lt;/span&gt;&lt;b&gt;편리하지만, 그 내부에서 어떤 동작이 일어나는지&lt;/b&gt;&lt;span&gt;, 그리고 &lt;/span&gt;&lt;b&gt;너무 쉽게 누르다 보면 의도하지 않은 병합이 발생할 수 있다는 점&lt;/b&gt;&lt;span&gt;을 반드시 인지하고 사용하면 더 좋을거같다는 생각에 포스팅을 작성하게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;1584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsCqph/dJMcaa4Dw9h/LVXH325k7tCQkYQDt0ZGf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsCqph/dJMcaa4Dw9h/LVXH325k7tCQkYQDt0ZGf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsCqph/dJMcaa4Dw9h/LVXH325k7tCQkYQDt0ZGf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsCqph%2FdJMcaa4Dw9h%2FLVXH325k7tCQkYQDt0ZGf0%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;700&quot; height=&quot;642&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;1584&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고한 링크&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Resolving a merge conflict on GitHub &amp;ndash; GitHub Docs: &lt;a href=&quot;https://docs.github.com/articles/about-merge-conflicts&quot;&gt;https://docs.github.com/articles/about-merge-conflicts&lt;/a&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Resolving a merge conflict using the command line &amp;ndash; GitHub Docs: &lt;a href=&quot;https://docs.github.com/articles/resolving-a-merge-conflict-using-the-command-line&quot;&gt;https://docs.github.com/articles/resolving-a-merge-conflict-using-the-command-line&lt;/a&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;How GitHub merge conflict: How to handle the most common merge conflicts &amp;ndash; CloudBees Blog&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Github/Conflict</category>
      <category>base</category>
      <category>Branch</category>
      <category>conflict</category>
      <category>Github</category>
      <category>github ui</category>
      <category>merge conflict</category>
      <category>병합</category>
      <category>충돌</category>
      <category>충돌 해결</category>
      <category>컨플릭트</category>
      <author>huncozyboy</author>
      <guid isPermaLink="true">https://huncozyboy.tistory.com/62</guid>
      <comments>https://huncozyboy.tistory.com/62#entry62comment</comments>
      <pubDate>Wed, 29 Oct 2025 17:55:15 +0900</pubDate>
    </item>
    <item>
      <title>Instagram Basic Display API vs Graph API 무슨 차이가 있을까 (OAuth)</title>
      <link>https://huncozyboy.tistory.com/61</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/02Wf5/btsQJA3xQ9m/owKfvQe18WbDdX8zdRsL50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/02Wf5/btsQJA3xQ9m/owKfvQe18WbDdX8zdRsL50/img.png&quot; data-alt=&quot;instagram, tiktok 계정연동 기능&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/02Wf5/btsQJA3xQ9m/owKfvQe18WbDdX8zdRsL50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F02Wf5%2FbtsQJA3xQ9m%2FowKfvQe18WbDdX8zdRsL50%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;600&quot; height=&quot;428&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;826&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;instagram, tiktok 계정연동 기능&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글로벌 크리에이터, 브랜드를 연결해주는 서비스 Lococo를 개발하면서, 크리에이터는 브랜드가 올린 &quot;캠페인&quot;이라는 것에 신청하고, 해당 제품에 대한 후기를 SNS에 업로드하는 로직이 있었다. 때문에 업로드한 SNS에 대한 정보를 가져오기 위해서는 SNS 연동이 필수적이였는데, 해당 포스팅은 Instagram 연동을 하면서 고민했었던 내용을 작성해보려한다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본문&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLcmH1/btsQI7tDtDS/GFOL8uNL93jwTMUjqijKwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLcmH1/btsQI7tDtDS/GFOL8uNL93jwTMUjqijKwK/img.png&quot; data-alt=&quot;meta에 있는 공식문서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLcmH1/btsQI7tDtDS/GFOL8uNL93jwTMUjqijKwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLcmH1%2FbtsQI7tDtDS%2FGFOL8uNL93jwTMUjqijKwK%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;700&quot; height=&quot;529&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1548&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;meta에 있는 공식문서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스타그램 연동을 위해 공식 문서를 봐도 공식문서를 보면, Facebook &lt;span&gt;로그인을&lt;/span&gt; &lt;span&gt;통한&lt;/span&gt; Instagram API&lt;span data-token-index=&quot;4&quot;&gt;랑 &lt;/span&gt;Instagram &lt;span&gt;로그인을&lt;/span&gt; &lt;span&gt;통한&lt;/span&gt; Instagram API&lt;span data-token-index=&quot;4&quot;&gt;&lt;/span&gt;&lt;span data-token-index=&quot;6&quot;&gt;가 별도로 존재하는걸 알 수 있었다. &lt;/span&gt;이 글에서는 내가 겪었던 혼란 포인트들을 공유하면서, 상황에 따라서 어떤 플로우를 선택해야 하는지와 OAuth 차이를 정리해보려고한다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그인 화면의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메타(페이스북)가 현재 유지하는 인스타그램 Oauth는 크게 두가지이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;b&gt;Instagram API with Instagram Login&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2928&quot; data-origin-height=&quot;1822&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VWB7H/btsQJbQlkz6/twKrSGKECUk12K2HQDFSp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VWB7H/btsQJbQlkz6/twKrSGKECUk12K2HQDFSp1/img.png&quot; data-alt=&quot;실제 Insta Oauth 경로로 접속시 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VWB7H/btsQJbQlkz6/twKrSGKECUk12K2HQDFSp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVWB7H%2FbtsQJbQlkz6%2FtwKrSGKECUk12K2HQDFSp1%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;750&quot; height=&quot;467&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2928&quot; data-origin-height=&quot;1822&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 Insta Oauth 경로로 접속시 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 인스타그램으로 로그인은, 우리가 평소에 알던 인스타그램 화면이 뜨며 선택적으로 Facebook으로 로그인할 수 있는 플로우라고 이해해주면 된다. 만약 로그인한 인스타 계정이 &lt;b&gt;프로페셔널(비즈니스/크리에이터)&lt;/b&gt;&lt;span&gt;&lt;span&gt; 계정이 아니라면, 계정 전환을 해달라는 별도의 페이지가 나온다&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Instagram API with Facebook Login&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2928&quot; data-origin-height=&quot;1828&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PFOWY/btsQHr7CKMK/Hs66UfEF3ERukujgLj8aF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PFOWY/btsQHr7CKMK/Hs66UfEF3ERukujgLj8aF1/img.png&quot; data-alt=&quot;실제 Facebook Oauth 경로로 접속시 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PFOWY/btsQHr7CKMK/Hs66UfEF3ERukujgLj8aF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPFOWY%2FbtsQHr7CKMK%2FHs66UfEF3ERukujgLj8aF1%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;750&quot; height=&quot;468&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2928&quot; data-origin-height=&quot;1828&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 Facebook Oauth 경로로 접속시 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 마찬가지로 Facebook Oauth는 역시나 우리가 알던 Facebook으로 로그인할 수 있는 플로우다. 하지만, 위 화면에서도 알 수 있듯이 Insta로 로그인할 수 없다고 생각해주면 될거같다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본세팅 (Meta 콘솔 설정의 차이)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 서두에 짧게 얘기했듯이, 페이스북과 인스타그램은 모두 메타가 제공해주는 Oauth이다. 때문에 다른 Oauth와 마찬가지로 기능을 구현하기 위해서 Meta Developers에서 별도의 콘솔 설정을 해준뒤, clientID, redirectURI 등등을 세팅해줘야한다고 생각해주면 된다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2906&quot; data-origin-height=&quot;1658&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bW0Y9u/btsQG0Jm6Yd/cHUy4whTwrV9GwqIj6VXc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bW0Y9u/btsQG0Jm6Yd/cHUy4whTwrV9GwqIj6VXc1/img.png&quot; data-alt=&quot;Meta for Developers 사이트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bW0Y9u/btsQG0Jm6Yd/cHUy4whTwrV9GwqIj6VXc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbW0Y9u%2FbtsQG0Jm6Yd%2FcHUy4whTwrV9GwqIj6VXc1%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;600&quot; height=&quot;342&quot; data-origin-width=&quot;2906&quot; data-origin-height=&quot;1658&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Meta for Developers 사이트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;1.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Instagram API with Instagram Login&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 인스타 로그인 콘솔 세팅이다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5oCGJ/btsQI1fW3ah/6sno5uqhvdA4DOQk6JcsEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5oCGJ/btsQI1fW3ah/6sno5uqhvdA4DOQk6JcsEK/img.png&quot; data-alt=&quot;내앱 - 대시보드 - 제품추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5oCGJ/btsQI1fW3ah/6sno5uqhvdA4DOQk6JcsEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5oCGJ%2FbtsQI1fW3ah%2F6sno5uqhvdA4DOQk6JcsEK%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;600&quot; height=&quot;464&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;내앱 - 대시보드 - 제품추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 설명했듯이 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;clientID, redirectURI 등등의 세팅을 위해서 제품 추가를 해주어야하는데, 해당 &lt;span style=&quot;background-color: #ffffff; color: #1d2129; text-align: center;&quot;&gt;Instagram이라고 되어있는 제품을 추가해준 뒤에&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1898&quot; data-origin-height=&quot;1100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEuhSB/btsQIRYWaV5/ok4fPkYH1PUUHxzcgE386K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEuhSB/btsQIRYWaV5/ok4fPkYH1PUUHxzcgE386K/img.png&quot; data-alt=&quot;Instagram - Instagram 비즈니스 로그인이 포함된 API 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEuhSB/btsQIRYWaV5/ok4fPkYH1PUUHxzcgE386K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEuhSB%2FbtsQIRYWaV5%2Fok4fPkYH1PUUHxzcgE386K%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;650&quot; height=&quot;377&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1898&quot; data-origin-height=&quot;1100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Instagram - Instagram 비즈니스 로그인이 포함된 API 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: center;&quot;&gt;Instagram 비즈니스 로그인이 포함된 API 설정에서 기본 세팅을 진행하면 된다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;2. &lt;/span&gt;&lt;/span&gt;&lt;b&gt;Instagram API with Facebook Login&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 다른 Oauth인 Facebook login 콘솔 세팅이다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vXsQO/btsQI8e2Anc/ZYHraiK5IHgHRLwbQ9KVk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vXsQO/btsQI8e2Anc/ZYHraiK5IHgHRLwbQ9KVk1/img.png&quot; data-alt=&quot;내앱 - 대시보드 - 제품추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vXsQO/btsQI8e2Anc/ZYHraiK5IHgHRLwbQ9KVk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvXsQO%2FbtsQI8e2Anc%2FZYHraiK5IHgHRLwbQ9KVk1%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;600&quot; height=&quot;494&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;530&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;내앱 - 대시보드 - 제품추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;바로 이전에 나온 &lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1d2129; text-align: center;&quot;&gt;Instagram 콘솔과 똑같은 경로에서, &lt;span style=&quot;background-color: #ffffff; color: #1d2129; text-align: center;&quot;&gt;비즈니스용 Facebook 로그인이라고 되어있는 제품을 추가한 뒤에&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;944&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8Woec/btsQHQMSdfu/4BAWHZ8O5VbBcmd4qNrCw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8Woec/btsQHQMSdfu/4BAWHZ8O5VbBcmd4qNrCw0/img.png&quot; data-alt=&quot;비즈니스용 Facebook 로그인 - 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8Woec/btsQHQMSdfu/4BAWHZ8O5VbBcmd4qNrCw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8Woec%2FbtsQHQMSdfu%2F4BAWHZ8O5VbBcmd4qNrCw0%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;650&quot; height=&quot;321&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;944&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;비즈니스용 Facebook 로그인 - 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: center;&quot;&gt;비즈니스용 Facebook 로그인 - 설정으로 들어가서 위 캡쳐화면에 나온 내용들을 설정할 수 있다 &lt;span style=&quot;color: #ee2323;&quot;&gt;(Facebook 로그인의 clientID는 제품 추가시 별도로 설정되는 것이 아닌, 제일 상단에 있는 Meta 앱 ID와 동일하다)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;얻을 수 있는 정보의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 내용은 그렇다면 왜 나누어져 있는걸까 ? 먼저 Oauth API가 제공하는 정보의 차이가 존재한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) Instagram Basic Display API (개인 계정 전용&amp;middot;현재 중단)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;엔드포인트&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;a href=&quot;https://graph.instagram.com&quot;&gt;https://graph.instagram.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제공 데이터&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id&lt;span&gt;, &lt;/span&gt;username&lt;/li&gt;
&lt;li&gt;media&lt;span&gt; (사진/영상 URL, &lt;/span&gt;caption&lt;span&gt;, &lt;/span&gt;timestamp&lt;span&gt; 등)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;제공하지 않음&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;like_count&lt;span&gt;, &lt;/span&gt;comments_count&lt;span&gt;, 노출/도달/저장/조회수 같은 &lt;/span&gt;&lt;span&gt;&lt;b&gt;인사이트&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) Instagram Graph API (Business/Creator 전용)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;엔드포인트&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;a href=&quot;https://graph.facebook.com&quot;&gt;https://graph.facebook.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제공 데이터/기능&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;인사이트&lt;/b&gt;&lt;/span&gt;(impressions, reach, engagement, saved, plays 등)&lt;/li&gt;
&lt;li&gt;게시물&amp;middot;릴스&amp;middot;스토리 &lt;span&gt;&lt;b&gt;통계&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결론적으로 우리 Lococo에서 필요한 Oauth API는 2번이였고, 해당 내용을 사용하기 위해선 사업자 등록이 필요했다  &lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;사업자 등록 플로우&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;앞서 설명한 &quot; 사업자 &quot; 라는 개념은, 우리 서비스에서 필요했던 정보인 게시글, 릴스 등의&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;인사이트(통계)&amp;nbsp;&lt;/b&gt;를 얻기위한 정보들이였는데, &lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;비즈니스 검증(Business Verification)&lt;/span&gt;과 &lt;span style=&quot;color: #ee2323;&quot;&gt;앱 검수(App Review)&lt;/span&gt;를 통과해야 실제 사용자에게 &lt;span&gt;&lt;b&gt;Advanced Access&lt;/b&gt;&lt;/span&gt;로 관련한 권한들을 제공받을 수가 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 관련한 공식문서의 내용이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.facebook.com/docs/instagram-platform/app-review&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.facebook.com/docs/instagram-platform/app-review&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1758489681369&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;App Review - Instagram 플랫폼 - 문서 - Meta for Developers&quot; data-og-description=&quot;If you request permissions or features that your app does not use or does not align with the allowed usage for that permission or feature, your submission will not be approved.&quot; data-og-host=&quot;developers.facebook.com&quot; data-og-source-url=&quot;https://developers.facebook.com/docs/instagram-platform/app-review&quot; data-og-url=&quot;https://developers.facebook.com/docs/instagram-platform/app-review&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developers.facebook.com/docs/instagram-platform/app-review&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.facebook.com/docs/instagram-platform/app-review&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;App Review - Instagram 플랫폼 - 문서 - Meta for Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;If you request permissions or features that your app does not use or does not align with the allowed usage for that permission or feature, your submission will not be approved.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.facebook.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;602&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rjmLB/btsQGFrZHq0/uS2WsmJJhYNojhUtxKeisK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rjmLB/btsQGFrZHq0/uS2WsmJJhYNojhUtxKeisK/img.png&quot; data-alt=&quot;실제 테스트했었던 API들....&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rjmLB/btsQGFrZHq0/uS2WsmJJhYNojhUtxKeisK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrjmLB%2FbtsQGFrZHq0%2FuS2WsmJJhYNojhUtxKeisK%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;602&quot; height=&quot;686&quot; data-origin-width=&quot;602&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 테스트했었던 API들....&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 &amp;amp; 공식문서 링크&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Basic Display API 종료 공지&lt;/b&gt;&lt;span&gt;(Meta 블로그)&lt;br /&gt;&lt;/span&gt;&lt;a href=&quot;https://developers.facebook.com/blog/post/2024/09/04/update-on-instagram-basic-display-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.facebook.com/blog/post/2024/09/04/update-on-instagram-basic-display-api&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Instagram API with &lt;span&gt;&lt;b&gt;Instagram Login&lt;/b&gt;&lt;/span&gt; (동일 Graph 기반, 로그인 프로바이더만 변경)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Instagram API with &lt;span&gt;&lt;b&gt;Facebook Login&lt;/b&gt;&lt;/span&gt; (Get Started, 권한 흐름)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.facebook.com/docs/instagram-platform/instagram-api-with-facebook-login&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.facebook.com/docs/instagram-platform/instagram-api-with-facebook-login&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring/OAuth</category>
      <category>facebook</category>
      <category>instagram</category>
      <category>Instagram Basic Display API</category>
      <category>Instagram Graph API</category>
      <category>Instagram 로그인</category>
      <category>instagram 연동</category>
      <category>meta</category>
      <category>OAuth</category>
      <category>Spring</category>
      <author>huncozyboy</author>
      <guid isPermaLink="true">https://huncozyboy.tistory.com/61</guid>
      <comments>https://huncozyboy.tistory.com/61#entry61comment</comments>
      <pubDate>Mon, 22 Sep 2025 06:27:47 +0900</pubDate>
    </item>
    <item>
      <title>배포 후 구매한 도메인 연결하기 (Route 53)</title>
      <link>https://huncozyboy.tistory.com/60</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2로 서버를 배포한 뒤에는 &lt;a href=&quot;https://52.79.208.129.nip.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://***.nip.io/&lt;/a&gt;&amp;nbsp; 라는 무료 도메인인 nip.io를 붙여주면 정상적으로 스웨거가 접속이 되며 테스트가 가능할 순 있다. 하지만 실제 유저를 받기 위해서 고유한 도메인을 구매한 뒤에 라우팅을 시켜줘야하는데, 이번 포스팅은 Route 53을 활용해서 가비아에서 구매한 도메인을 EC2 서버와 호스팅하는 플로우를 다루려고한다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nFFDY/btsQEo4eqfr/ReMxv37abkNVDwrowr4oVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nFFDY/btsQEo4eqfr/ReMxv37abkNVDwrowr4oVk/img.png&quot; data-alt=&quot;실제 배포되어 운영중인 동아리 관리 서비스 Weeth 도메인 주소&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nFFDY/btsQEo4eqfr/ReMxv37abkNVDwrowr4oVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnFFDY%2FbtsQEo4eqfr%2FReMxv37abkNVDwrowr4oVk%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;600&quot; height=&quot;480&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 배포되어 운영중인 동아리 관리 서비스 Weeth 도메인 주소&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;Route 53이란 ?&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;AWS에서 지원하는 DNS(Domain Name System) 이라는 뜻을 가진다&lt;br /&gt;쉽게 설명하면 도메인을 발급하고 관리해주는 서비스를 의미한다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;span&gt;DNS는 &lt;/span&gt;특정&lt;span&gt; &lt;/span&gt;컴퓨터가&lt;span&gt; &lt;/span&gt;가지는&lt;span&gt; &lt;/span&gt;고유한&lt;span&gt; IP &lt;/span&gt;주소를&lt;span&gt; &lt;/span&gt;문자로&lt;span&gt; &lt;/span&gt;변환해주는&lt;span&gt; &lt;/span&gt;시스템을&lt;span&gt; &lt;/span&gt;의미하며, 결론적으로&lt;span&gt; &lt;/span&gt;프론트든&lt;span&gt; &lt;/span&gt;백엔드&lt;span&gt; &lt;/span&gt;서버든&lt;span&gt;, &lt;/span&gt;웹에서는&lt;span&gt; HTTPS&lt;/span&gt;를&lt;span&gt; &lt;/span&gt;적용한&lt;span&gt; &lt;/span&gt;도메인을&lt;span&gt; &lt;/span&gt;기반으로&lt;span&gt; &lt;/span&gt;통신하기에&lt;span&gt; DNS&lt;/span&gt;가&lt;span&gt; &lt;/span&gt;필요한&lt;span&gt; &lt;/span&gt;것이다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본문&lt;/h2&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yMEmo/btsQEFrqa0Z/Zp7kX0JsfWHCZK3rqt8EA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yMEmo/btsQEFrqa0Z/Zp7kX0JsfWHCZK3rqt8EA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yMEmo/btsQEFrqa0Z/Zp7kX0JsfWHCZK3rqt8EA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyMEmo%2FbtsQEFrqa0Z%2FZp7kX0JsfWHCZK3rqt8EA1%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;700&quot; height=&quot;60&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(대부분 학생 신분일거라고 생각하기때문에 도메인 기간은 1년으로 하는 것을 추천한다.... 서비스 동향 보고 변경을 하던, 투자해서 계속 갖고있던)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. AWS Route 53 &lt;span&gt;호스팅 영역 생성&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;도메인을 &lt;/span&gt;Route 53&lt;span&gt;에 호스팅 시켜주기 위해서,&amp;nbsp;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;AWS&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;의 &lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;Route 53에 접속한 뒤에 호스팅 영역을 생성해준다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;1308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xzw4R/btsQD8tMiN3/PxF7C8qJM83sGSjDaxvIwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xzw4R/btsQD8tMiN3/PxF7C8qJM83sGSjDaxvIwk/img.png&quot; data-alt=&quot;Route 53 - 시작하기 - 호스팅 영역 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xzw4R/btsQD8tMiN3/PxF7C8qJM83sGSjDaxvIwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxzw4R%2FbtsQD8tMiN3%2FPxF7C8qJM83sGSjDaxvIwk%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;750&quot; height=&quot;511&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;1308&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Route 53 - 시작하기 - 호스팅 영역 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1694&quot; data-origin-height=&quot;1748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bojwtu/btsQD28aMVa/Uk76nkjNQWGc2fmA9NvQs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bojwtu/btsQD28aMVa/Uk76nkjNQWGc2fmA9NvQs0/img.png&quot; data-alt=&quot;구매한 도메인에 맞게 호스팅 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bojwtu/btsQD28aMVa/Uk76nkjNQWGc2fmA9NvQs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbojwtu%2FbtsQD28aMVa%2FUk76nkjNQWGc2fmA9NvQs0%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;750&quot; height=&quot;774&quot; data-origin-width=&quot;1694&quot; data-origin-height=&quot;1748&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;구매한 도메인에 맞게 호스팅 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Route 53 &lt;span&gt;콘솔&lt;/span&gt; &amp;rarr; &amp;ldquo;&lt;span&gt;호스팅&lt;/span&gt; &lt;span&gt;영역&lt;/span&gt; &lt;span&gt;생성&lt;/span&gt;&amp;rdquo; &amp;rarr; &lt;span&gt;도메인명&lt;/span&gt; &lt;span&gt;입력&lt;/span&gt; (&lt;span&gt;프로토콜&lt;/span&gt;(https://)&lt;span&gt;이나&lt;/span&gt; &lt;span&gt;경로&lt;/span&gt;(/api) &lt;span&gt;같은&lt;/span&gt; &lt;span&gt;건&lt;/span&gt; &lt;span&gt;쓰지&lt;/span&gt; &lt;span&gt;않고&lt;/span&gt;, &lt;span&gt;순수하게&lt;/span&gt; &amp;ldquo;&lt;a href=&quot;http://example.com/&quot;&gt;&lt;span&gt;example.com&lt;/span&gt;&lt;/a&gt;&amp;rdquo; &lt;span&gt;형태만&lt;/span&gt; &lt;span&gt;입력&lt;/span&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본으로&lt;span&gt; &lt;/span&gt;생성된&lt;span&gt; SOA, NS &lt;/span&gt;레코드는&lt;span&gt; 일단 그대로 둔다&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. &lt;span data-token-index=&quot;0&quot;&gt;레코드 확인 및 등록&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;그 다음 앞서 호스팅 영역에서 기본으로&amp;nbsp;생성된 레코드 확인한다&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmaumq/btsQFxGpnwO/UnIhBelxAZNdJnPk7ShJrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmaumq/btsQFxGpnwO/UnIhBelxAZNdJnPk7ShJrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmaumq/btsQFxGpnwO/UnIhBelxAZNdJnPk7ShJrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbmaumq%2FbtsQFxGpnwO%2FUnIhBelxAZNdJnPk7ShJrK%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;750&quot; height=&quot;77&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;120&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 Gabia에 접속한 뒤에, NS(Name Server)의 &lt;span data-token-index=&quot;1&quot;&gt;값/트래픽 라우팅 대상을 Gabia에 네임서버에 등록해준다&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1389&quot; data-origin-height=&quot;748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FLQnN/btsQFBhpvkT/2sndJfPvTmkwKxKkX9ZGt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FLQnN/btsQFBhpvkT/2sndJfPvTmkwKxKkX9ZGt0/img.png&quot; data-alt=&quot;홈 - 도메인 정보 변경 - 네임 서버 - 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FLQnN/btsQFBhpvkT/2sndJfPvTmkwKxKkX9ZGt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFLQnN%2FbtsQFBhpvkT%2F2sndJfPvTmkwKxKkX9ZGt0%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;750&quot; height=&quot;404&quot; data-origin-width=&quot;1389&quot; data-origin-height=&quot;748&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;홈 - 도메인 정보 변경 - 네임 서버 - 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1668&quot; data-origin-height=&quot;1015&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csLPS5/btsQEKlVy9N/lBI2JH9Sos71g1N6CnSS91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csLPS5/btsQEKlVy9N/lBI2JH9Sos71g1N6CnSS91/img.png&quot; data-alt=&quot;입력된 값들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csLPS5/btsQEKlVy9N/lBI2JH9Sos71g1N6CnSS91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsLPS5%2FbtsQEKlVy9N%2FlBI2JH9Sos71g1N6CnSS91%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;750&quot; height=&quot;456&quot; data-origin-width=&quot;1668&quot; data-origin-height=&quot;1015&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;입력된 값들&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 1차 ~ 4차에 NS에 해당하는 값/트래픽 라우팅 대상의 4개의 값 넣어주는데 뒤에 . 은 제외하고 입력한다고 생각해주면 된다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. EC2 주소에 맞게 유형 A 레코드 생성&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1667&quot; data-origin-height=&quot;969&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dexwPg/btsQDYrwtjk/1petOd0xktbjiF4wWCYj1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dexwPg/btsQDYrwtjk/1petOd0xktbjiF4wWCYj1K/img.png&quot; data-alt=&quot;AWS 접속 -&amp;amp;gt; Route 53 &amp;amp;rarr; 좌측 &amp;amp;ldquo;호스팅 영역&amp;amp;rdquo; &amp;amp;rarr; 등록한 주소를 ( example.com ) 클릭&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dexwPg/btsQDYrwtjk/1petOd0xktbjiF4wWCYj1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdexwPg%2FbtsQDYrwtjk%2F1petOd0xktbjiF4wWCYj1K%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;750&quot; height=&quot;436&quot; data-origin-width=&quot;1667&quot; data-origin-height=&quot;969&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AWS 접속 -&amp;gt; Route 53 &amp;rarr; 좌측 &amp;ldquo;호스팅 영역&amp;rdquo; &amp;rarr; 등록한 주소를 ( example.com ) 클릭&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음에는 AWS 콘솔에 다시 접속해서 레코드를 생성해주어야 한다&lt;br /&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;레코드 이름&lt;/span&gt;: &lt;span data-token-index=&quot;2&quot;&gt;(빈칸)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;레코드 유형&lt;/span&gt;: &lt;span data-token-index=&quot;2&quot;&gt;A &amp;ndash; IPv4 주소&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;값&lt;/span&gt;: 00.00.000.000 (EC2 IP값)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;TTL(초)&lt;/span&gt;: 기본 300 (권장: 300~600)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tYibc/btsQEOhiwmG/4tMdLuKlCmfny8HAw7V8R0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tYibc/btsQEOhiwmG/4tMdLuKlCmfny8HAw7V8R0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tYibc/btsQEOhiwmG/4tMdLuKlCmfny8HAw7V8R0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtYibc%2FbtsQEOhiwmG%2F4tMdLuKlCmfny8HAw7V8R0%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;750&quot; height=&quot;188&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;294&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. &lt;span data-token-index=&quot;0&quot;&gt;CNAME 레코드 만들기 (www 서브도메인 &amp;rarr; Vercel)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 동일한 호스팅 영역(example.com)에서 다시 &amp;ldquo;레코드 생성&amp;rdquo; 클릭 후 CNAME 레코드를 생성해주면 된다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;레코드 이름&lt;/b&gt;: &lt;b&gt;(빈칸)&lt;/b&gt; 혹은 @&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레코드 유형&lt;/b&gt;: &lt;b&gt;CNAME&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;값&lt;/b&gt;: (vercel 주소)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TTL(초)&lt;/b&gt;: 기본 300&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DevOps/Route 53</category>
      <category>A 레코드</category>
      <category>aws</category>
      <category>CNAME</category>
      <category>https</category>
      <category>nip.io</category>
      <category>NS</category>
      <category>route 53</category>
      <category>네임서버</category>
      <category>도메인</category>
      <category>호스팅</category>
      <author>huncozyboy</author>
      <guid isPermaLink="true">https://huncozyboy.tistory.com/60</guid>
      <comments>https://huncozyboy.tistory.com/60#entry60comment</comments>
      <pubDate>Fri, 19 Sep 2025 04:29:01 +0900</pubDate>
    </item>
    <item>
      <title>userId를 매번 입력받지않고도 로그인한 유저 정보를 가져올 수 있을까 ? (JWT)</title>
      <link>https://huncozyboy.tistory.com/59</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;들어가며&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티는 &lt;span&gt;SecurityContext&lt;/span&gt;에 인증 객체(&lt;span&gt;Authentication&lt;/span&gt;)를 저장한다&lt;/p&gt;
&lt;pre id=&quot;code_1758222257562&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public String getCurrentUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        
        // Principal 객체 (UserDetails 구현체) 가져오기
        Object principal = authentication.getPrincipal();

        if (principal instanceof UserDetails userDetails) {
            return &quot;현재 로그인한 사용자: &quot; + userDetails.getUsername();
        } else {
            return &quot;현재 로그인한 사용자: &quot; + principal.toString();
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 컨트롤러에서 &lt;span&gt;SecurityContextHolder.getContext().getAuthentication()&lt;/span&gt; &amp;rarr; &lt;span&gt;getPrincipal()&lt;/span&gt;을 타고 들어가면 로그인한 사용자의 정보를 꺼낼 수 있지만, 이렇게 매번 &lt;span&gt;SecurityContextHolder&lt;/span&gt;를 직접 꺼내 쓰면 컨트롤러 코드가 지저분해지고, 꺼낼 수 있는 정보도 상황에 따라 한정적이게된다&lt;/p&gt;
&lt;pre id=&quot;code_1758219918937&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
CustomUser custom = (CustomUser) authentication == null ? null : authentication.getPrincipal();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 3.2부터는&amp;nbsp;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;@AuthenticationPrincipal 어노테이션을 활용해 줄 수 있는데, &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 어노테이션은 현재 인증 객체의 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;principal&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;을 메서드 파라미터로 &amp;ldquo;주입&amp;rdquo; 해준다&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clkthA/btsQEjPj804/Eq08mVBQ3WkHKMhSOL7jb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clkthA/btsQEjPj804/Eq08mVBQ3WkHKMhSOL7jb0/img.png&quot; data-alt=&quot;상세 내용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clkthA/btsQEjPj804/Eq08mVBQ3WkHKMhSOL7jb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclkthA%2FbtsQEjPj804%2FEq08mVBQ3WkHKMhSOL7jb0%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;600&quot; height=&quot;199&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;384&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;상세 내용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스팅은 앞서 설명한 AuthenticationPrincipal의 개념을 활용한 CurrentUser에 대한 내용을 다뤄보려고 한다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;@CurrentUser 추상화&lt;/h3&gt;
&lt;pre id=&quot;code_1758220073352&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/api/youtube&quot;)
public class YoutubeController {
    private final YoutubeCrawlerService youtubeCrawlerService;

    @Hidden
    @PostMapping(&quot;/{productId}/crawl&quot;)
    public ApiResponse&amp;lt;CrawlResponse&amp;gt; crawl(@PathVariable Long userId) {
        List&amp;lt;String&amp;gt; videoUrls = youtubeCrawlerService.crawlAndStoreReviews(userId);

        return ApiResponse.success(HttpStatus.OK, ResponseMessage.CRAWL_SUCCESS.getMessage(),
                CrawlResponse.of(videoUrls));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 흔히 사용할 수있는 컨트롤러의 예시이다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로그인한 사용자와, 로그인하지 않은 사용자를 구분하기 위해 JWT 토큰을 헤더에 받고, 비즈니스 로직에서 유저와 관련한 정보를 필요로 하기때문에 userId를 헤더나 바디에 받는 방식이라고 이해해주면 된다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 CurrentUser를 추상화해서 로직을 구현하면, 아래처럼 헤더에 토큰만 넣어준다면, 클라쪽에서 별도로 userId를 보내줄 필요가 없게된다&lt;/p&gt;
&lt;pre id=&quot;code_1758220253251&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Authorization: Bearer eyJ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;즉, 토큰만 헤더에 실어서 보내면 서버가 자동으로 &lt;b&gt;JWT 토큰에서 userId를 읽어 비즈니스 로직에 전달&lt;/b&gt;해준다. (요청 스펙이 깔끔해지고, 불필요한 필드가 하나 줄어든다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 원리로 가능한걸까 ?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;@CurrentUser 작동 방식&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1098&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sKOQ5/btsQGezywmb/vyjDlKWY62lOKv6Dv932n0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sKOQ5/btsQGezywmb/vyjDlKWY62lOKv6Dv932n0/img.png&quot; data-alt=&quot;JWT 원문&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sKOQ5/btsQGezywmb/vyjDlKWY62lOKv6Dv932n0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsKOQ5%2FbtsQGezywmb%2FvyjDlKWY62lOKv6Dv932n0%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;600&quot; height=&quot;455&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;1098&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JWT 원문&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 &amp;ldquo;양 당사자 사이에서 전달할 클레임을 안전하게 표현&amp;rdquo;하는 표준이다(RFC 7519). 헤더 + 페이로드 + 서명 세 부분으로 구성되고, 페이로드에 &lt;span&gt;sub&lt;/span&gt;, &lt;span&gt;iss&lt;/span&gt;, &lt;span&gt;exp&lt;/span&gt; 같은 표준 클레임과 &lt;span&gt;id&lt;/span&gt; 같은 &lt;span&gt;&lt;b&gt;커스텀 클레임&lt;/b&gt;&lt;/span&gt;을 넣을 수 있는데, 우리는 여기에 &lt;span&gt;userId&lt;/span&gt;를 담아둔다고 가정해보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;br /&gt;그러면 클라에서 요청을 보낼때 해당 값들로 암호화된 JWT가 담겨져서 온다면, 스프링 시큐리티에서 &lt;/span&gt;Authorization: Bearer&lt;span&gt; 토큰을 자동으로 검증하고, &lt;/span&gt;Authentication&lt;span&gt;을 구성해 &lt;/span&gt;SecurityContextHolder&lt;span&gt;에 넣어서 &lt;/span&gt;&lt;span&gt;principal&lt;/span&gt;이 &lt;span&gt;Jwt&lt;/span&gt; 객체가 되어 클레임을 바로 읽을 수 있게 되는것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰에 어떤 값이 들어있는지 궁금하면 jwt.io 같은 디버거로 바로 확인할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.jwt.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.jwt.io/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1758220534553&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;JSON Web Tokens - jwt.io&quot; data-og-description=&quot;JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS).&quot; data-og-host=&quot;www.jwt.io&quot; data-og-source-url=&quot;https://www.jwt.io/&quot; data-og-url=&quot;https://jwt.io&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/kXL8h/hyZJrwyMIB/cEdxq8KNt02pNkhwpRLVBk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/biUxsa/hyZI4CdvBU/DNBQyIo1lbFru1wRkZHzGK/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675&quot;&gt;&lt;a href=&quot;https://www.jwt.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.jwt.io/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/kXL8h/hyZJrwyMIB/cEdxq8KNt02pNkhwpRLVBk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/biUxsa/hyZI4CdvBU/DNBQyIo1lbFru1wRkZHzGK/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675');&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;JSON Web Tokens - jwt.io&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS).&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.jwt.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;본문&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제 구현한 내용을 단계별로 설명해보려고 한다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실제 구현 플로우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제 구현한 내용을 단계별로 설명해보려고 한다&lt;/p&gt;
&lt;pre id=&quot;code_1758220550900&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저, 컨트롤러 파라미터에 붙일 마커 애노테이션을 정의해준다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 CurrentUserResolver를 구현해주었다&lt;/p&gt;
&lt;pre id=&quot;code_1758220587417&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class CurrentUserResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(CurrentUser.class)
                &amp;amp;&amp;amp; parameter.getParameterType().equals(Long.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication == null || authentication.getPrincipal() == null
                || !(authentication.getPrincipal() instanceof Jwt jwt)) {
            return null; // or throw
        }

        return Long.valueOf(jwt.getClaimAsString(JwtProvider.ID_CLAIM));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;전체적인 메서드의 흐름은 아래와 같다&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;SecurityContextHolder&lt;/span&gt;에서 현재 토큰 꺼내기. (스레드 로컬 기반 컨텍스트) &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;principal&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;은 보통 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Jwt&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;. 여기서 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;getClaimAsString(&quot;id&quot;)&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 같은 식으로 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;b&gt;클레임을 읽어 userId로 변환&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;상황에 따라 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;null&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 대신 예외를 던질 수도 있음&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/ol&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그리고 &lt;/span&gt;구현한&lt;span&gt; &lt;/span&gt;리졸버를&lt;span&gt; MVC&lt;/span&gt;에&lt;span&gt; &lt;/span&gt;등록해야한다&lt;/p&gt;
&lt;pre id=&quot;code_1758220823440&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

    private final CurrentUserResolver currentUserResolver;

    @Override
    public void addArgumentResolvers(List&amp;lt;HandlerMethodArgumentResolver&amp;gt; resolvers) {
        resolvers.addFirst(currentUserResolver);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 WebMvcConfigurer#addArgumentResolvers로 커스텀 리졸버를 가장 앞에 두도록하여 우선 적용되도록 설정해주었다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; 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_1758220893062&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;/{productId}/crawl&quot;)
public ApiResponse&amp;lt;CrawlResponse&amp;gt; crawl(@CurrentUser Long userId) {
    List&amp;lt;String&amp;gt; videoUrls = youtubeCrawlerService.crawlAndStoreReviews(userId);
    return ApiResponse.success(HttpStatus.OK, ResponseMessage.CRAWL_SUCCESS.getMessage(),
            CrawlResponse.of(videoUrls));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사실 엄청 간단한 내용이지만, 클라이언트는 &lt;span&gt;&lt;b&gt;헤더에 토큰만&lt;/b&gt;&lt;/span&gt; 보내면 되기때문에 불필요한 request가 간소화될 수 있다. 또한 백엔드 측면에서도 바디/쿼리/경로에 &lt;span&gt;userId&lt;/span&gt;를 두지 않아도 되고, 서버가 검증한 토큰의 값을 사용할 수 있어서 보안적인 측면에서도 이점이 있다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고한 자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/api/java/org/springframework/security/core/annotation/AuthenticationPrincipal.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-security/reference/api/java/org/springframework/security/core/annotation/AuthenticationPrincipal.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring/JWT</category>
      <category>AuthenticationPrincipal</category>
      <category>currentuser</category>
      <category>JWT</category>
      <category>Spring</category>
      <category>spring security</category>
      <category>userid</category>
      <author>huncozyboy</author>
      <guid isPermaLink="true">https://huncozyboy.tistory.com/59</guid>
      <comments>https://huncozyboy.tistory.com/59#entry59comment</comments>
      <pubDate>Fri, 19 Sep 2025 03:43:01 +0900</pubDate>
    </item>
    <item>
      <title>슬랙 웹훅으로 신고하기 기능 구현하기 (Webhook)</title>
      <link>https://huncozyboy.tistory.com/58</link>
      <description>&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 링크 서비스에 기획에 대해서 가볍게 설명하려고 한다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GOIki/btsQa8M1muu/WDi1J6LCwYkY5DTLPKKGk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GOIki/btsQa8M1muu/WDi1J6LCwYkY5DTLPKKGk0/img.png&quot; data-alt=&quot;페르소나 : 동아리에서 친해지기 위한 커뮤니케이션 수단이 필요한 동아리원&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GOIki/btsQa8M1muu/WDi1J6LCwYkY5DTLPKKGk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGOIki%2FbtsQa8M1muu%2FWDi1J6LCwYkY5DTLPKKGk0%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;650&quot; height=&quot;356&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;페르소나 : 동아리에서 친해지기 위한 커뮤니케이션 수단이 필요한 동아리원&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Leenk 서비스는, Leets라는 동아리를 운영하는 입장에서 생각했을때 동아리 원들의 커뮤니케이션 수단을 개발해서 제공해준다면 어떨까 ?? 라는 생각이 들어 시작하게된 서비스다&lt;br /&gt;&lt;br /&gt;예쁜 디자인 + 랜딩 사이트 만들어주셔서 개큰감사  &amp;zwj;♂️ (Shout out to 도연, 한별)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://leets-makers.framer.website/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://leets-makers.framer.website/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756319594145&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Leets Makers&quot; data-og-description=&quot;Gachon University IT Collaboration Club, Leets Makers' Landing Page - Made by. Makers' Designer, Doaeng.&quot; data-og-host=&quot;leets-makers.framer.website&quot; data-og-source-url=&quot;https://leets-makers.framer.website/&quot; data-og-url=&quot;https://leets-makers.framer.website/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/DWwNw/hyZF8JE7YV/f5RvJGfiB9lKDuGIMmSkc1/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024,https://scrap.kakaocdn.net/dn/fHPd9/hyZC8kbKZY/FR1RnA6abVPZGhBEk8L111/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024,https://scrap.kakaocdn.net/dn/2Nyao/hyZDNT7Lbg/DpKOmasLK4f1dPG0MhchH0/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024&quot;&gt;&lt;a href=&quot;https://leets-makers.framer.website/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://leets-makers.framer.website/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/DWwNw/hyZF8JE7YV/f5RvJGfiB9lKDuGIMmSkc1/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024,https://scrap.kakaocdn.net/dn/fHPd9/hyZC8kbKZY/FR1RnA6abVPZGhBEk8L111/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024,https://scrap.kakaocdn.net/dn/2Nyao/hyZDNT7Lbg/DpKOmasLK4f1dPG0MhchH0/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024');&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;Leets Makers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Gachon University IT Collaboration Club, Leets Makers' Landing Page - Made by. Makers' Designer, Doaeng.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;leets-makers.framer.website&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1588&quot; data-origin-height=&quot;722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mOyUd/btsQbwNIgj7/7pTncJmRXAtUo65NYIVmtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mOyUd/btsQbwNIgj7/7pTncJmRXAtUo65NYIVmtk/img.png&quot; data-alt=&quot;실제 수료한 동아리 원들을 대상으로 한 설문조사 내용들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mOyUd/btsQbwNIgj7/7pTncJmRXAtUo65NYIVmtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmOyUd%2FbtsQbwNIgj7%2F7pTncJmRXAtUo65NYIVmtk%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;650&quot; height=&quot;296&quot; data-origin-width=&quot;1588&quot; data-origin-height=&quot;722&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 수료한 동아리 원들을 대상으로 한 설문조사 내용들&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1330&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbNGNk/btsP9D8lR6R/drwWuDbGJxV5kmFZuV5CbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbNGNk/btsP9D8lR6R/drwWuDbGJxV5kmFZuV5CbK/img.png&quot; data-alt=&quot;주요 기능인 피드 + 링크 기능&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbNGNk/btsP9D8lR6R/drwWuDbGJxV5kmFZuV5CbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbNGNk%2FbtsP9D8lR6R%2FdrwWuDbGJxV5kmFZuV5CbK%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;650&quot; height=&quot;341&quot; data-origin-width=&quot;1330&quot; data-origin-height=&quot;698&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;주요 기능인 피드 + 링크 기능&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동아리원들이 자유롭게 사진을 공유하고, 모각작과 같은 링크를 생성하면서 특정 게시글을 신고할 수 있는 기능이 필요하여 도입하게 되었다 + TMI로 지금 생각해보면 정말 필요한 기능이였는가? 라는 생각도 들지만, 아래 사진에 나와있듯이 앱 출시 리젝 사유중에 &lt;span style=&quot;color: #ee2323;&quot;&gt;&quot; 신고 및 차단 기능이 존재해야함 &quot;&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;이라는 내용이 있었어서 도입을 결정하게 되었다  &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xKrtJ/btsP8EGt8ij/MvlVKYzlkC9PpDbWMvmHbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xKrtJ/btsP8EGt8ij/MvlVKYzlkC9PpDbWMvmHbK/img.png&quot; data-alt=&quot;문서화 했었던 앱 출시시 리젝 사유들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xKrtJ/btsP8EGt8ij/MvlVKYzlkC9PpDbWMvmHbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxKrtJ%2FbtsP8EGt8ij%2FMvlVKYzlkC9PpDbWMvmHbK%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;650&quot; height=&quot;271&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;문서화 했었던 앱 출시시 리젝 사유들&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style7&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;구현한 플로우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서론이 조금 길었지만, 다시 돌아와서 이번 포스팅에서 다룰 내용은 &lt;b&gt;&quot; 링크 신고하기 기능 &quot;&amp;nbsp;&lt;/b&gt;이다. 유저가 신고하기 버튼을 누르면 Slack의 Webhook을 활용해서, 운영진의 슬랙으로 알림이 오는 로직으로 구현을 하였다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJtpmA/btsP7CvPJCk/hhQ0IVS2utQT5MYNmkH88k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJtpmA/btsP7CvPJCk/hhQ0IVS2utQT5MYNmkH88k/img.png&quot; data-alt=&quot;링크 신고하기 기능 바텀시트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJtpmA/btsP7CvPJCk/hhQ0IVS2utQT5MYNmkH88k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJtpmA%2FbtsP7CvPJCk%2FhhQ0IVS2utQT5MYNmkH88k%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;650&quot; height=&quot;655&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;802&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;링크 신고하기 기능 바텀시트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;1. 슬랙 웹훅 URL 환경변수 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 아래의 앱추가를 눌러준다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;466&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbZu2t/btsQbxsjUDA/jMKsBMkSf4zKLu5zW4JMvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbZu2t/btsQbxsjUDA/jMKsBMkSf4zKLu5zW4JMvK/img.png&quot; data-alt=&quot;앱추가 클릭&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbZu2t/btsQbxsjUDA/jMKsBMkSf4zKLu5zW4JMvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbZu2t%2FbtsQbxsjUDA%2FjMKsBMkSf4zKLu5zW4JMvK%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;466&quot; height=&quot;250&quot; data-origin-width=&quot;466&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;앱추가 클릭&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 아래처럼 생긴 웹훅 앱을 사용하고싶은 채널에 추가해주면 된다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wsDvI/btsQaPUuSMm/AaeNVYJeB4D3A0gLuKAiR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wsDvI/btsQaPUuSMm/AaeNVYJeB4D3A0gLuKAiR0/img.png&quot; data-alt=&quot;web-hook-app 이라는 앱 추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wsDvI/btsQaPUuSMm/AaeNVYJeB4D3A0gLuKAiR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwsDvI%2FbtsQaPUuSMm%2FAaeNVYJeB4D3A0gLuKAiR0%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;330&quot; height=&quot;110&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;web-hook-app 이라는 앱 추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1yN4O/btsQagEQRP2/qI0oT3ntKtgNZNpWFzlcyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1yN4O/btsQagEQRP2/qI0oT3ntKtgNZNpWFzlcyk/img.png&quot; data-alt=&quot;사용하고 싶은 채널을 선택해서 지정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1yN4O/btsQagEQRP2/qI0oT3ntKtgNZNpWFzlcyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1yN4O%2FbtsQagEQRP2%2FqI0oT3ntKtgNZNpWFzlcyk%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;500&quot; height=&quot;223&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;460&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사용하고 싶은 채널을 선택해서 지정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;554&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciBr86/btsP91Vr7W5/bcq5KxkHp4r8U8dGH5Uj6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciBr86/btsP91Vr7W5/bcq5KxkHp4r8U8dGH5Uj6K/img.png&quot; data-alt=&quot;위 사진처럼 웹훅 URL이 나오게 되는데 Copy를 눌러 복사&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciBr86/btsP91Vr7W5/bcq5KxkHp4r8U8dGH5Uj6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciBr86%2FbtsP91Vr7W5%2Fbcq5KxkHp4r8U8dGH5Uj6K%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;554&quot; height=&quot;256&quot; data-origin-width=&quot;554&quot; data-origin-height=&quot;256&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;위 사진처럼 웹훅 URL이 나오게 되는데 Copy를 눌러 복사&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 yml 파일에 아래 처럼 선언하여, 위에서 복사한 웹훅 URL을 주입받는다고 이해해주면 된다&lt;/p&gt;
&lt;pre id=&quot;code_1756320131934&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;slack:
  webhook_url: ${SLACK_WEBHOOK_URL}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;2. LeenkReportRequest 입력 DTO 추가&lt;/h3&gt;
&lt;pre id=&quot;code_1756319677694&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record LeenkReportRequest(

        @NotBlank
        @Size(max = 100)
        String report
) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 세팅이 끝난 뒤엔 먼저 위에 뷰에서도 나와있듯이, 신고 사유를 입력받을 수 있도록 LeenkReportRequest를 추가해주었다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. reportLeenk 비즈니스 로직 구현&lt;/h3&gt;
&lt;pre id=&quot;code_1756319751493&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(readOnly = true)
    public void reportLeenk(Long userId, Long leenkId, LeenkReportRequest request) {
        User user = userGetService.findById(userId);
        Leenk leenk = leenkGetService.findById(leenkId);

        notionDatabaseService.sendLeenkReport(request.report(), user.getId(), leenk.getId());
        slackWebhookService.sendLeenkReport(request.report());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 실제 존재하는 유저인지, 링크인지 조회해오면서 예외처리를 진행해주고 노션 데이터베이스와 슬랙 웹훅에 알림을 전송하는 로직을 구현했다 (노션 데이터베이스에도 저장하는 이유는 예상치 못한 장애 발생시에도 유실 방지 + 신고가 들어오면 회의때 쉽게 트래킹하기 위함)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. sendLeenkReport로 슬랙에 신고 내용 전송&lt;/h3&gt;
&lt;pre id=&quot;code_1756319944291&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void sendLeenkReport(String report) {
        String formatted = &quot;*[링크 신고]*\n```&quot; + report + &quot;```&quot;;
        
        slackRestClient.post()
                .uri(webhookUrl)
                .header(&quot;Content-Type&quot;, &quot;application/json&quot;)
                .body(Map.of(&quot;text&quot;, formatted))
                .retrieve()
                .toBodilessEntity();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SlackWebhookService에서 선언한 메서드인데, webhookUrl을 환경변수로 주입받아서 슬랙으로 전송할 최소 페이로드: &lt;span&gt;{&quot;text&quot;: &quot;메시지&quot;}&lt;/span&gt; 를 설정해주고, 위 코드를 보면 알 수 있듯이 json 형식으로 자유롭게 커스텀 할 수 있다고 생각해주면 된다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bx3xE/btsP9BbDV5a/10iGwNzb7tyy5mexxlyPF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bx3xE/btsP9BbDV5a/10iGwNzb7tyy5mexxlyPF1/img.png&quot; data-alt=&quot;정상적으로 슬랙에 남는 신고 기능&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bx3xE/btsP9BbDV5a/10iGwNzb7tyy5mexxlyPF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBx3xE%2FbtsP9BbDV5a%2F10iGwNzb7tyy5mexxlyPF1%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;664&quot; height=&quot;392&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;정상적으로 슬랙에 남는 신고 기능&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 유저가 신고하기를 누르면, 슬랙에서 정상적으로 트래킹을 할 수 있었다&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGJh7o/btsP7xnCRnI/XD146r9XubxMPea1NUky8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGJh7o/btsP7xnCRnI/XD146r9XubxMPea1NUky8k/img.png&quot; data-alt=&quot;링크 서비스 피드백 기능&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGJh7o/btsP7xnCRnI/XD146r9XubxMPea1NUky8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGJh7o%2FbtsP7xnCRnI%2FXD146r9XubxMPea1NUky8k%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;650&quot; height=&quot;113&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;226&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;링크 서비스 피드백 기능&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;+ 추가로, 비슷한 플로우로 링크 서비스 자체에 대한 피드백도 위처럼 받을 수 있도록 구현해놓아서, 실제 사용자의 의견을 신속하게 받을 수 있도록 하였다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Discord Webhook 과의 차이점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 이전 게시글인데, Discord Webhook의 구현 플로우가 궁금하다면 아래 링크를 참고해주면 더 좋을거같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://huncozyboy.tistory.com/57&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://huncozyboy.tistory.com/57&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756321156200&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;디스코드를 활용한 500 에러 알림 자동화 (Webhook)&quot; data-og-description=&quot;들어가며지금 해당 API 500 뜨는데 원인 알 수 있을까요지금 오류 로그 한번만 확인해주실래요 ??해당 API 500 뜨는데 원인이 뭘까요백엔드로 다른 파트와 개발을 진행하다보면, 플젝을 진행하면서 &quot; data-og-host=&quot;huncozyboy.tistory.com&quot; data-og-source-url=&quot;https://huncozyboy.tistory.com/57&quot; data-og-url=&quot;https://huncozyboy.tistory.com/57&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cTjenT/hyZGhs21lx/U9Jjy8tMKsHdSg1AWKwo00/img.png?width=800&amp;amp;height=778&amp;amp;face=0_0_800_778,https://scrap.kakaocdn.net/dn/FsD6u/hyZF7YhacQ/QUVjkNhqmpms5ZtpCH30jK/img.png?width=800&amp;amp;height=778&amp;amp;face=0_0_800_778,https://scrap.kakaocdn.net/dn/dAZRdz/hyZGg8J9WL/njKAbpVOPnXdh5nvx71jaK/img.png?width=1488&amp;amp;height=1496&amp;amp;face=0_0_1488_1496&quot;&gt;&lt;a href=&quot;https://huncozyboy.tistory.com/57&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://huncozyboy.tistory.com/57&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cTjenT/hyZGhs21lx/U9Jjy8tMKsHdSg1AWKwo00/img.png?width=800&amp;amp;height=778&amp;amp;face=0_0_800_778,https://scrap.kakaocdn.net/dn/FsD6u/hyZF7YhacQ/QUVjkNhqmpms5ZtpCH30jK/img.png?width=800&amp;amp;height=778&amp;amp;face=0_0_800_778,https://scrap.kakaocdn.net/dn/dAZRdz/hyZGg8J9WL/njKAbpVOPnXdh5nvx71jaK/img.png?width=1488&amp;amp;height=1496&amp;amp;face=0_0_1488_1496');&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;디스코드를 활용한 500 에러 알림 자동화 (Webhook)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며지금 해당 API 500 뜨는데 원인 알 수 있을까요지금 오류 로그 한번만 확인해주실래요 ??해당 API 500 뜨는데 원인이 뭘까요백엔드로 다른 파트와 개발을 진행하다보면, 플젝을 진행하면서&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;huncozyboy.tistory.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;p data-ke-size=&quot;size16&quot;&gt;개인적으로 슬랙은 디스코드와 비교해서&amp;nbsp;&lt;span&gt;&lt;b&gt;JSON 문법이 엄격&lt;/b&gt;&lt;/span&gt;해서 테스트할때 잘못 보내면 &lt;span style=&quot;color: #ee2323;&quot;&gt;400 &lt;span style=&quot;color: #333333;&quot;&gt;에러가 나오는 경우가 더 많았었다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;Block Kit&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;복잡한 구조&lt;/b&gt;&lt;/span&gt;를 공식 지원해주는 것은 장점이라고 생각했었다. 디스코드는 JSON 형태라기보다는, &lt;/span&gt;&lt;/span&gt;텍스트 마크다운과 코드블럭을 기본 지원해주어서 표현 부분이 조금 다르다는 생각도 들었다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 추가로 찾아보다가 알게된 정보인데, Slack은 4,000자까지 UI 잘림 없이 잘 반환되지만, &lt;span style=&quot;color: #ee2323;&quot;&gt;Discord는 2,000자 제한&lt;/span&gt;이 있기 때문에 긴 로그같은 내용은 분할하거나 파일로 첨부하는 방식도 있다고 나와있었다&lt;br /&gt;&lt;br /&gt;채팅 기능이나, 매칭 기능, 알림 기능 등등에서 DLQ를 활용할 수 있듯이, 슬랙에서는&amp;nbsp;&lt;span&gt;레이트 리밋 시 &lt;/span&gt;&lt;b&gt;HTTP 429 + Retry-After&lt;/b&gt;&lt;span&gt; 헤더로 백오프 로직을 짤 수 있고, 디스코드에서는 &lt;/span&gt;429 retry_after&lt;span&gt; 기반으로 재시도 로직을 짤 수 있어 트래픽 병목이 생긴다면 해당 내용까지 고려해주면 더 좋은 설계가 될 수 있을거라고 생각했었다  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고한 자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://support.discord.com/hc/ko/articles/228383668&quot;&gt;https://support.discord.com/hc/ko/articles/228383668&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;a href=&quot;https://api.slack.com/messaging/webhooks&quot;&gt;https://api.slack.com/messaging/webhooks&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/Webhook</category>
      <category>discord</category>
      <category>slack</category>
      <category>Spring</category>
      <category>webhook</category>
      <category>디스코드</category>
      <category>디코</category>
      <category>스프링</category>
      <category>슬랙</category>
      <category>신고하기</category>
      <category>웹훅</category>
      <author>huncozyboy</author>
      <guid isPermaLink="true">https://huncozyboy.tistory.com/58</guid>
      <comments>https://huncozyboy.tistory.com/58#entry58comment</comments>
      <pubDate>Thu, 28 Aug 2025 03:58:07 +0900</pubDate>
    </item>
  </channel>
</rss>