<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>00kang_jh.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 16 Jul 2026 07:45:27 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>00kang_jh.log</title>
            <url>https://velog.velcdn.com/images/00kang_jh/profile/4644aebf-1d0d-4cd5-a988-8d1e243b4346/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 00kang_jh.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/00kang_jh" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[회의록 자동화 만들기 3: 실행까지 연결하고, 시리즈를 마치며 (6편)]]></title>
            <link>https://velog.io/@00kang_jh/%ED%9A%8C%EC%9D%98%EB%A1%9D-%EC%9E%90%EB%8F%99%ED%99%94-%EB%A7%8C%EB%93%A4%EA%B8%B0-3-%EC%8B%A4%ED%96%89%EA%B9%8C%EC%A7%80-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B3%A0-%EC%8B%9C%EB%A6%AC%EC%A6%88%EB%A5%BC-%EB%A7%88%EC%B9%98%EB%A9%B0-6%ED%8E%B8</link>
            <guid>https://velog.io/@00kang_jh/%ED%9A%8C%EC%9D%98%EB%A1%9D-%EC%9E%90%EB%8F%99%ED%99%94-%EB%A7%8C%EB%93%A4%EA%B8%B0-3-%EC%8B%A4%ED%96%89%EA%B9%8C%EC%A7%80-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B3%A0-%EC%8B%9C%EB%A6%AC%EC%A6%88%EB%A5%BC-%EB%A7%88%EC%B9%98%EB%A9%B0-6%ED%8E%B8</guid>
            <pubDate>Thu, 16 Jul 2026 07:45:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>n8n 학습기 시리즈 — 6편(마지막). 토이 프로젝트 (3): Slack 알림 + 이슈 자동 등록</strong>
5편에서 회의록이 구조화된 데이터가 됐다. 이제 이 데이터를 실제 행동으로 옮긴다. 요약은 Slack으로, 액션아이템은 이슈로. &quot;요약만 하는 봇&quot;과 &quot;실행까지 이어지는 자동화&quot;의 차이가 여기서 완성된다.</p>
</blockquote>
<h2 id="들어가며">들어가며</h2>
<p>지금까지의 여정을 되짚어보자. 4편에서 회의록을 워크플로우 안으로 들여왔고, 5편에서 그걸 LLM에 태워 이런 데이터를 얻었다.</p>
<pre><code class="language-json">{
  &quot;summary&quot;: &quot;로그인 버그 수정과 회원가입 리뉴얼 일정을 논의함&quot;,
  &quot;decisions&quot;: [&quot;회원가입 페이지 리뉴얼은 다음 스프린트로 연기&quot;],
  &quot;actionItems&quot;: [
    { &quot;assignee&quot;: &quot;박서연&quot;, &quot;task&quot;: &quot;로그인 세션 만료 버그 수정&quot;, &quot;dueDate&quot;: &quot;2026-07-17&quot;, &quot;priority&quot;: &quot;high&quot; },
    { &quot;assignee&quot;: &quot;이민호&quot;, &quot;task&quot;: &quot;디자인 시안 피그마 링크 공유&quot;, &quot;dueDate&quot;: &quot;2026-07-15&quot;, &quot;priority&quot;: &quot;medium&quot; }
  ]
}</code></pre>
<p>여기서 멈추면 그냥 &quot;잘 정리된 JSON&quot;이다. 우리가 애초에 못 견뎌 했던 건 <strong>결정이 실행에서 누락되는 것</strong>이었다. 그러니 이 데이터가 실제로 누군가의 할 일 목록에 꽂혀야 프로젝트가 완성된다.</p>
<p>이번 편의 목표는 둘이다. <strong>요약과 결정사항은 Slack에 한 번 발송</strong>하고, <strong>액션아이템은 각각 이슈로 등록</strong>하는 것.</p>
<h2 id="두-출력은-처리-방식이-다르다">두 출력은 처리 방식이 다르다</h2>
<p>5편에서 &quot;사람이 읽을 것 / 기계가 처리할 것&quot;으로 나눴는데, 그 구분이 여기서 처리 방식의 차이로 이어진다.</p>
<ul>
<li><strong>요약·결정사항</strong> → 하나의 메시지로 묶어 Slack에 <strong>한 번</strong> 보낸다. (1건)</li>
<li><strong>액션아이템</strong> → 배열 안에 여러 개가 들어 있고, <strong>각각을 개별 이슈로</strong> 만들어야 한다. (N건)</li>
</ul>
<p>전자는 단순하다. 후자가 이번 편의 핵심이자, 3편에서 배운 개념이 마침내 실전으로 쓰이는 지점이다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/115efe14-ac63-46f6-a561-034357c4bd37/image.png" alt=""></p>
<h2 id="배열을-아이템으로-쪼개기--split-out">배열을 아이템으로 쪼개기 — Split Out</h2>
<p>문제가 하나 있다. LLM이 뱉은 데이터는 <strong>한 아이템 안에 <code>actionItems</code> 배열이 통째로</strong> 들어 있는 형태다. 이 상태로는 이슈를 하나씩 만들 수 없다. 이슈 등록 노드는 &quot;아이템 하나 = 이슈 하나&quot;로 동작하는데, 지금은 아이템이 하나뿐이고 그 안에 할일이 여러 개 뭉쳐 있기 때문이다.</p>
<p>3편에서 배운 게 여기서 떠올랐다. n8n은 <strong>아이템 단위로</strong> 노드를 반복 실행한다. 그러니 액션아이템 배열을 <strong>개별 아이템으로 펼쳐주면</strong>, 그다음 이슈 노드가 알아서 각각에 대해 한 번씩 돈다. 이 &quot;배열을 아이템으로 펼치는&quot; 일을 하는 게 <strong>Split Out</strong> 노드다.</p>
<p>Split Out 노드를 추가하고 설정한다.</p>
<ul>
<li><strong>Field To Split Out</strong> → <code>actionItems</code> (펼칠 배열 필드)</li>
<li><strong>Include Other Fields</strong> → 필요하면 켠다 (요약 등 상위 필드를 각 아이템에 딸려 보내고 싶을 때)</li>
</ul>
<p>이러면 액션아이템이 2개였을 때, Split Out을 통과한 뒤 <strong>아이템 2개</strong>가 된다. 하나씩 흐르는 것이다.</p>
<pre><code>[ { actionItems: [A, B] } ]     →  Split Out  →  [ {A}, {B} ]
   (1개 아이템, 배열 안에 2개)                      (2개 아이템)</code></pre><p>1편에서 &quot;왜 데이터를 배열로 감싸지?&quot;라고 물었던 것, 3편에서 &quot;노드는 아이템마다 자동 반복된다&quot;고 배운 것이 <strong>바로 이 순간을 위한</strong> 것이었다. 액션아이템이 3개인 회의록을 넣으면 이슈가 3개 생기고, 0개인 회의록을 넣으면(빈 배열) 이슈가 안 생긴다. 내가 루프를 짜지 않아도.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/a4f811ea-b8bf-4289-b219-cd686115d0af/image.png" alt=""></p>
<h2 id="액션아이템을-이슈로-등록">액션아이템을 이슈로 등록</h2>
<p>Split Out 뒤에 이슈 생성 노드를 붙인다. 나는 GitHub Issues를 썼다(H-HABIT 레포와 엮으면 실전 그대로다). Notion, Jira, ClickUp 등 팀이 쓰는 도구로 바꿔도 구조는 같다.</p>
<p>노드 설정에서 이슈의 각 필드를 표현식으로 채운다. 2편에서 익힌 그 매핑이다.</p>
<ul>
<li><strong>Title</strong> → <code>{{ $json.task }}</code></li>
<li><strong>Body</strong> → 담당자와 기한을 조합. 예: <code>담당: {{ $json.assignee }} / 기한: {{ $json.dueDate }}</code></li>
<li><strong>Labels</strong> → 우선순위를 라벨로. <code>{{ $json.priority }}</code></li>
</ul>
<p>Split Out 덕분에 이 노드는 <strong>각 액션아이템마다 한 번씩</strong> 실행되고, 실행할 때마다 그 아이템의 <code>task</code>, <code>assignee</code>가 표현식에 채워진다. 아이템 단위 실행이 이렇게 편할 수가 없다.</p>
<p>한 가지 QA 습관이 발동한 부분. 이슈 제목이나 본문에 <code>null</code>이 그대로 박히면 보기 흉하다(담당자 미정인 경우). 그래서 표현식에 기본값을 넣었다.</p>
<pre><code>담당: {{ $json.assignee ?? &quot;미정&quot; }}</code></pre><p>5편에서 &quot;담당자 없으면 null&quot;로 뽑게 해놓고, 여기서 그 null을 &quot;미정&quot;으로 표시하는 것이다. 데이터 계층에선 null로 정확하게 두고, 표시 계층에서 사람이 읽기 좋게 바꾼다. 관심사를 분리한 셈이다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/a2aab446-e8f1-4e24-aa67-9da7aa673b74/image.png" alt=""></p>
<h2 id="요약은-slack으로">요약은 Slack으로</h2>
<p>액션아이템과 달리 요약·결정사항은 회의당 하나면 된다. 그래서 Split Out을 거치지 않은 원래 데이터에서 뽑아 Slack 노드로 보낸다.</p>
<p>여기서 워크플로우가 갈라진다. LLM 출력 하나가 두 갈래로 나뉘는 것이다. 한 갈래는 Split Out → 이슈 등록으로, 다른 갈래는 곧장 Slack으로. Slack 노드의 메시지를 표현식으로 구성한다.</p>
<p>Slack을 처음 연결할 때 한 가지 짚어둘 게 있다. 셀프호스팅 n8n에서 Slack을 붙이는 방식은 OAuth2와 Access Token 두 가지인데, OAuth2로 가려면 Slack에서 앱을 하나 만들어야 한다. api.slack.com/apps에서 앱을 만들고, n8n 자격증명 화면에 표시된 리다이렉트 URL을 Slack 앱의 Redirect URLs에 등록한 뒤, <code>chat:write</code> 스코프를 부여하고 Client ID/Secret을 n8n에 넣는 흐름이다. 4편의 구글 OAuth와 구조가 똑같다. 한 번 해봤다고 두 번째는 확실히 빨랐다. (Access Token 방식으로 <code>xoxb-</code> 봇 토큰만 넣는 더 간단한 길도 있으니, OAuth 설정이 번거로우면 그쪽을 택해도 된다.) 연결 후에는 메시지를 보낼 채널에 봇을 초대해두는 걸 잊지 말자.</p>
<pre><code>📋 *회의 요약*
{{ $json.summary }}

*결정사항*
{{ $json.decisions.join(&quot;\n- &quot;) }}

액션아이템 {{ $json.actionItems.length }}건은 이슈로 등록되었습니다.</code></pre><p><code>decisions</code>는 배열이니 <code>join</code>으로 줄바꿈해 붙이고, 액션아이템은 개수만 알려준다(상세는 이슈에 있으니까). Slack에 이 메시지가 뜨면, 회의에 못 들어온 사람도 결정사항과 할일 현황을 한눈에 본다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/cf151efc-7e24-4401-bda0-2da73091a69a/image.png" alt=""></p>
<h2 id="실패를-조용히-넘기지-않기">실패를 조용히 넘기지 않기</h2>
<p>5편에서 예고한 <strong>silent failure 방지</strong>를 여기서 마무리한다. 자동화가 신뢰를 얻으려면, 실패했을 때 실패했다고 말해줘야 한다.</p>
<p>이게 왜 필수인지 만들면서 몸으로 겪었다. 실행하다 이런 에러를 만난 것이다.</p>
<pre><code>[503 Service Unavailable] This model is currently experiencing high demand.
consider setting this node to retry automatically</code></pre><p>내 설정이 틀린 게 아니라, Google의 LLM 서버가 순간적으로 과부하라 응답을 못 준 것이었다. 잠시 후 다시 실행하니 멀쩡히 됐다. 문제는 <strong>실제 운영에선 내가 지켜보고 있지 않다</strong>는 점이다. 새벽에 회의록이 올라왔는데 마침 외부 API가 잠깐 죽어 있으면, 재시도 장치가 없는 한 그 회의록은 조용히 누락된다. 에러 메시지조차 &quot;자동 재시도를 설정하라&quot;고 대놓고 알려주고 있었다.</p>
<p>그래서 두 가지를 넣었다. 하나는 각 노드 Settings의 <strong>Retry On Fail</strong>로 일시적 오류(네트워크, API 순간 장애, 방금 그 503)에 자동 재시도. 다른 하나는 <strong>에러 브랜치</strong>다. n8n은 노드가 실패했을 때 흐를 별도 출력(On Error → Continue using error output)을 만들 수 있는데, 이 에러 갈래를 <strong>&quot;처리 실패&quot; Slack 알림</strong>으로 연결했다.</p>
<p>이러면 회의록 처리가 실패해도 그냥 사라지지 않고, &quot;OO 회의록 처리 중 오류 발생&quot;이라는 알림이 온다. 회의록이 조용히 누락되는 일이 없어진다. 애초에 이 프로젝트를 시작한 이유가 &quot;결정이 누락되는 걸 못 견뎌서&quot;였는데, 그 자동화 자체가 조용히 누락되면 앞뒤가 안 맞으니까.</p>
<h2 id="완성된-파이프라인">완성된 파이프라인</h2>
<p>최종 워크플로우는 이렇게 생겼다.</p>
<pre><code>Google Drive Trigger (새 회의록)
  → IF (Google Docs 문서만 통과)
  → Google Docs (텍스트 읽기)
  → Basic LLM Chain (+ Auto-fixing Parser)  → 구조화된 JSON
      ├─→ Slack (요약·결정사항 발송)
      └─→ Split Out (actionItems 펼치기)
            → 이슈 생성 (아이템마다 하나씩)
      [에러 발생 시] → Slack (실패 알림)</code></pre><p>회의가 끝나면, 아무도 손대지 않아도 요약이 Slack에 뜨고 할 일이 이슈로 등록된다. 6편에 걸쳐 만든 것이 이 한 흐름이다.</p>
<h2 id="오늘-막혔던-것--메모">오늘 막혔던 것 / 메모</h2>
<ul>
<li><strong>외부 연동은 OAuth보다 토큰이 편할 때가 많다</strong> — GitHub는 Personal Access Token, Slack은 OAuth2와 Bot Token(<code>xoxb-</code>) 중 선택. 셀프호스팅에선 리다이렉트 설정이 없는 토큰 방식이 대체로 빠르다.</li>
<li><strong>Slack 봇을 채널에 초대해야 함</strong> — 연결만 하고 채널에 초대 안 하면 메시지 발송이 실패한다.</li>
<li><strong>일시적 503은 반드시 만난다</strong> — LLM API도 외부 서비스라 순간 과부하로 죽을 수 있다. Retry On Fail은 선택이 아니라 필수.</li>
<li><strong>Split Out 경로 확인</strong> — 펼칠 배열의 정확한 경로(<code>actionItems</code>인지 그 상위를 거치는지)를 INPUT 패널에서 확인해야 빈 결과를 피한다.</li>
</ul>
<h2 id="마지막-관문-실제로-자동으로-돌게-하기">마지막 관문: 실제로 자동으로 돌게 하기</h2>
<p>여기까지는 <strong>Fetch Test Event</strong>와 <strong>Execute step</strong>으로 수동 실행하며 만들었다. 실제로 회의 후 자동으로 돌게 하려면 두 가지가 남는다.</p>
<p><strong>하나, 워크플로우 활성화.</strong> 4편에서 짚었듯 트리거는 워크플로우가 Active 상태여야 자동으로 돈다. 우측 상단에서 활성화한다.</p>
<p><strong>둘, 24시간 켜져 있는 환경.</strong> Drive 트리거는 폴링이라, 내 로컬 도커가 꺼져 있으면 그동안의 회의록을 놓친다. 그래서 실제 운용하려면 항상 켜진 서버가 필요하다. 나는 로컬에서 개발을 끝낸 뒤 클라우드로 옮길 계획이다. 이 배포 과정은 그 자체로 이야깃거리라, 별도 글로 다루려 한다.</p>
<h2 id="시리즈를-마치며--코드로-자동화하던-사람이-배운-것">시리즈를 마치며 — 코드로 자동화하던 사람이 배운 것</h2>
<p>0편에서 나는 &quot;코드로 자동화하던 QA 엔지니어가 왜 노코드를 배우는가&quot;라는 질문으로 시작했다. 여섯 편을 지나 답을 정리할 수 있게 됐다.</p>
<p><strong>노코드는 코드의 하위 호환이 아니다.</strong> 처음엔 &quot;이거 그냥 코드로 짜면 되는데&quot;라는 생각을 계속했다. 실제로 단발성 스크립트라면 코드가 빠르다. 하지만 트리거, 인증, 재시도, 분기, 배열 처리, LLM 파싱 복구 같은 <strong>주변 뼈대</strong>를 매번 직접 만드는 비용을 n8n이 걷어가자, 나는 로직과 설계에만 집중할 수 있었다. 이번 프로젝트에서 내가 실제로 고민한 건 노드 배치가 아니라 &quot;무엇을 검증하고, 어떤 경계 조건을 정의하고, 실패를 어떻게 드러낼 것인가&quot;였다. 그건 QA 경험 그 자체였다.</p>
<p><strong>동시에, 노코드도 결국 이해가 필요하다.</strong> OAuth의 복잡함(4편), LLM의 비결정성(5편), 아이템 단위 실행의 원리(3편)는 노코드라고 사라지지 않았다. 오히려 이 원리를 알아야 노드를 제대로 조립할 수 있었다. &quot;버튼만 누르면 되는&quot; 도구가 아니라, &quot;구조를 이해한 사람이 빠르게 조립하는&quot; 도구에 가까웠다.</p>
<p>그래서 이 시리즈의 진짜 결론은 이거다. <strong>자동화의 본질은 도구가 아니라 문제 정의에 있다.</strong> 코드로 짜든 노드로 잇든, &quot;회의에서 정한 것이 실행에서 누락된다&quot;는 문제를 발견하고, 그걸 해결하는 흐름을 설계하는 일은 똑같이 사람의 몫이다. 도구는 그 설계를 얼마나 빨리 현실로 만드느냐의 차이일 뿐이다.</p>
<p>여기까지 긴 시리즈를 따라와 주셔서 감사합니다. 처음 n8n을 켜던 0편의 나보다, 지금은 조금 더 넓은 자동화의 지도를 갖게 된 것 같다. 다음엔 이 파이프라인을 실제 서비스 데이터에 붙여보는 이야기로 찾아오겠습니다. 🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[회의록 자동화 만들기 2: LLM에게 액션아이템을 시키다 (5편)]]></title>
            <link>https://velog.io/@00kang_jh/%ED%9A%8C%EC%9D%98%EB%A1%9D-%EC%9E%90%EB%8F%99%ED%99%94-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-LLM%EC%97%90%EA%B2%8C-%EC%95%A1%EC%85%98%EC%95%84%EC%9D%B4%ED%85%9C%EC%9D%84-%EC%8B%9C%ED%82%A4%EB%8B%A4-5%ED%8E%B8</link>
            <guid>https://velog.io/@00kang_jh/%ED%9A%8C%EC%9D%98%EB%A1%9D-%EC%9E%90%EB%8F%99%ED%99%94-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-LLM%EC%97%90%EA%B2%8C-%EC%95%A1%EC%85%98%EC%95%84%EC%9D%B4%ED%85%9C%EC%9D%84-%EC%8B%9C%ED%82%A4%EB%8B%A4-5%ED%8E%B8</guid>
            <pubDate>Thu, 16 Jul 2026 06:53:39 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>n8n 학습기 시리즈 — 5편. 토이 프로젝트 (2): LLM으로 요약과 액션아이템 뽑기</strong>
4편에서 회의록 텍스트가 워크플로우 안으로 들어왔다. 이제 그 텍스트를 LLM에 넘겨, 사람이 읽을 요약과 <strong>기계가 처리할 수 있는 구조화된 액션아이템</strong>을 뽑아낸다. 그런데 LLM은 생각보다 말을 안 들었다.</p>
</blockquote>
<h2 id="들어가며">들어가며</h2>
<p>4편의 결과물은 &quot;회의 원문 대화가 담긴 한 덩어리의 텍스트&quot;였다. 사람이 읽을 순 있지만, 그것만으로는 아무 일도 일어나지 않는다. 우리 목표는 요약을 넘어 <strong>액션아이템을 이슈 트래커에 자동 등록</strong>하는 것이다(6편). 그러려면 텍스트가 아니라 <strong>데이터</strong>가 필요하다.</p>
<p>즉 이번 편의 진짜 과제는 &quot;LLM에게 요약을 시키는 것&quot;이 아니다. <strong>LLM이라는 비결정적인 녀석에게서, 다음 노드가 믿고 쓸 수 있는 형태의 출력을 안정적으로 받아내는 것</strong>이다. QA 하던 입장에서 보면 익숙한 문제다. 신뢰할 수 없는 컴포넌트의 출력을 어떻게 검증하고 방어할 것인가. 그리고 이 편을 쓰는 내내, 정확히 그 문제로 씨름했다.</p>
<h2 id="두-가지-출력이-필요하다">두 가지 출력이 필요하다</h2>
<p>회의록에서 뽑을 건 성격이 다른 두 가지다.</p>
<ul>
<li><strong>사람이 읽을 것</strong> — 요약, 결정사항. Slack에 그대로 붙여넣을 자연어.</li>
<li><strong>기계가 처리할 것</strong> — 액션아이템. 이슈로 등록하려면 담당자·할일·기한·우선순위가 <strong>필드로 분리</strong>돼 있어야 한다.</li>
</ul>
<p>후자가 이 프로젝트의 핵심이다. &quot;회의 요약 봇&quot;은 앞의 것만 하고 끝나지만, 우리는 뒤의 것까지 가야 한다. 그래서 원하는 출력은 이런 모양이다.</p>
<pre><code class="language-json">{
  &quot;summary&quot;: &quot;로그인 버그 수정과 회원가입 리뉴얼 일정을 논의함&quot;,
  &quot;decisions&quot;: [&quot;회원가입 페이지 리뉴얼은 다음 스프린트로 연기&quot;],
  &quot;actionItems&quot;: [
    {
      &quot;assignee&quot;: &quot;홍길동&quot;,
      &quot;task&quot;: &quot;로그인 버그 수정&quot;,
      &quot;dueDate&quot;: &quot;2026-07-17&quot;,
      &quot;priority&quot;: &quot;high&quot;
    }
  ]
}</code></pre>
<p>이 형태로 나오면 6편에서 <code>actionItems</code> 배열을 순회하며 이슈를 하나씩 만들 수 있다. 3편에서 배운 아이템 단위 실행이 거기서 쓰인다.</p>
<h2 id="n8n의-ai-노드-구조">n8n의 AI 노드 구조</h2>
<p>n8n에서 LLM을 쓰는 방식은 처음 보면 낯설다. 노드 하나가 아니라 <strong>여러 노드가 붙어서 한 덩어리</strong>로 동작한다.</p>
<ul>
<li><strong>Basic LLM Chain</strong> — 루트 노드. 프롬프트를 정의하고 결과를 내보낸다.</li>
<li><strong>Chat Model</strong> — 서브노드. 어떤 모델을 쓸지와 API 키를 설정한다. LLM Chain 아래에 매단다.</li>
<li><strong>Structured Output Parser</strong> — 서브노드. 출력을 정해진 JSON 스키마로 강제한다.</li>
</ul>
<p>AI Agent 노드와 헷갈릴 수 있는데, Agent는 도구를 호출하고 여러 단계로 추론하는 물건이고 <strong>Basic LLM Chain은 프롬프트 하나 넣고 응답 하나 받는</strong> 단순한 물건이다. 우리 목적(텍스트 → 구조화된 데이터)엔 Chain이 맞다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/a0cb4dc9-c236-4edd-98fc-3548fa26a1ae/image.png" alt=""></p>
<h2 id="첫-번째-벽-모델-이름이-계속-죽어-있다">첫 번째 벽: 모델 이름이 계속 죽어 있다</h2>
<p>Chat Model로 Gemini를 골랐다. 무료 티어가 있고, 4편에서 이미 구글 생태계에 들어와 있으니 자연스러운 선택이었다. Google AI Studio에서 API 키를 발급받아 연결했다. 여기까지는 순조로웠다. 문제는 모델 선택이었다.</p>
<p>튜토리얼을 보고 <code>gemini-2.0-flash</code>를 골랐더니 이런 에러가 났다.</p>
<pre><code>[429] Quota exceeded ... limit: 0, model: gemini-2.0-flash</code></pre><p><code>limit: 0</code>. 무료 할당이 초과된 게 아니라 아예 0이었다. 알고 보니 이 모델은 이미 <strong>폐기되어 퇴역</strong>한 상태였다. 그럼 다음 버전, 하고 <code>gemini-2.5-flash</code>로 바꿨다.</p>
<pre><code>[404] This model is no longer available to new users.</code></pre><p>이번엔 &quot;신규 사용자에겐 제공 안 함&quot;이었다. 나는 방금 키를 발급받은 신규 사용자였다. 결국 당시 신규 사용자가 쓸 수 있는 최신 Flash 계열(<code>gemini-3-flash-preview</code>)로 바꾸고 나서야 응답이 왔다.</p>
<p>여기서 얻은 교훈은 기술적인 게 아니라 습관에 관한 것이다. <strong>LLM 모델명은 몇 달 단위로 죽고 태어난다.</strong> 어제 글에 나온 모델이 오늘 404가 된다. 그래서 특정 모델명을 외우는 건 의미가 없고, <strong>Google AI Studio에서 지금 내 계정으로 실제 호출되는 모델이 뭔지 확인하는 습관</strong>을 들이는 게 맞다. AI Studio 드롭다운에 뜨는 모델은 같은 키로 n8n에서도 된다. (이 글을 읽는 시점엔 또 다른 이름일 것이다. 그러니 이름 말고 이 방법을 기억하길.)</p>
<h2 id="프롬프트-설계">프롬프트 설계</h2>
<p>모델이 응답하기 시작하자 본격적인 작업이 시작됐다. Basic LLM Chain의 프롬프트에 4편에서 읽어온 회의록 텍스트를 표현식으로 꽂고(2편 문법), 시스템 메시지에 지시를 넣었다. 지키려 한 원칙이 몇 가지 있다.</p>
<p><strong>하나, 역할과 작업을 분리한다.</strong> 시스템 메시지엔 지속적인 지시(&quot;너는 회의록에서 실행 가능한 항목을 추출하는 도우미다&quot;)를, 사용자 메시지엔 매번 달라지는 회의록 본문을 넣는다.</p>
<p><strong>둘, 출력 형식을 예시로 보여준다.</strong> 스키마를 말로 설명하는 것보다 원하는 출력의 실제 예시를 넣는 게 훨씬 잘 먹힌다.</p>
<p><strong>셋, 애매한 경우의 규칙을 미리 정한다.</strong> 회의록은 깔끔하지 않다.</p>
<ul>
<li>담당자가 명시되지 않은 할일은? → <code>assignee</code>를 <code>null</code>로</li>
<li>&quot;다음 주까지&quot; 같은 상대 표현은? → 회의 날짜 기준으로 계산해 <code>YYYY-MM-DD</code>로</li>
<li>액션아이템이 없는 회의는? → 빈 배열 <code>[]</code> (억지로 만들지 말 것)</li>
<li>&quot;미루기로 결정&quot; 같은 건? → 액션아이템이 아니라 <code>decisions</code>로</li>
</ul>
<p>이런 경계 조건을 명시하지 않으면 LLM은 매번 다르게 처리한다. <strong>테스트 케이스를 설계할 때 경계값부터 잡던 습관이 프롬프트 설계에도 그대로 쓰였다.</strong></p>
<p><strong>넷, 어떤 부분을 근거로 삼을지 못 박는다.</strong> 실제 회의록을 열어보니 Google Workspace가 <strong>한 문서 안에 자동 요약과 원문 스크립트를 두 탭으로</strong> 넣어줬고, Docs 노드로 읽으면 둘이 섞여 들어왔다. 텍스트를 물리적으로 자르는 건 구조 변화에 약하니, 프롬프트로 통제했다. &quot;요약은 무시하고 원문 스크립트만 근거로 삼아라. 원문에 근거가 없는 내용은 포함하지 마라.&quot; 자동 요약은 Google이 이미 한 번 가공한 2차 정보다. <strong>신뢰할 수 있는 원본과 파생물을 구분해 원본을 근거로 삼게</strong> 한 것이다.</p>
<p>여기서 한 번 짚고 갈 질문. Google이 이미 요약해주는데 왜 직접 뽑나? 답은 <strong>형식의 통제권</strong>이다. Google의 요약은 사람이 읽기 좋은 자연어지만, 이슈로 자동 등록하려면 <code>{담당자, 할일, 기한}</code>으로 필드가 분리된 데이터가 필요하다. 그 필드와 규칙을 내 업무에 맞게 통제하려면, 범용 요약이 아니라 내 워크플로우 전용 추출이 필요하다. 이게 이 프로젝트가 존재하는 이유다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/319530ee-47f9-46b3-ab4e-96c2f9cd3564/image.png" alt=""></p>
<h2 id="두-번째-벽-llm은-약속을-안-지킨다">두 번째 벽: LLM은 약속을 안 지킨다</h2>
<p>프롬프트를 공들여 짜고 실행했더니 이 에러가 떴다.</p>
<pre><code>Model output doesn&#39;t fit required format</code></pre><p>모델이 응답은 했는데, 그 응답이 Structured Output Parser가 요구하는 JSON 형식에 안 맞는다는 뜻이다. 출력을 뜯어보니 원인은 매번 조금씩 달랐다. 어떤 날은 <code>```json</code> 코드펜스로 감쌌고, 어떤 날은 &quot;네, 다음과 같이 정리했습니다&quot; 같은 말을 앞에 붙였고, 어떤 날은 값이 없는 필드를 아예 빼버렸다.</p>
<p><strong>중요한 건 이게 버그가 아니라 특성이라는 점이다.</strong> 언어 모델은 본질적으로 비결정적이라, 온도를 0으로 낮춰도 출력이 흔들린다. 그래서 &quot;JSON으로만 답해&quot;라고 아무리 적어도 완벽히 지켜지지 않는다. QA를 해본 사람에게 이 패턴은 익숙하다. <strong>가끔 실패하는 게 항상 실패하는 것보다 훨씬 위험하다.</strong> 20번 잘 되다 21번째에 깨지는 컴포넌트가 제일 무섭다.</p>
<h2 id="방어선-1--프롬프트를-더-강하게">방어선 1 — 프롬프트를 더 강하게</h2>
<p>첫 번째 대응은 프롬프트 강화였다. 시스템 메시지 끝에 출력 형식 규칙을 <strong>독립된 블록으로 분리</strong>해서 못 박았다.</p>
<ul>
<li>유효한 JSON 객체 하나만 출력한다</li>
<li>코드블록 표시(```)를 붙이지 마라</li>
<li>JSON 앞뒤에 인사말이나 설명을 붙이지 마라</li>
<li><strong>값이 없는 필드는 생략하지 말고 반드시 null을 넣어라</strong></li>
<li>모든 문자열은 큰따옴표를 쓴다</li>
</ul>
<p>특히 &quot;필드를 생략하지 말고 null&quot;이 중요했다. Structured Output Parser를 JSON 예시로 생성하면 <strong>모든 필드가 필수</strong>로 잡히는데, LLM이 <code>dueDate</code>가 없다고 그 필드를 빼면 &quot;형식 불일치&quot;로 실패하기 때문이다. 값이 없어도 null을 채우게 하면 이 문제가 준다.</p>
<p>이걸로 성공률이 확 올라갔다. 하지만 0이 되지는 않았다.</p>
<h2 id="방어선-2--auto-fixing-output-parser">방어선 2 — Auto-fixing Output Parser</h2>
<p>프롬프트로 아무리 조여도 가끔은 형식을 어겼다. 결국 프롬프트만으로는 못 잡는다는 걸 인정하고, 두 번째 방어선을 얹었다. <strong>Auto-fixing Output Parser</strong>다.</p>
<p>이 노드는 파싱이 실패하면, <strong>그 실패한 출력을 LLM에게 다시 보내 고쳐오게</strong> 한다. 구성이 조금 특이해서 정리해둔다.</p>
<pre><code>Basic LLM Chain
 └─ (Model)         → Chat Model          [본 작업용]
 └─ (Output Parser) → Auto-fixing Output Parser
                        ├─ (Model)  → Chat Model            [고칠 때용]
                        └─ (Parser) → Structured Output Parser  [스키마]</code></pre><p>핵심은 <strong>Structured Output Parser를 LLM Chain에 직접 붙이지 않고, Auto-fixing을 중간에 끼운다</strong>는 것이다. 스키마 검증은 여전히 Structured Output Parser가 하고, Auto-fixing은 그게 실패했을 때 재요청을 맡는다.</p>
<p>비용이 걱정될 수 있는데, <strong>파싱이 실패했을 때만</strong> 추가 호출이 일어난다. 1차에서 잘 나오면 추가 비용은 없다.</p>
<p>QA 관점에서 이 구조가 마음에 들었다. 1차 검증(스키마) → 실패 시 자동 복구(재요청) → 재검증. <strong>재시도 로직을 내가 짜지 않아도 되는 것</strong>, 이게 n8n이 대신 해주는 뼈대의 좋은 예다. 코드로 짰다면 응답을 파싱하고, 실패를 잡고, 프롬프트를 다시 구성해 재호출하는 걸 전부 손으로 만들었을 것이다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/a8f738fc-1961-45fc-944b-ab1526ff7d2e/image.png" alt=""></p>
<h2 id="방어선-3--그래도-실패하면-조용히-넘기지-않기">방어선 3 — 그래도 실패하면 조용히 넘기지 않기</h2>
<p>두 겹을 쌓아도 실패는 0이 안 된다. 그래서 세 번째 원칙은 <strong>&quot;조용히 실패하지 않게 하는 것&quot;</strong>이다.</p>
<p>n8n 노드 Settings에 <strong>On Error</strong> 옵션이 있다. 이걸 <code>Continue</code>로 두면 실패해도 워크플로우가 멈추지 않는다. 편해 보이지만, 아무 장치 없이 이것만 켜면 <strong>회의록이 처리 안 됐는데 아무도 모르는</strong> 상황이 된다. QA에서 제일 싫어하는 게 이 <strong>silent failure</strong>다.</p>
<p>그래서 방향은 이렇다. 일시적 오류엔 <strong>Retry On Fail</strong>로 재시도하되, 최종 실패는 넘기지 말고 <strong>실패했다는 사실을 알림으로 보낸다</strong>(6편에서 구현). 자동화가 신뢰를 얻으려면, 실패했을 때 실패했다고 말해줘야 한다.</p>
<h2 id="실행해보니">실행해보니</h2>
<p>여러 방어선을 쌓고 테스트용 회의록을 넣었다.</p>
<blockquote>
<p>&quot;다음 주 금요일까지 로그인 버그 수정하기로 했고 담당은 OOO입니다. 회원가입 페이지 리뉴얼은 다음 스프린트로 미루기로 결정했습니다. 디자인 시안은 수요일까지 공유하기로 했습니다.&quot;</p>
</blockquote>
<p>출력에 <code>summary</code>, <code>decisions</code>, <code>actionItems</code>가 스키마대로 안정적으로 들어왔다. &quot;다음 주 금요일&quot;이 실제 날짜로 변환되고, &quot;미루기로 결정&quot;이 액션아이템이 아니라 <code>decisions</code>로 분류되고, 요약 탭이 아니라 원문 기준으로 뽑힌 게 확인됐다. 프롬프트에 규칙을 명시하고 방어선을 쌓은 효과다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/90eb8921-4265-485e-8a25-9375f988d8e9/image.png" alt=""></p>
<h2 id="코드로-자동화하던-관점에서">코드로 자동화하던 관점에서</h2>
<ul>
<li>Basic LLM Chain → LLM API를 호출하는 함수</li>
<li>Chat Model 서브노드 → API 키와 모델을 주입하는 것</li>
<li>Structured Output Parser → 응답 스키마 검증(JSON Schema 밸리데이션)</li>
<li>Auto-fixing Parser → 검증 실패 시 재요청하는 재시도 로직</li>
</ul>
<p>코드로 짰다면 이 네 가지를 다 직접 만들었을 것이다. 특히 파싱 실패 시 재요청 로직은 매번 새로 짜기 귀찮다. 그걸 서브노드 하나로 대신하는 건 확실히 편했다.</p>
<p>다만 <strong>프롬프트 설계와 경계 조건 정의는 누구도 대신해주지 않는다.</strong> 노코드든 코드든, &quot;무엇을 검증할 것인가&quot;를 정하는 건 사람의 일이다. 이 부분에서 QA 경험이 그대로 쓰였다. 결국 이번 편의 진짜 작업은 노드 배치가 아니라, <strong>믿을 수 없는 출력을 믿을 수 있게 만드는 설계</strong>였다.</p>
<h2 id="오늘-막혔던-것--메모">오늘 막혔던 것 / 메모</h2>
<ul>
<li><strong>모델명이 죽어 있음</strong> — <code>gemini-2.0-flash</code>는 퇴역(<code>limit: 0</code>), <code>gemini-2.5-flash</code>는 신규 사용자 차단(404). 모델명은 자주 바뀌니 AI Studio에서 지금 쓸 수 있는 걸 확인하는 게 답이다.</li>
<li><strong>출력이 형식을 안 지킴</strong> — 코드펜스, 앞뒤 잡담, 필드 누락. 프롬프트로 못 박되, &quot;값 없으면 null&quot; 지시가 특히 효과적이었다.</li>
<li><strong>프롬프트로도 0%는 안 됨</strong> — 결국 Auto-fixing Output Parser로 자동 복구를 얹어야 안정된다.</li>
<li><strong>회의록에 요약 탭이 딸려 옴</strong> — 원문과 요약이 한 문서에. 자르지 말고 &quot;원문만 근거로&quot;를 프롬프트로 지시하는 게 낫다.</li>
<li><strong>스키마가 너무 빡빡하면</strong> — JSON 예시로 만들면 모든 필드가 필수가 된다. 계속 실패하면 JSON Schema 직접 정의로 바꿔 필수 필드를 최소화한다.</li>
</ul>
<h2 id="마치며">마치며</h2>
<p>5편의 결론은 이렇다. <strong>LLM에게 일을 시키는 것보다, LLM의 출력을 믿을 수 있게 만드는 것이 더 어렵고 더 중요하다.</strong> 프롬프트로 형식을 못 박고, Structured Output Parser로 스키마를 강제하고, Auto-fixing으로 복구하고, 실패는 조용히 넘기지 않는다. 이게 비결정적 컴포넌트를 실무에 쓰는 방법이다.</p>
<p>이제 손에 구조화된 액션아이템 데이터가 들어왔다. 다음 6편에서는 이걸 <strong>Slack 요약 발송과 이슈 자동 등록</strong>으로 연결한다. 3편에서 배운 아이템 단위 실행이 진가를 발휘한다. 액션아이템 배열이 아이템으로 쪼개져, 각각이 이슈 하나가 되는 것이다. 요약만 하는 봇과 실행까지 이어지는 자동화의 차이가, 마지막 편에서 완성된다.</p>
<p>읽어주셔서 감사합니다. 🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[회의록 자동화 만들기 1: 회의록을 n8n 안으로 가져오기 (4편)]]></title>
            <link>https://velog.io/@00kang_jh/%ED%9A%8C%EC%9D%98%EB%A1%9D-%EC%9E%90%EB%8F%99%ED%99%94-%EB%A7%8C%EB%93%A4%EA%B8%B0-1-%ED%9A%8C%EC%9D%98%EB%A1%9D%EC%9D%84-n8n-%EC%95%88%EC%9C%BC%EB%A1%9C-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0-4%ED%8E%B8</link>
            <guid>https://velog.io/@00kang_jh/%ED%9A%8C%EC%9D%98%EB%A1%9D-%EC%9E%90%EB%8F%99%ED%99%94-%EB%A7%8C%EB%93%A4%EA%B8%B0-1-%ED%9A%8C%EC%9D%98%EB%A1%9D%EC%9D%84-n8n-%EC%95%88%EC%9C%BC%EB%A1%9C-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0-4%ED%8E%B8</guid>
            <pubDate>Mon, 13 Jul 2026 07:39:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>n8n 학습기 시리즈 — 4편. 토이 프로젝트 (1): 데이터 진입점 뚫기</strong>
개념 세 편(데이터 흐름, 표현식, 제어 흐름)을 지나 이제 실전이다. 이번 편의 목표는 단 하나, 실제 회의록이 n8n 워크플로우 안으로 들어오게 만드는 것이다.</p>
</blockquote>
<h2 id="들어가며">들어가며</h2>
<p>회의는 잘 한다. 문제는 그 다음이다. &quot;이건 누가, 언제까지&quot;까지 정해놓고도, 회의가 끝나면 결정사항이 흐지부지 흩어진다. 누군가 정리해 공유하면 다행이지만 그것도 매번 손이 간다.</p>
<p>QA를 하면서 몸에 밴 감각이 하나 있다. <strong>결정된 것이 실행에서 누락되는 지점</strong>을 못 견디는 것. 테스트 케이스가 빠지면 버그가 새듯, 회의에서 정한 액션아이템이 빠지면 일이 샌다. 그래서 이걸 자동화해보기로 했다. 지금까지 배운 n8n으로.</p>
<h2 id="무엇을-만드나">무엇을 만드나</h2>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/6408482f-fef3-4653-8ac6-74d1640320f7/image.png" alt=""></p>
<p>만들려는 파이프라인은 이렇다.</p>
<p><strong>화상 회의가 끝난다 → 회의록이 자동 저장된다 → n8n이 감지해 읽는다 → LLM이 요약과 액션아이템을 뽑는다 → 요약은 Slack으로, 액션아이템은 이슈로 자동 등록된다.</strong></p>
<p>단순한 &quot;회의 요약 봇&quot;에서 멈추지 않고, 뽑아낸 액션아이템을 담당자·기한이 있는 구조로 정리해 실제 이슈 트래커까지 자동 등록하는 것까지가 목표다. <strong>요약만 하는 봇과 실행까지 이어지는 자동화의 차이</strong>가 거기서 갈린다.</p>
<p>전체를 한 편에 담기엔 크니 이렇게 나눈다.</p>
<ul>
<li><strong>4편 (이 글)</strong> — 회의록을 n8n 안으로 가져오기</li>
<li><strong>5편</strong> — LLM으로 요약과 액션아이템 뽑기</li>
<li><strong>6편</strong> — Slack 알림 + 이슈 자동 등록으로 실행까지 연결</li>
</ul>
<h2 id="회의록은-어디서-오나">회의록은 어디서 오나</h2>
<p>데이터 소스부터. 화상 회의는 Google Meet을 쓴다. Meet은 회의 중 트랜스크립트를 켜두면, 회의가 끝난 뒤 대화 내용을 <strong>Google Docs 문서로 만들어 주최자의 Drive에 자동 저장</strong>해준다. 이게 파이프라인의 출발 데이터다.</p>
<p>두 가지를 짚어둔다. 이 자동 트랜스크립트 기능은 자격이 되는 Google Workspace 구독이 필요하다(개인 무료 Gmail로는 안 된다). 그리고 트랜스크립트는 <strong>회의를 주최한 계정의 Drive</strong>에 저장되므로, 회의는 그 계정으로 여는 게 좋다.</p>
<p>한 가지 의도적인 선택도 있다. 나는 Google이 만들어주는 AI 요약이 아니라 <strong>대화 원문 트랜스크립트</strong>를 쓴다. 요약은 5편에서 LLM에게 시킬 일이라, Google이 미리 요약해버리면 이 프로젝트의 재미가 사라진다.</p>
<h2 id="첫-번째-관문-셀프호스팅-n8n에-구글-붙이기">첫 번째 관문: 셀프호스팅 n8n에 구글 붙이기</h2>
<p>여기가 이번 편에서 제일 험한 구간이다. 실제로 4편 작업 시간의 대부분을 여기서 썼다.</p>
<p>n8n Cloud라면 &quot;Sign in with Google&quot; 버튼 하나로 끝난다. 그런데 도커로 <strong>셀프호스팅</strong>하는 경우엔 그게 안 된다. Google Cloud Console에서 <strong>직접 OAuth 앱을 만들어</strong> n8n에 연결해야 한다.</p>
<p>순서가 중요하다. 구글부터 가면 두 번 왔다갔다 하게 되니, <strong>n8n에서 리다이렉트 URL을 먼저 확보하고</strong> 구글로 넘어가는 게 낫다.</p>
<ol>
<li>n8n에서 Google Drive Trigger 노드를 추가하고, 자격증명 생성 화면을 연다. 여기 표시되는 <strong>OAuth Redirect URL</strong>을 복사한다(<code>.../rest/oauth2-credential/callback</code> 형태).</li>
<li>Google Cloud Console에서 프로젝트를 만들고, <strong>Google Drive API</strong>와 <strong>Google Docs API</strong>를 켠다.</li>
<li>OAuth 동의 화면을 구성한다. Workspace 계정이면 User Type을 <strong>Internal</strong>로 할 수 있는데, 이게 훨씬 편하다(테스트 유저 등록도, 뒤에 말할 7일 만료 문제도 없다).</li>
<li>OAuth 클라이언트 ID(Web application)를 만들고, <strong>Authorized redirect URIs에 1번에서 복사한 URL을 붙여넣는다.</strong></li>
<li>발급된 <strong>Client ID와 Client Secret</strong>을 n8n 자격증명에 입력하고 연결한다.</li>
</ol>
<p>로컬 도커 사용자에게 다행인 점 하나. <strong>공개 도메인이나 SSL, 포트포워딩이 없어도 된다.</strong> 구글이 개발 목적의 localhost 리다이렉트를 허용해주기 때문이다.</p>
<h2 id="두-번째-관문-자격증명은-서비스마다-따로다">두 번째 관문: 자격증명은 서비스마다 따로다</h2>
<p>여기서 내가 헛다리를 짚었다.</p>
<p>Drive 연결을 끝내고 뿌듯한 마음으로 Google Docs 노드를 붙였는데, 실행하자마자 이런 에러가 떴다.</p>
<pre><code>Node does not have any credentials set</code></pre><p>표현식도 제대로 걸었고 문서 ID도 미리보기에 잘 뜨는데 왜? 알고 보니 <strong>n8n에서 Google Drive와 Google Docs는 서로 다른 자격증명 타입</strong>이었다. Drive Trigger에 붙인 &quot;Google Drive account&quot;는 Docs 노드에서 쓸 수 없다. Docs는 <strong>Google Docs OAuth2 API</strong> 자격증명을 따로 요구한다.</p>
<p>당황할 필요는 없다. <strong>Google Cloud Console에서 앱을 새로 만들 필요는 없기 때문이다.</strong> 같은 OAuth 앱의 Client ID와 Secret을 그대로 다시 입력하고 로그인만 한 번 더 하면 된다. 요컨대 <strong>구글 쪽 앱은 하나, n8n 쪽 자격증명은 서비스별로 하나씩</strong>이다. 이 구분을 몰라서 &quot;분명 연결했는데 왜 없다고 하지?&quot;로 한참 헤맸다.</p>
<p>(그래서 2번 단계에서 Docs API도 미리 켜두라고 적었다. Drive API만 켜두면 로그인은 되는데 실행 시점에 API not enabled로 또 막힌다.)</p>
<h2 id="drive-트리거로-새-회의록-감지하기">Drive 트리거로 새 회의록 감지하기</h2>
<p>연결이 됐으면 트리거를 놓는다. <strong>Google Drive Trigger</strong> 노드는 지정한 폴더를 주기적으로 확인(폴링)해서 새 파일을 감지한다.</p>
<p>설정은 단순하다. Trigger On을 <strong>Changes Involving a Specific Folder</strong>로 두고, <strong>감시할 폴더</strong>를 지정하고, Watch For를 <strong>File Created</strong>로 고른다. Poll Times는 기본값(1분마다)이면 충분하다.</p>
<p>여기서 알아둘 점 몇 가지. My Drive 전체를 감시하면 아무 파일에나 반응하니 <strong>폴더를 콕 집어야</strong> 한다. 그리고 노드 안내문에 적혀 있듯 <strong>하위 폴더의 변경은 감지하지 않는다.</strong> 또 트리거가 실제로 자동 실행되려면 <strong>워크플로우가 활성화(Active) 상태여야</strong> 한다. 에디터에서 열어만 둔 상태로는 돌지 않는다. 개발 중엔 <strong>Fetch Test Event</strong> 버튼으로 최근 파일을 하나 가져와 테스트하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/0b55bc36-dc6b-414f-a66c-bd21fca57729/image.png" alt=""></p>
<h2 id="세-번째-관문-폴더엔-회의록만-있는-게-아니다">세 번째 관문: 폴더엔 회의록만 있는 게 아니다</h2>
<p>Fetch Test Event를 눌렀더니 파일 하나가 잡혔다. 그런데 열어보니 회의록이 아니었다.</p>
<pre><code class="language-json">&quot;name&quot;: &quot;...분석.docx&quot;,
&quot;mimeType&quot;: &quot;application/vnd.openxmlformats-officedocument.wordprocessingml.document&quot;</code></pre>
<p>내가 감시하도록 지정한 폴더에 예전에 넣어둔 <strong>Word(.docx) 파일</strong>이 있었던 것이다. 그리고 이건 그냥 실수가 아니라, <strong>실제 운영에서 반드시 만나는 문제</strong>다. 폴더는 회의록 전용이 아니고, 사람들은 거기에 아무 파일이나 넣는다.</p>
<p>문제가 되는 이유는 명확하다. Google Docs 노드는 <strong>네이티브 Google Docs 문서</strong>(<code>application/vnd.google-apps.document</code>)만 읽을 수 있다. Word 파일이 들어오면 그대로 터진다.</p>
<p>QA 하던 버릇이 발동했다. <strong>입력이 기대한 형식인지 먼저 검증해야 한다.</strong> 그래서 트리거 뒤에 3편에서 배운 <strong>IF 노드</strong>를 붙여, <code>mimeType</code>이 Google Docs인 아이템만 통과시키기로 했다.</p>
<pre><code>{{ $json.mimeType }}  →  is equal to  →  application/vnd.google-apps.document</code></pre><p>개념 편에서 배운 분기가 프로젝트에서 처음 실전으로 쓰이는 순간이었다. 3편의 IF가 여기서 이렇게 쓰일 줄은 그때는 몰랐다.</p>
<h2 id="회의록-내용-읽어오기">회의록 내용 읽어오기</h2>
<p>이제 감지한 파일에서 텍스트를 꺼낸다. <strong>Google Docs 노드</strong>의 Get 동작을 쓴다.</p>
<p>핵심은 <strong>Doc ID or URL</strong> 필드다. 여기에 고정값을 넣으면 안 된다. 트리거가 방금 감지한 <strong>그 파일</strong>을 읽어야 하기 때문이다. 그래서 필드를 Expression 모드로 바꾸고 이렇게 넣는다.</p>
<pre><code>{{ $json.id }}</code></pre><p>2편에서 배운 드래그앤드롭 매핑이 실전에서 처음 쓰이는 순간이다. 왼쪽 INPUT 패널에서 <code>id</code> 필드를 끌어다 놓으면 표현식이 자동 생성되고, 필드 아래 미리보기에 실제 문서 ID가 뜬다.</p>
<p>여기서 잠깐 헷갈릴 수 있다. 트리거가 넘겨주는 JSON에는 <code>permissionId</code>, <code>headRevisionId</code>, <code>thumbnailVersion</code> 등 id처럼 생긴 필드가 여럿 있다. 우리가 쓸 건 <strong>최상위의 <code>id</code></strong> 하나다. 이게 파일 자체의 고유 ID고, <code>webViewLink</code>의 <code>/document/d/</code> 뒤에 붙는 그 값과 같다.</p>
<p><strong>Execute step</strong>을 누르면 오른쪽 OUTPUT에 문서 내용이 들어온다. 노드의 <strong>Simplify</strong> 옵션을 켜두면 응답이 간소화되어 다루기 편하다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/54c3ca8c-484d-462b-be2d-256de6c4c55a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/666c0faf-dea4-4251-951d-890f1951bde1/image.png" alt=""></p>
<h2 id="여기까지의-확인">여기까지의 확인</h2>
<p>이번 편에서 만든 흐름은 이렇게 정리된다.</p>
<p><strong>Google Drive Trigger(새 파일 감지) → IF(회의록만 통과) → Google Docs(텍스트 읽기)</strong></p>
<p>실제로 회의를 한 번 하고 트랜스크립트를 켠 뒤, 그 파일이 폴더에 저장되고 n8n이 내용을 읽어오면 성공이다.</p>
<p>코드로 자동화하던 시절이라면 파일 시스템을 감시하고, OAuth 토큰을 갱신하고, Docs API를 파싱하는 코드를 직접 짰어야 할 부분이다. 그걸 노드 세 개로 대신했다. 다만 <strong>인증만큼은 &quot;노드 하나로 대신&quot;이 전혀 아니었다.</strong> 노코드 툴이라도 결국 OAuth의 복잡함은 그대로 마주해야 한다는 게 이번 편의 솔직한 소감이다.</p>
<h2 id="오늘-막혔던-것--메모">오늘 막혔던 것 / 메모</h2>
<ul>
<li><strong>자격증명이 서비스마다 별개</strong> — Drive에 연결했다고 Docs가 되는 게 아니다. 구글 쪽 OAuth 앱은 하나로 재사용하되, n8n 자격증명은 서비스마다 따로 만들어야 한다.</li>
<li><strong>API를 안 켜서 나는 에러</strong> — Drive API만 켜고 Docs API를 빠뜨리면, 연결은 되는데 실행에서 막힌다. 쓸 노드의 API는 미리 다 켜두자.</li>
<li><strong>redirect_uri_mismatch</strong> — n8n 자격증명 화면의 리다이렉트 URL과 Google Console에 등록한 URI가 한 글자라도 다르면 난다. 화면의 URL을 그대로 복사해 붙이는 게 안전하다.</li>
<li><strong>7일 뒤 연결이 풀림</strong> — OAuth 앱이 External + Testing 상태면 토큰이 약 7일 후 만료된다. Workspace 계정으로 Internal을 쓸 수 있으면 그게 편하다.</li>
<li><strong>엉뚱한 파일이 트리거를 깨움</strong> — 폴더엔 회의록만 있는 게 아니다. <code>mimeType</code> 검증은 선택이 아니라 필수다.</li>
<li><strong>트리거가 안 울림</strong> — 워크플로우가 비활성 상태이거나, 폴더 지정이 잘못됐거나 둘 중 하나였다.</li>
</ul>
<h2 id="마치며">마치며</h2>
<p><strong>셀프호스팅 n8n에 구글을 연결하고, Drive 트리거로 새 회의록을 감지해, mimeType으로 걸러낸 뒤, Docs 노드로 텍스트를 읽어온다.</strong> 인증 관문이 무겁지, 넘고 나면 흐름 자체는 노드 세 개로 단순하다.</p>
<p>이제 손에 회의 원문 텍스트가 들어왔다. 다음 5편에서는 이 텍스트를 LLM에 넣어, 요약과 결정사항, 그리고 담당자·기한이 있는 액션아이템을 <strong>구조화된 JSON</strong>으로 뽑아낸다. 2편에서 다진 표현식과 JSON 감각이 거기서 진짜로 쓰인다.</p>
<p>읽어주셔서 감사합니다. 🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[n8n 제어 흐름: 분기와 아이템 단위 실행 (3편)]]></title>
            <link>https://velog.io/@00kang_jh/n8n-%EC%A0%9C%EC%96%B4-%ED%9D%90%EB%A6%84-%EB%B6%84%EA%B8%B0%EC%99%80-%EC%95%84%EC%9D%B4%ED%85%9C-%EB%8B%A8%EC%9C%84-%EC%8B%A4%ED%96%89-3%ED%8E%B8</link>
            <guid>https://velog.io/@00kang_jh/n8n-%EC%A0%9C%EC%96%B4-%ED%9D%90%EB%A6%84-%EB%B6%84%EA%B8%B0%EC%99%80-%EC%95%84%EC%9D%B4%ED%85%9C-%EB%8B%A8%EC%9C%84-%EC%8B%A4%ED%96%89-3%ED%8E%B8</guid>
            <pubDate>Thu, 09 Jul 2026 12:51:01 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>n8n 학습기 시리즈 — 3편. 제어 흐름: IF, Switch, 그리고 아이템 단위 실행</strong>
1편에서 던졌던 질문, &quot;왜 데이터를 굳이 배열로 감싸지?&quot;의 진짜 답이 이번 편에 있다. 개념 파트의 마지막 편이다.</p>
</blockquote>
<h2 id="들어가며">들어가며</h2>
<p>지금까지는 데이터가 한 건일 때를 다뤘다. 1편에서 HTTP로 하나 가져오고, 2편에서 그 하나를 표현식으로 가공했다. 그런데 실제 자동화는 대개 여러 건을 다룬다. 메일 여러 통, 행 여러 개, 상담 여러 건. 게다가 조건에 따라 처리 경로가 갈린다. &quot;완료된 건과 아닌 건을 다르게 처리한다&quot; 같은 것.</p>
<p>이번 편은 그 두 가지, <strong>여러 아이템을 어떻게 도는가</strong>와 <strong>어떻게 갈래를 나누는가</strong>를 정리한다.</p>
<h2 id="아이템-단위-실행--n8n이-대신-돌려주는-for-루프">아이템 단위 실행 — n8n이 대신 돌려주는 for 루프</h2>
<p>1편에서 데이터가 항상 아이템 배열로 흐른다고 했다. 그리고 &quot;왜 배열로 감싸지?&quot;라고 물었었다. 답은 이렇다.</p>
<p><strong>n8n의 노드는 입력으로 들어온 아이템 배열의 각 아이템에 대해 자동으로 반복 실행된다.</strong></p>
<p>예를 들어 HTTP Request가 할 일 5개를 배열로 가져오면, 그 뒤에 붙은 노드는 5개 아이템 각각에 대해 한 번씩 동작한다. 내가 <code>for</code> 루프를 짜서 배열을 순회하지 않아도, 플랫폼이 각 아이템에 노드 로직을 알아서 적용해준다. 코드로 치면 이런 걸 n8n이 암묵적으로 해주는 셈이다.</p>
<pre><code class="language-javascript">// 내가 코드로 짜던 것
for (const item of items) {
  process(item);
}

// n8n에서는: 노드를 하나 놓으면 위 루프가 자동으로 돈다</code></pre>
<p>이게 1편 질문의 답이다. 데이터를 항상 배열로 두는 이유는, 노드가 &quot;한 건이든 여러 건이든 똑같은 코드로&quot; 각 아이템을 처리할 수 있게 하기 위해서다. 단건은 그냥 원소가 하나인 배열일 뿐이다.</p>
<!-- 이미지: 여러 아이템이 노드를 통과하며 각각 처리되는 모습 (INPUT n items → 노드 → OUTPUT n items) -->

<h2 id="그럼-루프-노드는-어디-있나">그럼 루프 노드는 어디 있나</h2>
<p>여기서 나는 헤맸다. &quot;여러 개를 도는 거면 루프 노드가 있어야 하는 거 아냐?&quot; 하고 찾아다녔다. 그런데 <strong>대부분의 경우 루프 노드는 필요 없다.</strong> 방금 본 것처럼 반복은 기본으로 일어나기 때문이다.</p>
<p>물론 반복을 직접 제어해야 하는 노드도 있다. 그럴 때 쓰는 게 <strong>Loop Over Items</strong> 노드다(노드를 검색하면 괄호로 <code>Split in Batches</code>라는 옛 이름이 같이 뜨는데, 같은 노드다). 이 노드는 아이템을 정해진 크기(batch size)로 잘라 한 묶음씩 처리한다. 주된 쓰임새는 <strong>API 호출 rate limit 대응</strong>이다. 한 번에 다 보내면 막히는 API에, 10개씩 보내고 잠깐 쉬고(Wait 노드), 다시 10개 보내는 식으로 조절할 때 쓴다.</p>
<p>정리하면 이렇다. 단순히 &quot;여러 개를 처리&quot;하려고 루프 노드를 넣을 필요는 없다. 그건 자동이다. 루프 노드는 &quot;몇 개씩 나눠서, 속도를 조절하며&quot; 돌려야 할 때만 꺼내면 된다.</p>
<p>한 가지 주의. 모든 노드가 자동 반복하는 건 아니다. 일부 노드는 첫 아이템만 처리하거나 전체를 한 덩어리로 다룬다(예: RSS 읽기, 특정 DB 삽입 동작, &quot;Run Once for All Items&quot; 모드의 Code 노드). 이런 예외 노드를 만나면 그때 Loop Over Items로 직접 루프를 짜야 한다. 반대로, 여러 아이템 중 첫 번째만 처리하고 싶으면 노드 Settings 탭의 <strong>Execute Once</strong>를 켜면 된다.</p>
<h2 id="갈래-나누기-1--if-노드">갈래 나누기 1 — IF 노드</h2>
<p>이제 조건 분기다. 가장 기본은 <strong>IF 노드</strong>다. 조건을 평가해서 아이템을 <strong>true 출력과 false 출력</strong> 두 갈래로 내보낸다. 코드의 <code>if / else</code>와 정확히 같다.</p>
<p>예를 들어 할 일 목록에서 &quot;완료된 것과 아닌 것&quot;을 나눈다고 하면, IF 노드에 이런 조건을 건다.</p>
<pre><code>{{ $json.completed }}  →  is true</code></pre><p>그러면 <code>completed</code>가 true인 아이템은 위쪽(true) 출력으로, 나머지는 아래쪽(false) 출력으로 흘러간다. 각 갈래에 서로 다른 노드를 붙이면 경로별로 다르게 처리할 수 있다.</p>
<p>조건은 여러 개를 조합할 수도 있다. AND로 묶으면 모든 조건이 참일 때, OR로 묶으면 하나라도 참일 때 true가 된다. 한 가지 실무 팁은, 최신 IF 노드는 타입 검증(type validation)이 엄격(strict)이라 문자열 <code>&quot;true&quot;</code>와 불린 <code>true</code>를 다르게 본다는 점이다. 값 타입이 안 맞으면 조건이 조용히 안 걸리니, 미리보기로 실제 평가 결과를 확인하는 게 안전하다.</p>
<h2 id="갈래-나누기-2--switch-노드">갈래 나누기 2 — Switch 노드</h2>
<p>IF는 갈래가 둘이다. 그런데 &quot;상태가 new면 A, processing이면 B, done이면 C&quot;처럼 셋 이상으로 나눠야 할 때가 있다. 이때 IF를 여러 개 사슬처럼 잇는 대신 <strong>Switch 노드</strong>를 쓴다.</p>
<p>Switch는 표현식 하나를 여러 값과 비교해서 각각에 맞는 갈래로 아이템을 보낸다. 코드의 <code>switch</code> 문과 같다.</p>
<pre><code>{{ $json.status }}  →  &quot;new&quot; | &quot;processing&quot; | &quot;done&quot; ...</code></pre><p>IF를 사슬로 잇는 것과 Switch의 차이를 기록해두면, IF 체인은 아이템이 매번 위에서부터 조건을 거치며 첫 매치에서 멈추는 반면, Switch는 한 번의 평가로 해당 갈래로 바로 보낸다. 캔버스도 훨씬 깔끔하다. 그래서 나는 규칙을 이렇게 잡았다. <strong>갈래가 둘이면 IF, 셋 이상이면 Switch.</strong></p>
<p>한 가지 함정도 있다. Switch는 여러 갈래 조건이 동시에 참일 때 첫 매치에서 멈추지 않고 <strong>매치되는 모든 갈래로 아이템을 보낸다.</strong> 의도한 게 아니라면 조건이 겹치지 않게 설계해야 한다.</p>
<h2 id="다시-합치기--merge-노드">다시 합치기 — Merge 노드</h2>
<p>갈래로 나눴으면 다시 합쳐야 할 때가 온다. 이때 쓰는 게 <strong>Merge 노드</strong>다. 여러 갈래에서 흘러온 아이템을 하나의 흐름으로 모은다. 분기 → 각자 처리 → 병합이라는, 코드에서 조건별로 갈라 처리한 뒤 결과를 한 리스트로 다시 모으던 패턴과 같다.</p>
<h2 id="작은-실습--여러-아이템을-if로-나눠보기">작은 실습 — 여러 아이템을 IF로 나눠보기</h2>
<p>개념을 확인해봤다. 1편 워크플로우를 살짝 바꿔서, 이번엔 여러 건을 가져온다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/54769c0f-f717-4132-b6c2-68c3a3d6305e/image.png" alt=""></p>
<ol>
<li>HTTP Request의 URL을 여러 건 반환하는 주소로 바꾼다.</li>
</ol>
<pre><code>GET https://jsonplaceholder.typicode.com/todos?_limit=5</code></pre><p>실행하면 이번엔 아이템이 5개 들어온다(1편에선 1개였다). 여기서 이미 &quot;여러 아이템이 흐른다&quot;가 눈에 보인다.</p>
<ol start="2">
<li>그 뒤에 <strong>IF 노드</strong>를 붙이고 조건을 <code>{{ $json.completed }}</code> is true로 건다.</li>
<li><strong>Execute step</strong>을 누른다.</li>
</ol>
<p>실행하면 IF 노드의 두 출력이 갈린다. 완료된 할 일은 true 쪽으로, 아직인 것은 false 쪽으로 나뉘어 각각 몇 개인지 보인다. 노드를 하나 놨을 뿐인데 5개 아이템이 각자 조건을 평가받아 두 갈래로 정리된 것이다. &quot;아이템 단위 실행 + 분기&quot;가 한 화면에서 확인되는 순간이다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/85028fd6-119e-499e-b28c-ec140314fc59/image.png" alt=""></p>
<h2 id="코드로-자동화하던-관점에서">코드로 자동화하던 관점에서</h2>
<ul>
<li>아이템 단위 실행 → 내가 짜던 <code>for (item of items)</code> 루프를 플랫폼이 암묵적으로 돌려주는 것</li>
<li>Loop Over Items → 배치 크기와 대기를 직접 제어하는 명시적 루프(주로 rate limit 대응)</li>
<li>IF 노드 → <code>if / else</code></li>
<li>Switch 노드 → <code>switch</code> 문</li>
<li>Merge 노드 → 갈라 처리한 결과를 다시 한 리스트로 모으는 것</li>
</ul>
<p>1편에서 &quot;그냥 코드가 빠르겠는데?&quot;라고 했던 감각이 여기서 한 번 더 흔들렸다. 여러 건을 도는 루프를 내가 안 짜도 된다는 것, 분기와 병합이 노드 몇 개로 눈에 보이게 정리된다는 것은, 로직의 큰 그림을 잡을 때 분명한 장점이었다. 반대로 세밀한 제어가 필요하면 언제든 Code 노드로 내려가면 되고.</p>
<h2 id="오늘-막혔던-것--메모">오늘 막혔던 것 / 메모</h2>
<ul>
<li><strong>루프 노드를 찾다 헤맴</strong> — &quot;여러 개니까 루프가 필요하겠지&quot; 하고 Loop Over Items부터 넣었다가, 대부분 자동 반복이라 필요 없다는 걸 알고 걷어냈다. 루프 노드는 배치·속도 제어가 필요할 때만.</li>
<li><strong>IF 조건이 안 걸림</strong> — 타입 검증이 엄격해서 문자열과 불린을 다르게 본다. <code>&quot;true&quot;</code>(문자열)와 <code>true</code>(불린)를 혼동하면 조건이 조용히 실패한다. 미리보기로 실제 값과 타입을 확인.</li>
<li><strong>Switch 다중 매치</strong> — 조건이 겹치면 여러 갈래로 동시에 흘러간다. 갈래를 상호 배타적으로 설계하는 걸 기본으로.</li>
</ul>
<h2 id="마치며">마치며</h2>
<p>개념 파트는 여기까지다. 세 편을 요약하면, 데이터는 아이템 배열로 흐르고(1편), 표현식으로 값을 꺼내 가공하고(2편), 아이템 단위로 자동 반복되며 IF와 Switch로 갈래를 나눈다(3편). 이 세 가지만 손에 익으면 n8n 워크플로우의 대부분은 이 위에 얹히는 조합으로 읽힌다.</p>
<p>다음 편부터는 실제 토이 프로젝트다. 지금까지 배운 트리거, 데이터 흐름, 표현식, 분기를 엮어서, 회의록에서 할 일을 자동으로 뽑아 정리하는 파이프라인을 만들어볼 생각이다. 개념이 실제 문제 앞에서 어떻게 조립되는지, 그 과정을 그대로 기록해보려 한다.</p>
<p>읽어주셔서 감사합니다. 🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[n8n에서 데이터 꺼내 쓰기: 표현식과 json  (2편)]]></title>
            <link>https://velog.io/@00kang_jh/n8n%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%BA%BC%EB%82%B4-%EC%93%B0%EA%B8%B0-%ED%91%9C%ED%98%84%EC%8B%9D%EA%B3%BC-json-2%ED%8E%B8</link>
            <guid>https://velog.io/@00kang_jh/n8n%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%BA%BC%EB%82%B4-%EC%93%B0%EA%B8%B0-%ED%91%9C%ED%98%84%EC%8B%9D%EA%B3%BC-json-2%ED%8E%B8</guid>
            <pubDate>Thu, 09 Jul 2026 12:33:48 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>n8n 학습기 시리즈 — 2편. 표현식과 데이터 변형</strong>
1편에서 데이터가 &quot;아이템 배열 + <code>json</code> 키&quot; 형태로 흐른다는 걸 봤다. 이번 편은 그 데이터를 실제로 꺼내 쓰고 가공하는 방법이다. 개인적으로 n8n에서 제일 헷갈리면서 제일 중요한 부분이라 시간을 들였다.</p>
</blockquote>
<h2 id="들어가며">들어가며</h2>
<p>1편의 결론은 &quot;데이터가 노드 사이를 정해진 형태로 흐른다&quot;였다. 그런데 흐르기만 해서는 소용이 없다. 앞 노드가 가져온 값을 뒤 노드에서 꺼내 쓰고, 필요하면 모양을 바꿔야 자동화가 된다. 그 연결고리가 <strong>표현식(Expression)</strong> 이다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/3e6e3329-21f1-4327-997d-a14e01fbbf1d/image.png" alt=""></p>
<p>1편에서 만든 <code>Trigger manually → HTTP Request</code> 워크플로우를 그대로 이어서 진행한다. 그때 HTTP Request가 가져온 데이터는 이거였다.</p>
<pre><code class="language-json">[
  {
    &quot;json&quot;: {
      &quot;userId&quot;: 1,
      &quot;id&quot;: 1,
      &quot;title&quot;: &quot;delectus aut autem&quot;,
      &quot;completed&quot;: false
    }
  }
]</code></pre>
<p>이 <code>title</code>, <code>id</code> 같은 값을 뒤 노드에서 어떻게 꺼내 쓰는지가 이번 편의 주제다.</p>
<h2 id="표현식이란----안의-자바스크립트">표현식이란 — <code>{{ }}</code> 안의 자바스크립트</h2>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/53f24123-ba21-488c-83b4-587bd9ce2f99/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/2640c848-7c53-4299-ab5b-176d4e619e7d/image.png" alt=""></p>
<p>n8n의 표현식은 노드 입력 필드에 직접 써넣는 작은 자바스크립트 조각이고, <code>{{ ... }}</code> 중괄호로 감싼다. 예를 들면 이런 것들이 다 표현식이다.</p>
<pre><code>{{ 2 + 3 }}
{{ &quot;Hello &quot; + &quot;World&quot; }}
{{ $json.title }}</code></pre><p>노드의 파라미터 필드는 기본적으로 &quot;고정값(fixed)&quot; 모드다. 필드 옆의 토글(중괄호 아이콘)을 누르면 &quot;표현식(expression)&quot; 모드로 바뀌고, 그때부터 <code>{{ }}</code>를 써서 값을 동적으로 계산할 수 있다. 편집기가 마지막 실행 데이터를 기준으로 <strong>실시간 미리보기</strong>를 보여줘서, 표현식이 실제로 어떤 값으로 평가되는지 즉시 확인할 수 있다. 이 미리보기가 있어서 학습이 훨씬 수월했다.</p>
<h2 id="json--현재-아이템의-데이터-꺼내기"><code>$json</code> — 현재 아이템의 데이터 꺼내기</h2>
<p>가장 많이 쓰는 패턴이다. <code>$json</code>은 <strong>현재 노드로 들어온 입력 아이템의 <code>json</code> 데이터</strong>를 가리킨다. 정확히는 <code>$input.item.json</code>의 축약형이다.</p>
<p>1편의 응답 기준으로, 필드를 이렇게 꺼낸다.</p>
<pre><code>{{ $json.title }}      // &quot;delectus aut autem&quot;
{{ $json.id }}         // 1
{{ $json.completed }}  // false</code></pre><p>중첩된 객체는 점(<code>.</code>)으로 파고들고, 배열은 대괄호로 인덱싱한다.</p>
<pre><code>{{ $json.user.email }}
{{ $json.items[0].name }}</code></pre><p>1편에서 &quot;실제 값은 <code>json</code> 키 아래에 있다&quot;고 강조했던 이유가 여기서 드러난다. <code>$json</code>이 바로 그 <code>json</code>에 대한 지름길이라, 필드명만 이어 붙이면 값에 닿는다. 코드로 치면 앞 함수가 반환한 객체에서 <code>.title</code>을 꺼내는 것과 다르지 않다.</p>
<h2 id="손으로-다-외울-필요는-없다--드래그앤드롭-매핑">손으로 다 외울 필요는 없다 — 드래그앤드롭 매핑</h2>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/aa2eb0b8-2162-4f44-be41-521d39f630b7/image.gif" alt=""></p>
<p>처음엔 이 표현식들을 다 외워야 하나 싶었는데, 아니었다. 표현식 모드에서 왼쪽 입력 데이터 패널의 필드를 <strong>드래그해서 파라미터에 떨어뜨리면 표현식이 자동으로 생성</strong>된다. <code>title</code> 필드를 끌어다 놓으면 <code>{{ $json.title }}</code>이 알아서 채워진다.</p>
<p>그래서 실무 흐름은 &quot;필드를 끌어다 놓고, 필요하면 뒤에 자바스크립트 메서드를 덧붙이는&quot; 식이 된다. IDE의 자동완성이 경로를 잡아주는 것과 비슷한 감각이라 금방 익숙해졌다.</p>
<h2 id="다른-노드-참조하기--노드이름">다른 노드 참조하기 — <code>$(&#39;노드이름&#39;)</code></h2>
<p>바로 앞 노드가 아니라, 워크플로우 중간의 특정 노드 데이터를 참조해야 할 때가 있다. 이때는 노드를 이름으로 부른다.</p>
<pre><code>{{ $(&#39;HTTP Request&#39;).item.json.userId }}</code></pre><p>여기서 주의할 점 두 가지를 기록해둔다.</p>
<ul>
<li><strong>노드 이름은 대소문자와 공백까지 정확히</strong> 써야 한다. 편집기에 표시된 이름 그대로여야 하고, 오타가 있으면 값을 못 찾는다.</li>
<li>오래된 자료에는 <code>$node[&quot;HTTP Request&quot;].json</code> 같은 문법이 자주 보인다. 이건 구버전 방식이고, 현재 공식 문서가 권장하는 건 <code>$(&#39;노드이름&#39;)</code> 형태다. 검색으로 예제를 참고할 때 이 둘이 섞여 있으니 헷갈리지 않는 게 좋다.</li>
</ul>
<p>한 가지 흥미로웠던 건 <code>.item</code>의 의미다. <code>$(&#39;HTTP Request&#39;).item</code>은 단순히 &quot;그 노드의 첫 아이템&quot;이 아니라, <strong>지금 처리 중인 현재 아이템에 대응(linked)되는 그 노드의 아이템</strong>을 가져온다. 여러 아이템이 흐를 때 짝을 맞춰주는 개념인데, 코드로 배열을 순회할 때 인덱스를 직접 맞춰주던 걸 n8n이 알아서 연결해준다고 이해했다. (여러 아이템을 다루는 이야기는 3편에서 더 자세히 다룬다.)</p>
<h2 id="setedit-fields-노드로-데이터-변형하기">Set(Edit Fields) 노드로 데이터 변형하기</h2>
<p>값을 꺼내는 것까지 봤으니, 이제 모양을 바꿔본다. 데이터 변형에 가장 기본이 되는 건 <strong>Edit Fields(옛 이름 Set)</strong> 노드다. 필드를 추가하거나, 값을 수정하거나, 필요한 필드만 남기는 노드다.</p>
<p>1편 워크플로우 뒤에 Edit Fields 노드를 붙이고, 필드를 세 개 만들어봤다.</p>
<pre><code>// 문자열 메서드 활용
titleUpper = {{ $json.title.toUpperCase() }}

// 여러 값을 조합
summary = {{ &quot;할 일 #&quot; + $json.id + &quot;: &quot; + $json.title }}

// 내장 날짜 변수
fetchedAt = {{ $now }}</code></pre><p><img src="https://velog.velcdn.com/images/00kang_jh/post/4b37a4f7-41e9-4896-9067-fe0c00da8387/image.png" alt=""></p>
<p>실행하면 출력 아이템에 세 필드가 더해진다.</p>
<pre><code class="language-json">{
  &quot;titleUpper&quot;: &quot;DELECTUS AUT AUTEM&quot;,
  &quot;summary&quot;: &quot;할 일 #1: delectus aut autem&quot;,
  &quot;fetchedAt&quot;: &quot;2026-07-09T...&quot;
}</code></pre>
<p>여기서 두 가지를 확인했다. 첫째, 표현식 안에서 <code>toUpperCase()</code> 같은 <strong>평범한 자바스크립트 문자열 메서드가 그대로 먹힌다.</strong> 둘째, <code>$now</code> 같은 n8n 내장 변수를 쓸 수 있다(<code>$now</code>는 Luxon 기반 날짜/시간 객체라, 날짜 계산에 강하다). 즉 표현식은 &quot;n8n이 데이터를 꽂아주는 자바스크립트 실행 환경&quot;에 가까웠다.</p>
<h2 id="표현식이냐-code-노드냐">표현식이냐, Code 노드냐</h2>
<p>여기까지 오니 자연스럽게 든 의문. &quot;그럼 복잡한 로직도 다 표현식에 우겨넣나?&quot; 답은 아니오였고, n8n 문서의 기준도 명확했다.</p>
<ul>
<li><strong>표현식</strong> — 이전 노드 값 참조, 가벼운 변형·계산. 파라미터 필드 안에서 로직을 값 가까이에 두고 싶을 때.</li>
<li><strong>Code 노드</strong> — 자바스크립트나 파이썬을 한 스텝으로 돌리는 전용 노드. 여러 아이템을 한꺼번에 가공하거나, 조건이 복잡하거나, 재사용 로직이 필요할 때.</li>
</ul>
<p>경험칙으로 정리하면 이렇다. <strong>한 줄로 읽히면 표현식, 함수로 빼고 싶어지면 Code 노드.</strong> 표현식에 삼항 연산자를 두세 번 중첩하기 시작하면, 그건 Code 노드로 옮기라는 신호였다. 코드에서 인라인 표현식과 별도 함수 분리를 판단하던 기준과 똑같았다.</p>
<h2 id="코드로-자동화하던-관점에서">코드로 자동화하던 관점에서</h2>
<ul>
<li><code>{{ $json.title }}</code> → 앞 함수가 반환한 객체에서 <code>.title</code> 꺼내기</li>
<li>드래그앤드롭 매핑 → IDE 자동완성이 접근 경로를 잡아주는 것</li>
<li><code>$(&#39;노드이름&#39;).item</code> → 현재 아이템에 짝지어진 특정 노드의 값. 인덱스 매칭을 플랫폼이 대신 해주는 지점</li>
<li>Edit Fields 노드 → 객체 리터럴을 만들어 필드를 매핑하는 것</li>
<li>표현식 vs Code 노드 → 인라인 한 줄이냐, 함수로 분리하냐</li>
</ul>
<p>값을 참조하고 가공하는 이 층까지 익히고 나니, 1편에서 &quot;그냥 코드가 빠르겠는데?&quot; 했던 감각이 조금 달라졌다. 값 하나 꺼내려고 파싱 코드를 쓰지 않아도 되고, 날짜 처리 라이브러리를 따로 import하지 않아도 되는 건 분명한 편의였다.</p>
<h2 id="오늘-막혔던-것--메모">오늘 막혔던 것 / 메모</h2>
<ul>
<li><strong>&quot;referenced node hasn&#39;t run yet&quot; 류 에러</strong> — 이전 노드가 아직 실행되지 않은 상태에서 그 노드를 참조하면 값을 못 가져온다. 표현식을 테스트할 땐 앞 노드부터 한 번 실행해두는 게 안전하다.</li>
<li><strong>노드 이름 오타</strong> — <code>$(&#39;http request&#39;)</code>처럼 대소문자를 틀리면 조용히 <code>undefined</code>가 된다. 편집기 표시 이름을 그대로 복사하는 습관을 들였다.</li>
</ul>
<h2 id="마치며">마치며</h2>
<p>2편의 핵심은 이렇게 요약된다. <strong><code>$json</code>으로 현재 아이템의 값을 꺼내고, <code>$(&#39;노드이름&#39;)</code>으로 다른 노드를 참조하고, Edit Fields 노드와 표현식으로 데이터를 가공한다.</strong> 그리고 한 줄을 넘어가면 Code 노드로 내려간다.</p>
<p>여기까지 오면 단일 아이템을 다루는 감은 잡힌다. 그런데 자동화는 대개 여러 건을 다루고, 조건에 따라 길이 갈린다. 다음 3편에서는 분기(IF/Switch)와, n8n 특유의 &quot;아이템 단위 실행&quot; 방식을 정리한다. 1편에서 &quot;왜 데이터를 배열로 감싸지?&quot;라고 물었던 것의 진짜 답이 거기서 나온다.</p>
<p>읽어주셔서 감사합니다. 🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[n8n을 처음 띄우고, "데이터가 흐른다"는 감각을 잡기까지 (1편)]]></title>
            <link>https://velog.io/@00kang_jh/n8n%EC%9D%84-%EC%B2%98%EC%9D%8C-%EB%9D%84%EC%9A%B0%EA%B3%A0-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%ED%9D%90%EB%A5%B8%EB%8B%A4%EB%8A%94-%EA%B0%90%EA%B0%81%EC%9D%84-%EC%9E%A1%EA%B8%B0%EA%B9%8C%EC%A7%80-1%ED%8E%B8</link>
            <guid>https://velog.io/@00kang_jh/n8n%EC%9D%84-%EC%B2%98%EC%9D%8C-%EB%9D%84%EC%9A%B0%EA%B3%A0-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%ED%9D%90%EB%A5%B8%EB%8B%A4%EB%8A%94-%EA%B0%90%EA%B0%81%EC%9D%84-%EC%9E%A1%EA%B8%B0%EA%B9%8C%EC%A7%80-1%ED%8E%B8</guid>
            <pubDate>Wed, 08 Jul 2026 14:09:52 GMT</pubDate>
            <description><![CDATA[<h1 id="n8n을-처음-띄우고-데이터가-흐른다는-감각을-잡기까지-1편">n8n을 처음 띄우고, &quot;데이터가 흐른다&quot;는 감각을 잡기까지 (1편)</h1>
<blockquote>
<p><strong>n8n 학습기 시리즈 — 1편. 핵심 개념: 노드, 트리거, 그리고 JSON</strong>
0편에서 왜 n8n을 배우기로 했는지 적었다. 이번 편은 실제로 띄워보고 첫 워크플로우를 뜯어보며 개념을 정리한다.</p>
</blockquote>
<h2 id="들어가며">들어가며</h2>
<p>말로만 개념을 읽는 것보다, 일단 화면을 띄워놓고 노드를 만져보는 게 빠르다고 생각했다. 그래서 이번 편은 순서가 이렇다. 먼저 n8n을 셀프호스팅으로 띄우고, 첫 워크플로우를 하나 만들어 실행해본 다음, 거기서 관찰한 걸로 핵심 개념 세 가지(노드 / 트리거 / 데이터)를 정리한다.</p>
<h2 id="일단-띄우고-본다-docker">일단 띄우고 본다 (Docker)</h2>
<p>0편에서 말했듯 처음부터 셀프호스팅으로 가기로 했다. 클라우드 버전의 실행 수 제약 없이 구조를 제대로 이해하고 싶었고, Docker 컨테이너 하나 띄우는 건 부담도 아니니까.</p>
<p>Mac에 Docker Desktop이 깔려 있다는 전제로, 터미널에 다음 두 줄이면 끝난다.</p>
<pre><code class="language-bash">docker volume create n8n_data

docker run -it --rm \
  --name n8n \
  -p 5678:5678 \
  -v n8n_data:/home/node/.n8n \
  docker.n8n.io/n8nio/n8n</code></pre>
<p>첫 줄은 워크플로우와 자격 증명(Credentials)을 저장할 볼륨을 만드는 것이다. 이걸 붙여줘야 컨테이너를 내렸다 올려도 만든 워크플로우가 날아가지 않는다. 이미지 경로가 <code>docker.n8n.io/n8nio/n8n</code>인 점만 주의하면 된다(공식 레지스트리 경로다).</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/91246819-6fa6-4788-b359-238c6549b956/image.png" alt=""></p>
<p>명령이 돌면 이미지를 받아 컨테이너가 뜨고, 브라우저에서 <code>http://localhost:5678</code> 로 접속하면 편집기가 나온다. 최초 1회 계정(오너) 설정을 하고 나면 빈 캔버스가 열린다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/484ca8ed-7cd8-48ec-9929-208bff913f4e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/fa9c05d9-2eec-4821-b0ad-9a525587084c/image.png" alt=""></p>
<blockquote>
<p>참고: 최근 버전은 <code>-e N8N_RUNNERS_ENABLED=true</code> 같은 환경 변수를 권장하는데, 입문 단계에선 위 최소 구성으로도 충분하다. 프로덕션으로 갈 땐 타임존, DB(PostgreSQL) 분리 등을 붙이게 되지만 그건 한참 뒤 이야기다.</p>
</blockquote>
<h2 id="개념-1--노드-워크플로우의-기본-단위">개념 1 — 노드: 워크플로우의 기본 단위</h2>
<p>빈 캔버스에서 첫인상은 단순했다. 자동화의 각 단계가 <strong>노드(Node)</strong> 라는 상자 하나이고, 노드를 선으로 이어 흐름을 만든다. 코드로 치면 함수 하나하나를 상자로 그려놓고, 앞 함수의 리턴값을 뒤 함수 입력으로 넘기는 걸 눈에 보이게 만든 셈이다.</p>
<p>노드는 크게 두 종류로 나뉜다.</p>
<ul>
<li><strong>트리거 노드</strong> — 워크플로우를 <em>시작</em>시키는 노드. 모든 워크플로우는 트리거에서 출발한다. 캔버스에 처음 노드를 추가할 때 트리거부터 고르라고 나오는 이유다.</li>
<li><strong>일반(액션) 노드</strong> — 트리거 이후에 데이터를 <em>처리</em>하는 노드. API 호출, 데이터 변형, 조건 분기, 외부 서비스로 전송 등이 여기 속한다.</li>
</ul>
<p>이 구분이 코드와 대응이 잘 됐다. 트리거 노드는 이벤트 리스너나 진입점(<code>main</code>, 웹훅 핸들러, cron 잡)이고, 일반 노드는 그 안에서 호출되는 함수들이다.</p>
<h2 id="개념-2--트리거-무엇이-워크플로우를-깨우는가">개념 2 — 트리거: 무엇이 워크플로우를 깨우는가</h2>
<p>트리거 종류를 훑어보니 익숙한 것들이었다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/fd17f43a-e7f0-4eb9-94c1-4c1a35db519b/image.png" alt=""></p>
<ul>
<li><strong>Trigger manually</strong> — 편집기에서 버튼을 눌러 수동 실행. 개발 중 테스트용이다.</li>
<li><strong>On a schedule</strong> — 정해진 주기로 실행. 사실상 cron이다.</li>
<li><strong>On webhook call</strong> — 외부에서 HTTP 요청이 들어오면 실행. 내가 API 서버에서 웹훅 핸들러 붙이던 것과 같다.</li>
<li><strong>On app event</strong> — Gmail에 새 메일이 오면, Google Drive에 새 파일이 생기면 같은 서비스별 이벤트.</li>
</ul>
<p>결국 &quot;무엇이 이 자동화를 깨우는가&quot;를 고르는 것이고, 코드로 자동화할 때 내가 매번 직접 만들던 진입 조건(스케줄러 등록, 웹훅 엔드포인트 개설, 폴링 루프)을 플랫폼이 노드 하나로 대신 제공하는 구조였다. 0편에서 &quot;매번 새로 만들던 뼈대를 플랫폼이 깔아준다&quot;고 기대했던 게 이 지점에서 처음 체감됐다.</p>
<h2 id="개념-3--데이터는-json으로-흐른다-이번-편의-핵심">개념 3 — 데이터는 JSON으로 흐른다 (이번 편의 핵심)</h2>
<p>여기가 제일 중요하다. n8n을 이해하는 열쇠는 <strong>노드 사이를 흐르는 데이터의 모양</strong>을 아는 것이다.</p>
<p>n8n에서 노드 간 데이터는 항상 <strong>아이템(item)들의 배열</strong>로 흐른다. 그리고 각 아이템은 이런 형태다.</p>
<pre><code class="language-json">[
  {
    &quot;json&quot;: {
      &quot;id&quot;: 1,
      &quot;title&quot;: &quot;예시 데이터&quot;
    }
  },
  {
    &quot;json&quot;: {
      &quot;id&quot;: 2,
      &quot;title&quot;: &quot;다음 아이템&quot;
    }
  }
]</code></pre>
<p>핵심을 정리하면 이렇다.</p>
<ul>
<li>모든 노드는 <strong>아이템 배열을 입력으로 받아, 아이템 배열을 출력으로 내보낸다.</strong></li>
<li>실제 데이터는 각 아이템의 <code>json</code> 키 안에 들어 있다. (파일 같은 바이너리는 별도로 <code>binary</code> 키에 담긴다.)</li>
<li>그래서 다음 노드에서 앞 노드의 값을 참조할 때 <code>json</code>을 거쳐 접근하게 된다. (구체적인 표현식 문법은 2편에서 다룬다.)</li>
</ul>
<p>처음엔 &quot;왜 데이터를 굳이 배열로 감싸고, 또 <code>json</code> 키로 한 번 더 감싸지?&quot; 싶었다. 그런데 이유는 금방 납득됐다. 자동화는 대부분 <strong>여러 건</strong>을 다룬다. 메일 10통, 행 100개, 파일 여러 개. 데이터를 항상 배열로 취급하면, 노드는 &quot;한 건이든 여러 건이든 똑같은 방식으로&quot; 처리할 수 있다. 코드로 치면 단일 객체와 리스트를 분기 처리하지 않고, 처음부터 전부 리스트로 두고 <code>map</code>을 도는 것과 같은 발상이다.</p>
<h2 id="첫-워크플로우-데이터를-눈으로-확인하기">첫 워크플로우: 데이터를 눈으로 확인하기</h2>
<p>개념을 글로 읽었으니 실제로 확인해봤다. 가장 단순한 조합이다.</p>
<p><strong>Trigger manually → HTTP Request</strong></p>
<ol>
<li>캔버스에서 트리거로 <strong>Trigger manually</strong>를 추가한다. (추가하면 노드 이름이 <code>When clicking &#39;Execute workflow&#39;</code>로 표시된다.)</li>
<li>그 뒤에 <strong>HTTP Request</strong> 노드를 잇고, Method는 <code>GET</code>, URL에 테스트용 공개 API를 넣는다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/ec11fcc0-41b3-40b6-9304-ddb1032185f5/image.png" alt=""></p>
<pre><code>GET https://jsonplaceholder.typicode.com/todos/1</code></pre><ol start="3">
<li>노드의 <strong>Execute step</strong> 버튼을 누른다. (워크플로우 전체를 돌릴 땐 <strong>Execute workflow</strong>를 쓴다.)</li>
</ol>
<p>실행하면 HTTP Request 노드에 결과가 뜨는데, 여기서 처음으로 &quot;데이터가 흐른다&quot;는 게 눈에 보인다. 노드 출력 패널을 보면 응답이 이렇게 담겨 있다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/63174a09-8e80-4c3f-9ce7-16594f71e31f/image.png" alt=""></p>
<pre><code class="language-json">[
  {
    &quot;json&quot;: {
      &quot;userId&quot;: 1,
      &quot;id&quot;: 1,
      &quot;title&quot;: &quot;delectus aut autem&quot;,
      &quot;completed&quot;: false
    }
  }
]</code></pre>
<p>앞서 정리한 그대로다. API 응답이 아이템 배열의 <code>json</code> 키 안에 들어와 있다. n8n UI에서는 이 출력을 Table(표), JSON, Schema 세 가지 뷰로 볼 수 있는데, 특히 Schema 뷰가 다음 노드에서 어떤 필드를 참조할 수 있는지 보여줘서 유용했다.</p>
<p>여기까지가 1편의 목표였다. HTTP Request 한 줄로 외부 데이터를 가져오는 건 코드로 수백 번 해봤지만, 그 결과가 <strong>정해진 형태(아이템 배열 + json 키)로 캔버스 위를 흐르고, 다음 노드가 그걸 받아 쓸 수 있다</strong>는 감각이 n8n의 출발점이었다.</p>
<h2 id="코드로-자동화하던-관점에서">코드로 자동화하던 관점에서</h2>
<p>정리하면서 계속 코드와 겹쳐 봤다.</p>
<ul>
<li><strong>Manual Trigger</strong> → 스크립트를 직접 실행하는 것(<code>main()</code> 호출)</li>
<li><strong>HTTP Request 노드</strong> → <code>fetch</code> / <code>axios</code> 한 줄</li>
<li><strong>노드를 잇는 선</strong> → 앞 함수의 리턴값을 뒤 함수 인자로 넘기는 것</li>
<li><strong>아이템 배열</strong> → API 응답을 리스트로 받아 <code>map</code>으로 순회하는 것</li>
</ul>
<p>여기까지 보면 &quot;그냥 코드가 더 빠르겠는데?&quot;라는 생각도 들었다. 실제로 단발성 스크립트라면 그 말이 맞다. 다만 n8n의 값어치는 이 다음부터 나온다고 본다. 인증을 노드가 관리하고, 스케줄과 웹훅이 트리거로 붙고, 실패 시 재시도가 얹히는 순간, 내가 매번 짜던 &quot;주변 코드&quot;가 사라진다. 그 대조를 다음 편들에서 계속 확인해볼 생각이다.</p>
<h2 id="오늘-막혔던-것--메모">오늘 막혔던 것 / 메모</h2>
<ul>
<li>처음에 노드 출력이 배열로 감싸여 있어서 잠깐 헷갈렸다. → n8n은 <strong>항상 아이템 배열 단위</strong>로 데이터를 다룬다는 걸 이해하니 풀렸다.</li>
<li>실제 값이 <code>json</code> 키 아래에 있다는 걸 놓치면, 나중에 표현식에서 필드를 잘못 참조하게 된다. 이건 2편에서 제대로 정리할 예정.</li>
</ul>
<h2 id="마치며">마치며</h2>
<p>1편의 결론은 한 문장이다. <strong>n8n에서 데이터는 아이템 배열의 형태로, 각 값은 <code>json</code> 키 안에 담겨 노드 사이를 흐른다.</strong> 이 구조만 손에 익으면 나머지는 그 위에 얹히는 이야기다.</p>
<p>다음 2편에서는 이 데이터를 실제로 꺼내 쓰고 변형하는 방법, 즉 표현식(<code>{{ $json }}</code>)과 노드 간 데이터 참조를 정리한다. 개인적으로 여기가 n8n에서 제일 헷갈리면서도 제일 중요한 부분이라 생각해서, 시간을 들여 다뤄보려 한다.</p>
<p>읽어주셔서 감사합니다. 🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[코드로 자동화하던 QA 엔지니어가 n8n을 배우기로 했다 (0편)]]></title>
            <link>https://velog.io/@00kang_jh/%EC%BD%94%EB%93%9C%EB%A1%9C-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%98%EB%8D%98-QA-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EA%B0%80-n8n%EC%9D%84-%EB%B0%B0%EC%9A%B0%EA%B8%B0%EB%A1%9C-%ED%96%88%EB%8B%A4-0%ED%8E%B8</link>
            <guid>https://velog.io/@00kang_jh/%EC%BD%94%EB%93%9C%EB%A1%9C-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%98%EB%8D%98-QA-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EA%B0%80-n8n%EC%9D%84-%EB%B0%B0%EC%9A%B0%EA%B8%B0%EB%A1%9C-%ED%96%88%EB%8B%A4-0%ED%8E%B8</guid>
            <pubDate>Mon, 06 Jul 2026 15:17:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>n8n 학습기 시리즈 — 0편. 왜 n8n인가</strong>
토이 프로젝트에 들어가기 전, 개념부터 하나씩 정리하며 남기는 기록입니다.</p>
</blockquote>
<h2 id="들어가며">들어가며</h2>
<p>2년 남짓 소프트웨어 품질 검증과 테스트 자동화를 해왔다. Playwright와 TypeScript로 E2E 테스트를 짜고, JUnit과 Maven으로 DB 동작을 검증하고, 반복되는 시나리오를 스크립트로 돌려왔다. 내 일의 상당 부분은 결국 <strong>&quot;사람이 손으로 반복하던 걸 코드가 대신하게 만드는 것&quot;</strong> 이었다.</p>
<p>그런 내가 요즘 n8n이라는 워크플로우 자동화 툴을 붙잡고 있다. 처음 마음먹었을 때 스스로도 좀 이상했다. 자동화라면 코드로 다 짜봤는데, 굳이 노코드 툴을 왜?</p>
<p>이 글은 그 질문에 대한 내 나름의 답이자, 앞으로 이어질 학습 기록의 0편이다. 배운 걸 정리하기 전에, 왜 배우기로 했는지부터 적어두려 한다.</p>
<h2 id="코드-자동화와-업무-자동화-사이의-간극">코드 자동화와 업무 자동화 사이의 간극</h2>
<p>테스트 자동화에는 명확한 대상이 있다. 로그인 폼, API 응답, 쿼리 결과. 검증할 게 정해져 있고, 기대와 다르면 빨간불이 켜진다. 이 세계는 익숙하고, 나름의 방법론도 몸에 배어 있다.</p>
<p>그런데 일을 하다 보면 &quot;테스트&quot;가 아닌 자동화가 계속 눈에 밟혔다.</p>
<ul>
<li>회의가 끝나면 회의록을 정리해서 공유하기</li>
<li>어딘가에서 데이터를 긁어와 시트에 정리하기</li>
<li>특정 이벤트가 발생하면 알림 보내기</li>
<li>API 두어 개를 엮어서 중간 데이터를 흘려보내기</li>
</ul>
<p>하나하나 뜯어보면 코드로 못 할 건 없다. 실제로 몇 개는 파이썬 스크립트로 만들어 쓰기도 했다. 그런데 매번 스크립트를 짜고, 인증 붙이고, 돌릴 환경을 마련하고, 실패하면 로그를 뒤지는 과정이 배보다 배꼽이 컸다. <strong>자동화 자체보다 자동화를 굴러가게 만드는 주변 작업이 더 무거웠다.</strong></p>
<p>이 지점에서 워크플로우 자동화 툴이 뭘 해결해주는지 궁금해졌다. 트리거, 인증, 스케줄링, 재시도 같은 &quot;매번 새로 만들던 뼈대&quot;를 플랫폼이 대신 깔아준다면, 나는 로직에만 집중하면 되는 것 아닌가.</p>
<h2 id="n8n이-뭐길래">n8n이 뭐길래</h2>
<p>먼저 정확히 짚고 넘어가자. n8n을 검색하면 &quot;오픈소스 자동화 툴&quot;이라는 소개가 흔하게 나오는데, 엄밀히 말하면 <strong>OSI 기준의 오픈소스는 아니다.</strong> n8n은 Sustainable Use License라는 fair-code(source-available) 라이선스를 따른다. 소스는 공개되어 있고, 개인이나 내부 업무 용도로는 자유롭게 셀프호스팅해서 무료로 쓸 수 있다. 다만 이걸 기반으로 상업용 SaaS를 만들어 남에게 재판매하는 건 별도 라이선스가 필요하다.</p>
<p>이 구분을 처음부터 정확히 알아둔 이유가 있다. 나중에 사업이나 제품을 고민할 때, &quot;무료 오픈소스인 줄 알았는데 아니었다&quot;는 건 꽤 뼈아픈 착오가 되기 때문이다.</p>
<p>기술적으로 n8n의 핵심은 이렇게 정리된다.</p>
<ul>
<li><strong>노드 기반 워크플로우 에디터</strong> — 자동화의 각 단계가 하나의 &quot;노드&quot;이고, 노드를 선으로 연결해 데이터 흐름을 만든다. Zapier의 선형적인 &quot;트리거 → 액션&quot; 모델과 달리, 분기·루프·병렬 실행·서브워크플로우를 기본으로 지원한다.</li>
<li><strong>실행(execution) 단위 과금</strong> — 워크플로우가 아무리 여러 단계로 복잡해도 한 번 도는 걸 1 execution으로 센다. 단계마다 과금하는 task 방식과 대조되는 지점이다.</li>
<li><strong>셀프호스팅</strong> — Docker로 내 인프라 위에 직접 띄울 수 있다. 데이터가 내 서버 밖으로 나가지 않는다.</li>
<li><strong>HTTP Request / Webhook 노드</strong> — 네이티브 연동이 없는 서비스라도 API만 있으면 직접 붙일 수 있다.</li>
<li><strong>Code 노드</strong> — 필요하면 JavaScript나 Python 코드를 워크플로우 중간에 그대로 실행할 수 있다.</li>
</ul>
<p>마지막 두 항목이 내 눈길을 끌었다. 노코드 툴이라고 해서 코드를 못 쓰게 막는 게 아니라, <strong>필요한 순간에 코드로 내려갈 수 있는 탈출구</strong>를 열어둔 구조다.</p>
<h2 id="왜-하필-n8n인가">왜 하필 n8n인가</h2>
<p>Zapier, Make, n8n. 흔히 비교되는 세 가지 중에서 n8n을 고른 이유는 결국 내 배경 때문이다.</p>
<p>Zapier는 비개발자도 몇 분 만에 자동화를 만들 수 있는 게 강점이다. 대신 코드를 넣는 순간 실행 시간·메모리·호출 횟수에 빡빡한 제약이 걸린다. 반면 n8n은 애초에 기술 유저를 겨냥해 설계됐다. Code 노드에서 로직을 온전히 돌릴 수 있고, HTTP Request로 어떤 API든 직접 호출하고, 셀프호스팅하면 내 인프라의 일부처럼 동작한다.</p>
<p>정리하면 이렇다. 나는 이미 API 인증, HTTP, JSON 데이터 다루기, 스크립트 짜기가 익숙하다. 그렇다면 <strong>그 익숙함이 제약이 아니라 무기가 되는 도구</strong>를 고르는 게 맞다. n8n에서 낯선 건 노드 기반 UI라는 패러다임 정도이고, 나머지 상당수는 내가 코드로 하던 것의 다른 표현일 거라고 봤다.</p>
<p>한 가지 더. n8n은 2.0(2026년 1월)부터 LangChain 기반 AI 노드와 AI Agent 노드를 정식으로 품었다. Claude, OpenAI, Gemini 같은 모델을 워크플로우 안에서 도구처럼 부릴 수 있다. 자동화에 LLM을 얹는 흐름을 제대로 실험해보기에도 좋은 시점이라고 판단했다.</p>
<h2 id="이-시리즈에서-다룰-것">이 시리즈에서 다룰 것</h2>
<p>거창한 강의는 아니다. 매일 조금씩 배우면서, 막혔던 지점과 그걸 어떻게 넘었는지를 위주로 남길 생각이다. 대략 이런 순서를 잡고 있다.</p>
<ul>
<li><strong>0편 (이 글)</strong> — 왜 n8n인가</li>
<li><strong>1편</strong> — 핵심 개념: 노드, 트리거, 그리고 데이터가 JSON으로 흐른다는 감각</li>
<li><strong>2편</strong> — 표현식과 데이터 변형: <code>{{ $json }}</code> 문법과 노드 간 데이터 참조</li>
<li><strong>3편</strong> — 제어 흐름: 분기(IF/Switch)와 아이템 단위 실행이라는 n8n 특유의 방식</li>
<li><strong>4편~</strong> — 실제 토이 프로젝트: 트리거 → 처리 → 실행까지 이어지는 워크플로우 만들기</li>
</ul>
<p>특히 코드로 자동화하던 관점에서 &quot;이건 코드로 하던 그거네&quot;, &quot;이건 오히려 코드가 편한데?&quot;, &quot;아 이래서 노코드가 필요하구나&quot; 같은 대조를 놓치지 않고 기록하려 한다. 같은 걸 다른 각도에서 보는 게 이 시리즈의 색깔이 될 것 같다.</p>
<h2 id="마치며">마치며</h2>
<p>배운 걸 글로 정리하는 습관은 예전 SDET 학습기를 쓰면서 효과를 봤다. 손으로 정리하지 않으면 &quot;돌아가는 걸 봤다&quot;에서 멈추고, 정작 왜 그렇게 되는지는 흐릿하게 남는다.</p>
<p>그래서 이번에도 토이 프로젝트에 뛰어들기 전에, 기초를 다지는 과정부터 차근차근 남겨보려 한다. 다음 글에서는 n8n을 직접 띄우고 첫 워크플로우를 뜯어보며 핵심 개념을 정리할 예정이다.</p>
<p>읽어주셔서 감사합니다. 🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Playwright 테스트 실행 옵션 가이드]]></title>
            <link>https://velog.io/@00kang_jh/Playwright-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%8B%A4%ED%96%89-%EC%98%B5%EC%85%98-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@00kang_jh/Playwright-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%8B%A4%ED%96%89-%EC%98%B5%EC%85%98-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Thu, 22 Jan 2026 02:57:43 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>이전 글에서 Playwright 설치를 완료했습니다. 이번 글에서는 설치 시 생성된 기본 예제 코드를 활용해서 다양한 테스트 실행 옵션을 알아보겠습니다.</p>
<blockquote>
<p>💻 <strong>작성 환경</strong></p>
<ul>
<li>macOS Tahoe 26.2</li>
<li>VS Code</li>
<li>Node.js</li>
</ul>
</blockquote>
<hr>
<h2 id="기본-예제-코드-살펴보기">기본 예제 코드 살펴보기</h2>
<p>Playwright 설치 시 <code>tests/example.spec.ts</code> 파일이 자동으로 생성됩니다.</p>
<pre><code class="language-typescript">import { test, expect } from &#39;@playwright/test&#39;;

test(&#39;has title&#39;, async ({ page }) =&gt; {
  await page.goto(&#39;https://playwright.dev/&#39;);

  // Expect a title &quot;to contain&quot; a substring.
  await expect(page).toHaveTitle(/Playwright/);
});

test(&#39;get started link&#39;, async ({ page }) =&gt; {
  await page.goto(&#39;https://playwright.dev/&#39;);

  // Click the get started link.
  await page.getByRole(&#39;link&#39;, { name: &#39;Get started&#39; }).click();

  // Expects page to have a heading with the name of Installation.
  await expect(page.getByRole(&#39;heading&#39;, { name: &#39;Installation&#39; })).toBeVisible();
});</code></pre>
<p>이 코드는 Playwright 공식 사이트에 접속해서 타이틀을 확인하고, &quot;Get started&quot; 링크를 클릭하는 간단한 테스트입니다. 이 예제를 기준으로 다양한 실행 옵션을 살펴보겠습니다.</p>
<hr>
<h2 id="테스트-실행-옵션">테스트 실행 옵션</h2>
<h3 id="기본-실행">기본 실행</h3>
<pre><code class="language-bash">npx playwright test</code></pre>
<p>모든 테스트 파일을 실행합니다. 기본적으로 headless 모드(브라우저 창 없이)로 실행되며, 설정된 모든 브라우저(Chromium, Firefox, WebKit)에서 테스트가 진행됩니다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/7ea6495e-ce33-4037-8861-68e46a9bd001/image.png" alt=""></p>
<h3 id="worker-수-지정">Worker 수 지정</h3>
<pre><code class="language-bash">npx playwright test --workers 3</code></pre>
<p>병렬로 실행할 worker 수를 지정합니다. 기본값은 CPU 코어 수의 절반입니다. 테스트가 많을 때 worker 수를 늘리면 전체 실행 시간을 단축할 수 있습니다. 반대로 리소스가 부족하거나 테스트 간 충돌이 발생하면 worker 수를 줄여볼 수 있습니다.</p>
<h3 id="특정-파일-실행">특정 파일 실행</h3>
<pre><code class="language-bash">npx playwright test one.spec.ts</code></pre>
<p>특정 테스트 파일만 실행합니다. 파일명 일부만 입력해도 매칭되는 파일을 찾아 실행합니다.</p>
<pre><code class="language-bash">npx playwright test one.spec.ts two.spec.ts</code></pre>
<p>여러 파일을 지정하려면 공백으로 구분해서 나열하면 됩니다.</p>
<h3 id="파일명-키워드로-실행">파일명 키워드로 실행</h3>
<pre><code class="language-bash">npx playwright test one two</code></pre>
<p>파일명에 <code>one</code> 또는 <code>two</code>가 포함된 모든 테스트 파일을 실행합니다. 확장자 없이 키워드만 입력해도 됩니다.</p>
<h3 id="테스트-이름으로-실행">테스트 이름으로 실행</h3>
<pre><code class="language-bash">npx playwright test -g &quot;test title&quot;</code></pre>
<p><code>-g</code> 옵션을 사용하면 테스트 이름(title)으로 필터링할 수 있습니다. 예제 코드 기준으로 <code>&quot;has title&quot;</code> 테스트만 실행하고 싶다면 다음과 같이 입력합니다.</p>
<pre><code class="language-bash">npx playwright test -g &quot;has title&quot;</code></pre>
<p>정규식 패턴도 사용 가능합니다.</p>
<h3 id="특정-브라우저에서-실행">특정 브라우저에서 실행</h3>
<pre><code class="language-bash">npx playwright test --project=chromium</code></pre>
<p>특정 브라우저에서만 테스트를 실행합니다. <code>playwright.config.js</code>에 정의된 프로젝트 이름을 사용합니다.</p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>--project=chromium</code></td>
<td>Chrome 기반 브라우저</td>
</tr>
<tr>
<td><code>--project=firefox</code></td>
<td>Firefox</td>
</tr>
<tr>
<td><code>--project=webkit</code></td>
<td>Safari 기반 브라우저</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/471db6df-f8d8-4a62-8d37-959a3ea386a4/image.png" alt=""></p>
<h3 id="브라우저-창-띄워서-실행">브라우저 창 띄워서 실행</h3>
<pre><code class="language-bash">npx playwright test --headed</code></pre>
<p>기본적으로 Playwright는 headless 모드로 실행되어 브라우저 창이 보이지 않습니다. <code>--headed</code> 옵션을 사용하면 실제 브라우저 창이 열리면서 테스트가 진행되는 모습을 눈으로 확인할 수 있습니다.</p>
<h3 id="디버그-모드-실행">디버그 모드 실행</h3>
<pre><code class="language-bash">npx playwright test --debug</code></pre>
<p>디버그 모드로 실행합니다. Playwright Inspector가 열리면서 테스트를 단계별로 실행하고, 각 단계에서 멈춰서 상태를 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/b6140b43-278c-4df5-8f43-e873f4972656/image.png" alt=""></p>
<h3 id="특정-라인부터-디버그">특정 라인부터 디버그</h3>
<pre><code class="language-bash">npx playwright test one.spec.ts:11 --debug</code></pre>
<p>파일명 뒤에 <code>:라인번호</code>를 붙이면 해당 라인에 있는 테스트부터 디버그 모드로 실행합니다. 특정 테스트만 집중적으로 디버깅할 때 유용합니다.</p>
<hr>
<h2 id="옵션-조합-예시">옵션 조합 예시</h2>
<p>여러 옵션을 조합해서 사용할 수 있습니다.</p>
<pre><code class="language-bash"># Chromium에서 headed 모드로 특정 파일 실행
npx playwright test example.spec.ts --project=chromium --headed

# worker 1개로 순차 실행하면서 디버그
npx playwright test --workers 1 --debug

# 특정 테스트만 Firefox에서 실행
npx playwright test -g &quot;has title&quot; --project=firefox</code></pre>
<hr>
<h2 id="실행-옵션-요약">실행 옵션 요약</h2>
<table>
<thead>
<tr>
<th>명령어</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>npx playwright test</code></td>
<td>전체 테스트 실행</td>
</tr>
<tr>
<td><code>npx playwright test --workers 3</code></td>
<td>worker 수 지정</td>
</tr>
<tr>
<td><code>npx playwright test one.spec.ts</code></td>
<td>특정 파일 실행</td>
</tr>
<tr>
<td><code>npx playwright test one two</code></td>
<td>키워드로 파일 필터링</td>
</tr>
<tr>
<td><code>npx playwright test -g &quot;test title&quot;</code></td>
<td>테스트 이름으로 필터링</td>
</tr>
<tr>
<td><code>npx playwright test --project=chromium</code></td>
<td>특정 브라우저 실행</td>
</tr>
<tr>
<td><code>npx playwright test --headed</code></td>
<td>브라우저 창 띄워서 실행</td>
</tr>
<tr>
<td><code>npx playwright test --debug</code></td>
<td>디버그 모드 실행</td>
</tr>
<tr>
<td><code>npx playwright test file.spec.ts:11 --debug</code></td>
<td>특정 라인부터 디버그</td>
</tr>
</tbody></table>
<hr>
<h2 id="마치며">마치며</h2>
<p>테스트를 작성하다 보면 전체 테스트를 돌리기보다 특정 테스트만 반복 실행하는 경우가 많습니다. 오늘 다룬 옵션들을 활용하면 개발 중에 빠르게 피드백을 받으면서 테스트를 작성할 수 있습니다.</p>
<p>다음 글에서는 테스트 코드를 직접 작성하는 방법을 알아보겠습니다.</p>
<hr>
<blockquote>
<p>📚 <strong>참고 자료</strong></p>
<ul>
<li><a href="https://playwright.dev/docs/running-tests">Playwright - Running Tests</a></li>
<li><a href="https://playwright.dev/docs/test-cli">Playwright - Command Line</a></li>
</ul>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Playwright 설치 가이드(js)]]></title>
            <link>https://velog.io/@00kang_jh/Playwright-%EC%84%A4%EC%B9%98-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@00kang_jh/Playwright-%EC%84%A4%EC%B9%98-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Thu, 22 Jan 2026 01:23:15 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>이전 글에서 Playwright가 무엇인지, 왜 사용하는지 알아봤습니다. 이번 글에서는 실제로 Playwright를 설치하고 첫 테스트를 실행하는 과정을 다뤄보겠습니다.</p>
<p>Node.js와 VS Code가 이미 설치되어 있다는 가정 하에 진행합니다.</p>
<blockquote>
<p>💻 <strong>작성 환경</strong></p>
<ul>
<li>macOS Tahoe 26.2</li>
<li>VS Code</li>
<li>Node.js</li>
</ul>
</blockquote>
<hr>
<h2 id="프로젝트-폴더-생성">프로젝트 폴더 생성</h2>
<p>먼저 Playwright 프로젝트를 위한 폴더를 생성하고 이동합니다.</p>
<pre><code class="language-bash">mkdir my-playwright-project
cd my-playwright-project</code></pre>
<p>VS Code에서 해당 폴더를 열어주세요.</p>
<pre><code class="language-bash">code .</code></pre>
<hr>
<h2 id="playwright-설치">Playwright 설치</h2>
<p>VS Code에서 터미널을 열고 다음 명령어를 실행합니다.</p>
<pre><code class="language-bash">npm init playwright@latest</code></pre>
<p>명령어를 실행하면 몇 가지 질문이 나타납니다.</p>
<h3 id="설치-옵션-선택">설치 옵션 선택</h3>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/182660a4-40c9-4610-a766-70d82bf4ef03/image.png" alt=""></p>
<p><strong>1. TypeScript 또는 JavaScript 선택</strong></p>
<pre><code>Do you want to use TypeScript or JavaScript?
  ▸ TypeScript
    JavaScript</code></pre><p>원하는 언어를 선택합니다. 이 가이드에서는 TypeScript를 기준으로 진행합니다.</p>
<p><strong>2. 테스트 폴더 경로</strong></p>
<pre><code>Where to put your end-to-end tests?
  ▸ tests</code></pre><p>테스트 파일이 저장될 폴더명입니다. 기본값인 <code>tests</code>를 그대로 사용하면 됩니다.</p>
<p><strong>3. GitHub Actions 워크플로우 추가 여부</strong></p>
<pre><code>Add a GitHub Actions workflow?
  ▸ false</code></pre><p>Github Actions에서 CI/CD 설정이 필요하면 <code>true</code>를 선택합니다. 지금은 <code>false</code>로 진행해도 괜찮습니다.</p>
<p><strong>4. 브라우저 설치 여부</strong></p>
<pre><code>Install Playwright browsers?
  ▸ true</code></pre><p>반드시 <code>true</code>를 선택합니다. Playwright가 사용할 브라우저 바이너리(Chromium, Firefox, WebKit)를 설치합니다. <code>false</code>를 할 경우 나중에 직접 브러우저를 설치해줘야하는 불편함이 있으니깐 웬만하면 <code>true</code>로 하시길 권장합니다.</p>
<hr>
<h2 id="설치-완료-확인">설치 완료 확인</h2>
<p>설치가 완료되면 프로젝트에 다음 파일들이 생성됩니다.</p>
<p align="center">
  <img src="https://velog.velcdn.com/images/00kang_jh/post/c3968e95-ff2c-4525-b404-35ce7ed52d57/image.png" width="50%" />
</p>


<p><code>package.json</code>을 열어보면 Playwright가 devDependencies에 추가된 것을 확인할 수 있습니다.</p>
<p align="center">
  <img src="https://velog.velcdn.com/images/00kang_jh/post/9dc0d368-cc88-4cab-b306-9bc2ae2b1970/image.png" width="50%" />
</p>


<hr>
<h2 id="제대로-설치되었는지-확인">제대로 설치되었는지 확인</h2>
<p>설치가 완료되면 다음 명령어로 Playwright 버전을 확인할 수 있습니다.</p>
<pre><code class="language-bash">npx playwright --version</code></pre>
<p>버전 정보가 출력되면 설치가 정상적으로 완료된 것입니다.
<img src="https://velog.velcdn.com/images/00kang_jh/post/066413b8-fe05-43e0-bc4e-77b5c061c692/image.png" alt=""></p>
<hr>
<h2 id="마치며">마치며</h2>
<p>터미널 명령어 몇 줄이면 Playwright 설치가 완료됩니다. 다음 글에서는 실제 테스트를 실행하고 코드를 작성하는 방법을 알아보겠습니다.</p>
<hr>
<blockquote>
<p>📚 <strong>참고 자료</strong></p>
<ul>
<li><a href="https://playwright.dev/docs/intro">Playwright - Installation</a></li>
<li><a href="https://playwright.dev/docs/running-tests">Playwright - Running Tests</a></li>
</ul>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Playwright, E2E 테스트 자동화 소개]]></title>
            <link>https://velog.io/@00kang_jh/Playwright-E2E-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%90%EB%8F%99%ED%99%94-%EC%86%8C%EA%B0%9C</link>
            <guid>https://velog.io/@00kang_jh/Playwright-E2E-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%90%EB%8F%99%ED%99%94-%EC%86%8C%EA%B0%9C</guid>
            <pubDate>Thu, 22 Jan 2026 00:52:26 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>웹 애플리케이션을 개발하다 보면 &quot;이 기능이 제대로 동작하는지 어떻게 확인하지?&quot;라는 고민을 하게 됩니다. 버튼을 클릭했을 때 페이지가 잘 이동하는지, 폼을 제출했을 때 데이터가 정상적으로 저장되는지, 매번 수동으로 확인하기엔 시간도 부족하고 실수할 가능성도 높죠.</p>
<p>이런 문제를 해결해주는 것이 바로 <strong>E2E(End-to-End) 테스트 자동화</strong>입니다. 그리고 오늘 소개할 <strong>Playwright</strong>는 이 영역에서 빠르게 주목받고 있는 도구입니다.</p>
<hr>
<h2 id="e2e-테스트란">E2E 테스트란?</h2>
<p>E2E 테스트는 사용자 관점에서 애플리케이션의 전체 흐름을 검증하는 테스트입니다.</p>
<p>예를 들어 쇼핑몰에서 &quot;로그인 → 상품 검색 → 장바구니 담기 → 결제&quot;까지의 과정이 정상적으로 동작하는지 확인하는 것이죠. 단위 테스트가 함수 하나하나를 검증한다면, E2E 테스트는 실제 사용자가 경험하는 시나리오 전체를 검증합니다.</p>
<p><strong>왜 필요할까요?</strong></p>
<ul>
<li>개별 컴포넌트는 정상이어도 통합했을 때 문제가 생길 수 있습니다</li>
<li>배포 전 주요 기능이 동작하는지 빠르게 확인할 수 있습니다</li>
<li>수동 테스트에 드는 반복적인 시간을 줄여줍니다</li>
</ul>
<hr>
<h2 id="playwright-소개">Playwright 소개</h2>
<p><strong>Playwright</strong>는 Microsoft에서 개발한 오픈소스 E2E 테스트 프레임워크입니다. 2020년에 처음 공개되었고, 빠른 속도와 안정성으로 Cypress, Selenium과 함께 대표적인 E2E 테스트 도구로 자리잡았습니다.</p>
<pre><code class="language-bash"># 설치
npm init playwright@latest</code></pre>
<p>설치 명령어 하나로 프로젝트 구조, 설정 파일, 예제 테스트까지 자동으로 생성해줍니다.</p>
<hr>
<h2 id="playwright의-주요-특징">Playwright의 주요 특징</h2>
<h3 id="1-크로스-브라우저-지원">1. 크로스 브라우저 지원</h3>
<p>Chromium, Firefox, WebKit 세 가지 브라우저 엔진을 단일 API로 제어할 수 있습니다. 하나의 테스트 코드로 Chrome, Safari, Firefox에서 모두 동작을 검증할 수 있다는 의미입니다.</p>
<pre><code class="language-javascript">// playwright.config.js
projects: [
  { name: &#39;chromium&#39;, use: { ...devices[&#39;Desktop Chrome&#39;] } },
  { name: &#39;firefox&#39;, use: { ...devices[&#39;Desktop Firefox&#39;] } },
  { name: &#39;webkit&#39;, use: { ...devices[&#39;Desktop Safari&#39;] } },
]</code></pre>
<h3 id="2-auto-waiting">2. Auto-waiting</h3>
<p>테스트 자동화에서 가장 골치 아픈 문제 중 하나가 타이밍입니다. 버튼이 아직 로딩 중인데 클릭하려고 하면 테스트가 실패하죠. Playwright는 요소가 클릭 가능한 상태가 될 때까지 자동으로 기다려줍니다.</p>
<pre><code class="language-javascript">// 별도의 wait 없이도 안정적으로 동작
await page.click(&#39;button#submit&#39;);</code></pre>
<h3 id="3-강력한-디버깅-도구">3. 강력한 디버깅 도구</h3>
<p>테스트가 실패했을 때 원인을 찾기 쉽도록 다양한 도구를 제공합니다.</p>
<ul>
<li><strong>Trace Viewer</strong>: 테스트 실행 과정을 스크린샷, DOM 스냅샷, 네트워크 요청과 함께 타임라인으로 확인</li>
<li><strong>Codegen</strong>: 브라우저에서 직접 조작하면 해당 동작을 코드로 자동 생성</li>
<li><strong>UI Mode</strong>: 테스트를 시각적으로 실행하고 각 단계를 확인</li>
</ul>
<pre><code class="language-bash"># Codegen 실행
npx playwright codegen https://example.com</code></pre>
<h3 id="4-네트워크-제어">4. 네트워크 제어</h3>
<p>API 요청을 가로채서 원하는 응답으로 대체하거나, 네트워크 상태를 시뮬레이션할 수 있습니다. 백엔드 없이도 프론트엔드 테스트가 가능해집니다.</p>
<pre><code class="language-javascript">await page.route(&#39;**/api/users&#39;, route =&gt; {
  route.fulfill({
    status: 200,
    body: JSON.stringify([{ id: 1, name: &#39;Test User&#39; }])
  });
});</code></pre>
<h3 id="5-병렬-실행">5. 병렬 실행</h3>
<p>테스트를 여러 워커에서 동시에 실행하여 전체 테스트 시간을 크게 단축할 수 있습니다.</p>
<hr>
<h2 id="playwright의-장점">Playwright의 장점</h2>
<h3 id="빠른-실행-속도">빠른 실행 속도</h3>
<p>브라우저와 직접 통신하는 방식을 사용해 Selenium 대비 체감될 정도로 빠릅니다. 특히 병렬 실행을 기본 지원하기 때문에 테스트 수가 많아져도 전체 실행 시간을 효율적으로 관리할 수 있습니다.</p>
<h3 id="안정적인-테스트">안정적인 테스트</h3>
<p>Auto-waiting과 자동 재시도(retry) 기능 덕분에 &quot;어제는 됐는데 오늘은 안 되는&quot; 불안정한 테스트(flaky test)가 줄어듭니다. 이건 실무에서 정말 큰 장점입니다.</p>
<h3 id="낮은-진입-장벽">낮은 진입 장벽</h3>
<p>Codegen으로 브라우저 조작을 녹화해서 코드를 생성할 수 있어, 처음 접하는 분들도 빠르게 테스트를 작성할 수 있습니다. 공식 문서도 잘 정리되어 있고 예제가 풍부합니다.</p>
<h3 id="활발한-생태계">활발한 생태계</h3>
<p>Microsoft의 지원 아래 꾸준히 업데이트되고 있고, VS Code 확장 프로그램도 제공합니다. GitHub Actions 등 CI/CD 파이프라인과의 연동도 간편합니다.</p>
<hr>
<h2 id="마치며">마치며</h2>
<p>E2E 테스트 자동화를 시작하려는 분들에게 Playwright는 좋은 선택지입니다. 설정이 간단하고, 디버깅 도구가 강력하며, 무엇보다 안정적으로 동작합니다.</p>
<p>다음 글에서는 실제로 간단한 테스트를 작성하고 실행하는 과정을 다뤄보겠습니다.</p>
<hr>
<blockquote>
<p>📚 <strong>참고 자료</strong></p>
<ul>
<li><a href="https://playwright.dev/">Playwright 공식 문서</a></li>
<li><a href="https://github.com/microsoft/playwright">Playwright GitHub</a></li>
</ul>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[🚀 AWS S3 이미지 업로드]]></title>
            <link>https://velog.io/@00kang_jh/AWS-S3-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C</link>
            <guid>https://velog.io/@00kang_jh/AWS-S3-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C</guid>
            <pubDate>Sat, 02 Aug 2025 06:07:02 GMT</pubDate>
            <description><![CDATA[<h1 id="🚀-aws-s3-이미지-업로드-구현-가이드">🚀 AWS S3 이미지 업로드 구현 가이드</h1>
<p>일반적으로 많은 프로젝트들에서 기본적으로 이미지 업로드 하는 기능들이 많이 들어갑니다.
그래서  AWS 기반으로 <strong>S3 + CloudFront + Presigned URL</strong> 조합을 사용하면 빠르고
안전하며 비용 효율적인 이미지 업로드를 구현할 수 있습니다.</p>
<hr>
<h2 id="1️⃣-이미지-업로드-아키텍처">1️⃣ 이미지 업로드 아키텍처</h2>
<p><strong>흐름</strong>  </p>
<ol>
<li>클라이언트 → 서버: 업로드 요청</li>
<li>서버 → AWS: Presigned URL 생성</li>
<li>서버 → 클라이언트: Presigned URL 전달</li>
<li>클라이언트 → S3: 이미지 직접 업로드</li>
<li>서버 → DB: 업로드 정보(경로, 파일명, 유저) 저장</li>
<li>조회 시: <strong>CloudFront URL</strong> 통해 이미지 접근</li>
</ol>
<blockquote>
<p>✅ 장점: 서버 부하 최소화 + 빠른 CDN 조회 + 보안 강화</p>
</blockquote>
<hr>
<h2 id="2️⃣-spring-boot-구현">2️⃣ Spring Boot 구현</h2>
<h3 id="🔹-presigned-url-생성">🔹 Presigned URL 생성</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class S3Service {
    private final AmazonS3 amazonS3;
    private final String bucketName = &quot;demo-images&quot;;

    public String generatePresignedUrl(String fileName) {
        Date expiration = new Date(System.currentTimeMillis() + 1000 * 60 * 5); // 5분 유효
        GeneratePresignedUrlRequest request =
            new GeneratePresignedUrlRequest(bucketName, fileName)
            .withMethod(HttpMethod.PUT)
            .withExpiration(expiration);
        return amazonS3.generatePresignedUrl(request).toString();
    }
}</code></pre>
<h3 id="🔹-클라이언트-업로드-react-native-예시">🔹 클라이언트 업로드 (React Native 예시)</h3>
<pre><code class="language-javascript">await axios.put(presignedUrl, file, {
  headers: { &quot;Content-Type&quot;: file.type }
});</code></pre>
<hr>
<h2 id="3️⃣-cloudfront--s3-구조">3️⃣ CloudFront + S3 구조</h2>
<ul>
<li><strong>S3 (Private)</strong>: 이미지 저장</li>
<li><strong>CloudFront</strong>: 전 세계 CDN 캐싱</li>
<li><strong>OAC(Origin Access Control)</strong>: S3 직접 접근 차단</li>
<li><strong>Signed URL(Optional)</strong>: 민감한 이미지 접근 제어
<img src="blob:https://velog.io/8a7be4c6-642e-494b-bf20-0186ec45d62e" alt="업로드중.."></li>
</ul>
<p><em>(예시 다이어그램: S3 + CloudFront + Presigned URL 구조)</em></p>
<hr>
<h2 id="4️⃣-보안-체크리스트">4️⃣ 보안 체크리스트</h2>
<p>업로드 서비스는 <strong>XSS·파일 위장·대용량 업로드</strong>에 취약할 수 있습니다.<br>아래 보안 전략을 적용하면 안전합니다.</p>
<ul>
<li><input disabled="" type="checkbox"> 허용 확장자만 업로드 (<code>jpg, png, webp</code>)</li>
<li><input disabled="" type="checkbox"> MIME 타입 검증 (<code>image/</code> 로 시작 여부)</li>
<li><input disabled="" type="checkbox"> 파일 크기 제한 (1~2MB)</li>
<li><input disabled="" type="checkbox"> UUID 기반 파일명 (사용자 입력 이름 사용 ❌)</li>
<li><input disabled="" type="checkbox"> S3 Private + Presigned URL 사용</li>
<li><input disabled="" type="checkbox"> CloudFront OAC 설정으로 직접 접근 차단</li>
<li><input disabled="" type="checkbox"> 필요 시 Signed URL or JWT 인증</li>
<li><input disabled="" type="checkbox"> 업로드 후 Lambda로 이미지 포맷 검증</li>
</ul>
<hr>
<h2 id="5️⃣-비용-최적화">5️⃣ 비용 최적화</h2>
<ul>
<li><strong>프리티어 사용 시</strong>  <ul>
<li>S3 5GB / CloudFront 1TB / 20,000 GET / 2,000 PUT 무료</li>
</ul>
</li>
<li><strong>월 사용량 예시 (300명 × 5장/일 × 1MB)</strong>  <ul>
<li>45GB 저장, 45,000 PUT → 월 $2~3 수준</li>
</ul>
</li>
<li><strong>비용 절감 방법</strong>  <ul>
<li>업로드 전 <strong>리사이징/압축</strong>  </li>
<li>오래된 파일 <strong>S3 Glacier로 이동</strong></li>
</ul>
</li>
</ul>
<hr>
<h2 id="6️⃣-최종-정리">6️⃣ 최종 정리</h2>
<blockquote>
<p><strong>S3 + CloudFront + Presigned URL</strong><br>구조를 적용하면 서버 부하를 최소화하고,<br>보안·속도·비용 모든 면에서 효율적인 구조를 만들 수 있습니다.</p>
</blockquote>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[미션 DAY4 (워밍업 클럽 3기)]]></title>
            <link>https://velog.io/@00kang_jh/%EB%AF%B8%EC%85%98-DAY4-%EC%9B%8C%EB%B0%8D%EC%97%85-%ED%81%B4%EB%9F%BD-3%EA%B8%B0</link>
            <guid>https://velog.io/@00kang_jh/%EB%AF%B8%EC%85%98-DAY4-%EC%9B%8C%EB%B0%8D%EC%97%85-%ED%81%B4%EB%9F%BD-3%EA%B8%B0</guid>
            <pubDate>Fri, 07 Mar 2025 12:29:57 GMT</pubDate>
            <description><![CDATA[<h1 id="📌-day-4-코드-리팩토링--solid-원칙-정리">📌 Day 4: 코드 리팩토링 &amp; SOLID 원칙 정리</h1>
<p>이번 미션에서는 <strong>더 읽기 좋은 코드</strong>를 만들기 위해 기존 코드를 리팩토링하고, <strong>SOLID 원칙</strong>을 자기만의 언어로 정리하는 작업을 수행했다.  </p>
<hr>
<h2 id="✅-미션-수행-목표">✅ <strong>미션 수행 목표</strong></h2>
<h3 id="🔹-1-코드-리팩토링">🔹 1. 코드 리팩토링</h3>
<ul>
<li>[섹션 3. 논리, 사고의 흐름]에서 이야기하는 내용을 중심으로 리팩토링  </li>
<li>코드의 가독성을 높이고, 유지보수를 쉽게 만들기  </li>
</ul>
<h3 id="🔹-2-solid-원칙-정리">🔹 2. SOLID 원칙 정리</h3>
<ul>
<li>SOLID 원칙을 이해하고, 이를 나만의 언어로 정리  </li>
</ul>
<hr>
<h1 id="📌-1️⃣-코드-리팩토링-과정">📌 1️⃣ 코드 리팩토링 과정</h1>
<h3 id="🕒-step-1-기존-코드-분석">🕒 <strong>[Step 1] 기존 코드 분석</strong></h3>
<p>다음은 주어진 <code>validateOrder</code> 메서드의 기존 코드이다.  </p>
<p>📌 <strong>기존 코드</strong>  </p>
<pre><code class="language-java">public boolean validateOrder(Order order) {
    if (order.getItems().size() == 0) {
        log.info(&quot;주문 항목이 없습니다.&quot;);
        return false;
    } else {
        if (order.getTotalPrice() &gt; 0) {
            if (!order.hasCustomerInfo()) {
                log.info(&quot;사용자 정보가 없습니다.&quot;);
                return false;
            } else {
                return true;
            }
        } else if (!(order.getTotalPrice() &gt; 0)) {
            log.info(&quot;올바르지 않은 총 가격입니다.&quot;);
            return false;
        }
    }
    return true;
}</code></pre>
<p>🔍 <strong>분석 결과</strong>  </p>
<ul>
<li><strong><code>getter</code>를 직접 호출하는 부분이 많음</strong> → 캡슐화를 해치고 있음  </li>
<li><strong>부정 연산자(<code>!</code>, <code>!=</code>) 사용이 많음</strong> → 코드의 직관성이 떨어짐  </li>
<li><strong>중첩된 조건문이 많음</strong> → 가독성이 낮음  </li>
</ul>
<hr>
<h3 id="🕒-step-2-리팩토링-방향-설정">🕒 <strong>[Step 2] 리팩토링 방향 설정</strong></h3>
<p>리팩토링 시 고려할 사항:<br>✔️ <code>getter</code> 사용을 최소화하기 위해 <strong>Order 내부에 메서드를 추가</strong><br>✔️ <code>부정 연산자(!)</code>를 제거하여 가독성을 높임<br>✔️ <strong>불필요한 else 문을 제거</strong>하여 코드 흐름을 단순화  </p>
<hr>
<h3 id="🕒-step-3-order-클래스-개선">🕒 <strong>[Step 3] Order 클래스 개선</strong></h3>
<p><code>Order</code> 클래스 내부에 필요한 메서드를 추가하여 <strong>객체 스스로 판단할 수 있도록 수정</strong>  </p>
<p>📌 <strong>수정된 Order 클래스</strong>  </p>
<pre><code class="language-java">import java.util.List;

public class Order {

  private final List&lt;Integer&gt; items;
  private final int totalPrice;
  private final int customer;

  public Order(List&lt;Integer&gt; items, int totalPrice, int customer) {
    this.items = items;
    this.totalPrice = totalPrice;
    this.customer = customer;
  }

  public boolean isEmpty() {
    return items.isEmpty();
  }

  public boolean hasCustomerInfo() {
    return customer != 0;
  }

  public boolean moreThanZeroTotalPrice() {
    return totalPrice &gt; 0;
  }
}
</code></pre>
<hr>
<h3 id="🕒-step-4-리팩토링된-validateorder-메서드">🕒 <strong>[Step 4] 리팩토링된 <code>validateOrder</code> 메서드</strong></h3>
<p>리팩토링된 <code>validateOrder</code>는 <strong>더 직관적이고 읽기 쉬운 코드</strong>가 되었다.  </p>
<p>📌 <strong>수정된 validateOrder 메서드</strong>  </p>
<pre><code class="language-java">public class MissionDay4 {

  public boolean validateOrder(Order order) {
    if (order.isEmpty()) {
      log.info(&quot;주문 항목이 없습니다.&quot;);
      return false;
    }

    if (order.moreThanZeroTotalPrice()) {
      if (order.hasCustomerInfo()) {
        return true;
      }
      log.info(&quot;사용자 정보가 없습니다.&quot;);
      return false;
    }

    log.info(&quot;올바르지 않은 총 가격입니다.&quot;);
    return false;
  }
}

</code></pre>
<hr>
<h2 id="🔍-리팩토링-결과-정리">🔍 <strong>리팩토링 결과 정리</strong></h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>리팩토링 전</th>
<th>리팩토링 후</th>
</tr>
</thead>
<tbody><tr>
<td><strong>getter 사용</strong></td>
<td>직접 호출 (<code>order.getTotalPrice()</code>)</td>
<td>Order 내부에서 메서드 제공 (<code>order.moreThanZeroTotalPrice()</code>)</td>
</tr>
<tr>
<td><strong>부정 연산자</strong></td>
<td><code>!order.hasCustomerInfo()</code> 사용</td>
<td><code>order.hasCustomerInfo()</code>로 긍정 표현</td>
</tr>
<tr>
<td><strong>조건문 구조</strong></td>
<td>중첩된 <code>if-else</code> 다수 포함</td>
<td>단순한 <code>if</code> 구조로 개선</td>
</tr>
<tr>
<td><strong>가독성</strong></td>
<td>흐름 파악이 어려움</td>
<td>더 직관적인 코드</td>
</tr>
</tbody></table>
<hr>
<h1 id="📌-2️⃣-solid-원칙-정리">📌 2️⃣ SOLID 원칙 정리</h1>
<p><strong>SOLID 원칙</strong>은 객체 지향 프로그래밍에서 <strong>유지보수성과 확장성을 높이기 위한 설계 원칙</strong>이다.<br>아래는 내가 이해한 SOLID 원칙을 나만의 언어로 정리한 내용이다.  </p>
<h3 id="✅-1-단일-책임-원칙-srp-single-responsibility-principle">✅ <strong>1. 단일 책임 원칙 (SRP, Single Responsibility Principle)</strong></h3>
<p><strong>&quot;하나의 클래스는 하나의 책임만 가져야 한다.&quot;</strong>  </p>
<ul>
<li>한 클래스가 너무 많은 역할을 맡으면 <strong>변경이 어려워짐</strong>  </li>
<li>하나의 책임만 있도록 <strong>기능을 나누는 것이 중요</strong>  </li>
</ul>
<h3 id="✅-2-개방-폐쇄-원칙-ocp-openclosed-principle">✅ <strong>2. 개방-폐쇄 원칙 (OCP, Open/Closed Principle)</strong></h3>
<p><strong>&quot;확장은 가능하지만, 기존 코드는 수정하지 않는다.&quot;</strong>  </p>
<ul>
<li>새로운 기능이 필요하면 <strong>기존 코드를 수정하는 것이 아니라, 확장해야 함</strong>  </li>
<li>인터페이스, 추상 클래스를 활용하면 유연한 코드 작성 가능  </li>
</ul>
<h3 id="✅-3-리스코프-치환-원칙-lsp-liskov-substitution-principle">✅ <strong>3. 리스코프 치환 원칙 (LSP, Liskov Substitution Principle)</strong></h3>
<p><strong>&quot;하위 클래스는 상위 클래스를 대체할 수 있어야 한다.&quot;</strong>  </p>
<ul>
<li>부모 클래스를 상속받은 자식 클래스가 <strong>부모처럼 동작해야 함</strong>  </li>
<li>그렇지 않으면 다형성을 활용할 수 없음  </li>
</ul>
<h3 id="✅-4-인터페이스-분리-원칙-isp-interface-segregation-principle">✅ <strong>4. 인터페이스 분리 원칙 (ISP, Interface Segregation Principle)</strong></h3>
<p><strong>&quot;필요한 기능만 포함된 인터페이스를 사용하라.&quot;</strong>  </p>
<ul>
<li>한 개의 큰 인터페이스보다는, <strong>작은 단위의 인터페이스를 여러 개 만드는 것이 좋음</strong>  </li>
<li>필요하지 않은 기능까지 구현하지 않아도 됨  </li>
</ul>
<h3 id="✅-5-의존-역전-원칙-dip-dependency-inversion-principle">✅ <strong>5. 의존 역전 원칙 (DIP, Dependency Inversion Principle)</strong></h3>
<p><strong>&quot;구체적인 것보다, 추상적인 것에 의존하라.&quot;</strong>  </p>
<ul>
<li>클래스가 다른 클래스를 직접 참조하는 것이 아니라, <strong>인터페이스나 추상 클래스를 통해 의존</strong>  </li>
<li>변경에 유연한 구조를 만들 수 있음  </li>
</ul>
<hr>
<h2 id="📌-출처">📌 출처</h2>
<p>📖 <strong>강의 제목:</strong> <a href="https://www.inflearn.com/course/readable-code-%EC%9D%BD%EA%B8%B0%EC%A2%8B%EC%9D%80%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%EC%82%AC%EA%B3%A0%EB%B2%95">Readable Code: 읽기 좋은 코드를 작성하는 사고법</a><br>👩‍🏫 <strong>강사:</strong> 박우빈  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQLD 공부 (2)]]></title>
            <link>https://velog.io/@00kang_jh/SQLD-%EA%B3%B5%EB%B6%80-2</link>
            <guid>https://velog.io/@00kang_jh/SQLD-%EA%B3%B5%EB%B6%80-2</guid>
            <pubDate>Tue, 04 Mar 2025 06:53:08 GMT</pubDate>
            <description><![CDATA[<h1 id="📌-entity-attribute-instance-개념-정리">📌 Entity, Attribute, Instance 개념 정리</h1>
<p>데이터 모델링에서 <strong>Entity(개체), Attribute(속성), Instance(인스턴스)</strong> 개념을 이해하는 것이 중요하다. 이를 쉽게 설명하기 위해 간단한 예제와 함께 살펴보자.  </p>
<hr>
<h2 id="🔹-개념-정리">🔹 개념 정리</h2>
<ul>
<li><strong>Entity (개체):</strong> 관리할 대상이 되는 데이터 (예: 학생, 강의, 도서 등)  </li>
<li><strong>Attribute (속성):</strong> 개체가 가지는 정보 (예: 학번, 이름, 학년 등)  </li>
<li><strong>Instance (인스턴스):</strong> 개체(Entity)의 실제 데이터 값 (예: 학번 <code>2023001</code>, 이름 <code>김철수</code>)  </li>
</ul>
<blockquote>
<p><strong>Entity는 데이터를 저장하는 개체, Attribute는 개체가 가지는 속성, Instance는 실제 데이터를 의미한다.</strong></p>
</blockquote>
<hr>
<h2 id="🎯-이해하기-쉬운-비유">🎯 이해하기 쉬운 비유</h2>
<p>📦 <strong>택배 시스템으로 설명하면?</strong>  </p>
<ul>
<li><strong>Entity (개체) → &quot;택배 상자&quot;</strong>  </li>
<li><strong>Attribute (속성) → &quot;택배 상자에 붙어 있는 송장 (수령인, 주소, 무게)&quot;</strong>  </li>
<li><strong>Instance (인스턴스) → &quot;실제 배송 중인 특정 택배&quot;</strong>  </li>
</ul>
<p>이렇게 비유하면 <strong>Entity, Attribute, Instance</strong> 개념이 더 쉽게 이해될 것이다! 🚀  </p>
<hr>
<h1 id="🔹-entity-엔터티">🔹 Entity (엔터티)</h1>
<blockquote>
<p><strong>&quot;식별 가능한 객체&quot;</strong>  </p>
</blockquote>
<h3 id="✅-엔터티의-특징">✅ 엔터티의 특징</h3>
<ol>
<li><strong>업무에서 사용되는 정보여야 함</strong>  </li>
<li><strong>유일성을 보장하는 식별자(Primary Key)가 있어야 함</strong>  </li>
<li><strong>두 개 이상의 인스턴스(Row)를 가져야 함</strong>  </li>
<li><strong>반드시 속성(Attribute)을 포함해야 함</strong>  </li>
<li><strong>다른 엔터티와 하나 이상의 관계(Relationship)를 가져야 함</strong>  </li>
</ol>
<hr>
<h2 id="🔹-엔터티의-분류">🔹 엔터티의 분류</h2>
<h3 id="1️⃣-유형에-따른-분류">1️⃣ <strong>유형에 따른 분류</strong></h3>
<ul>
<li><strong>유형(Entity):</strong> 물리적인 형태가 존재 (예: <code>학생</code>, <code>도서</code>)  </li>
<li><strong>개념(Entity):</strong> 물리적 형태 없이 개념적으로 존재 (예: <code>강의 계획</code>)  </li>
<li><strong>사건(Entity):</strong> 행위를 통해 발생하며, 빈번하고 통계 자료로 활용 가능 (예: <code>주문 내역</code>)  </li>
</ul>
<h3 id="2️⃣-발생-시점에-따른-분류">2️⃣ <strong>발생 시점에 따른 분류</strong></h3>
<ul>
<li><strong>기본 엔터티:</strong> 원래 존재하는 독립적인 정보, 자식 엔터티를 가질 수 있음  </li>
<li><strong>중심 엔터티:</strong> 기본 엔터티에서 파생되며, 데이터 양이 많고 업무에서 중심적인 역할 수행  </li>
<li><strong>행위 엔터티:</strong> 두 개 이상의 엔터티에서 파생되며, 데이터의 수정과 증가가 잦음  </li>
</ul>
<hr>
<h1 id="🔹-attribute-속성">🔹 Attribute (속성)</h1>
<blockquote>
<p><strong>&quot;객체의 특징&quot;</strong>  </p>
</blockquote>
<p>예를 들어, &quot;사람&quot;이라는 객체가 있다면 <strong>이름, 나이, 생년월일</strong> 등 다양한 특징이 존재할 것이다. 이처럼 개체(Entity)의 특정 정보를 나타내는 요소를 <strong>속성(Attribute)</strong>이라고 한다.  </p>
<hr>
<h2 id="🔹-속성의-분류">🔹 속성의 분류</h2>
<h3 id="1️⃣-특성에-따른-분류">1️⃣ <strong>특성에 따른 분류</strong></h3>
<ul>
<li><strong>기본 속성 (Basic Attribute):</strong> 업무 프로세스에서 바로 정의할 수 있는 속성  </li>
<li><strong>설계 속성 (Designed Attribute):</strong> 업무에 존재하지 않지만 설계 시 필요하다고 판단되는 속성 (예: <code>고유번호</code>)  </li>
<li><strong>파생 속성 (Derived Attribute):</strong> 다른 속성의 값을 계산하거나 특정 규칙으로 변형하여 생성하는 속성  </li>
</ul>
<h3 id="2️⃣-구성-방식에-따른-분류">2️⃣ <strong>구성 방식에 따른 분류</strong></h3>
<ul>
<li><strong>PK (Primary Key) 속성:</strong> 엔터티의 인스턴스를 식별할 수 있는 속성  </li>
<li><strong>FK (Foreign Key) 속성:</strong> 다른 엔터티의 속성에서 가져온 속성  </li>
<li><strong>일반 속성:</strong> PK, FK를 제외한 나머지 속성  </li>
</ul>
<hr>
<h2 id="🔹-도메인-domain">🔹 도메인 (Domain)</h2>
<blockquote>
<p><strong>&quot;속성이 가질 수 있는 값의 범위&quot;</strong>  </p>
</blockquote>
<p>예를 들어, <code>성별</code> 속성의 도메인은 <strong>[남, 여]</strong> 와 같이 미리 정의된 값의 범위를 가진다.  </p>
<hr>
<p>📖 <strong>참고 도서:</strong> 《SQL개발자 SQLD 과외노트》 - 정미나, 시대에듀  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQLD 공부 (1)]]></title>
            <link>https://velog.io/@00kang_jh/SQLD-%EA%B3%B5%EB%B6%80-1</link>
            <guid>https://velog.io/@00kang_jh/SQLD-%EA%B3%B5%EB%B6%80-1</guid>
            <pubDate>Sat, 01 Mar 2025 02:02:23 GMT</pubDate>
            <description><![CDATA[<h1 id="📌-데이터-모델의-이해">📌 데이터 모델의 이해</h1>
<h2 id="🔹-모델링modeling이란">🔹 모델링(Modeling)이란?</h2>
<blockquote>
<p><strong>&quot;현실 세계를 단순화하여 표현하는 기법&quot;</strong></p>
</blockquote>
<h3 id="✅-모델링의-3가지-조건">✅ 모델링의 3가지 조건</h3>
<ol>
<li><strong>현실 세계를 반영해야 함</strong>  </li>
<li><strong>단순화하여 표현해야 함</strong>  </li>
<li><strong>관찰하고자 하는 데이터를 모델로 설계해야 함</strong>  </li>
</ol>
<hr>
<h2 id="🔹-모델링의-3가지-특징">🔹 모델링의 3가지 특징</h2>
<h3 id="1️⃣-추상화abstraction">1️⃣ <strong>추상화(Abstraction)</strong></h3>
<ul>
<li>현실 세계를 일정한 형식으로 표현하는 과정  </li>
<li>아이디어나 개념을 간략하게 정리  </li>
</ul>
<h3 id="2️⃣-단순화simplification">2️⃣ <strong>단순화(Simplification)</strong></h3>
<ul>
<li>복잡한 현실 세계를 정해진 표기법으로 쉽게 표현  </li>
</ul>
<h3 id="3️⃣-명확화clarity">3️⃣ <strong>명확화(Clarity)</strong></h3>
<ul>
<li>불분명한 요소를 제거하여 해석이 명확하게 가능하도록 표현  </li>
</ul>
<hr>
<h2 id="🔹-모델링의-3가지-관점">🔹 모델링의 3가지 관점</h2>
<h3 id="1️⃣-데이터-관점-data-perspective">1️⃣ <strong>데이터 관점 (Data Perspective)</strong></h3>
<ul>
<li><strong>데이터 중심</strong>의 모델링  </li>
<li>업무에 연관된 데이터와 <strong>데이터 간의 관계</strong>를 분석  </li>
</ul>
<h3 id="2️⃣-프로세스-관점-process-perspective">2️⃣ <strong>프로세스 관점 (Process Perspective)</strong></h3>
<ul>
<li><strong>업무 프로세스 중심</strong>의 모델링  </li>
<li>현재 또는 미래의 업무 처리 방식에 초점  </li>
</ul>
<h3 id="3️⃣-데이터와-프로세스의-상관-관점-interaction-perspective">3️⃣ <strong>데이터와 프로세스의 상관 관점 (Interaction Perspective)</strong></h3>
<ul>
<li><strong>데이터와 프로세스의 관계</strong>를 분석  </li>
<li>프로세스의 흐름에 따라 데이터가 어떻게 변하는지 모델링  </li>
</ul>
<hr>
<h2 id="🔹-모델링의-3가지-단계">🔹 모델링의 3가지 단계</h2>
<h3 id="1️⃣-개념적-데이터-모델링-conceptual-data-modeling">1️⃣ 개념적 데이터 모델링 (Conceptual Data Modeling)</h3>
<ul>
<li><strong>가장 추상화 수준이 높은 모델링 단계</strong>  </li>
<li>기업(전사) 차원의 데이터를 분석하고, <strong>업무 중심적이고 포괄적인 모델링</strong>을 수행  </li>
<li>개체(Entity)와 그 관계(Relationship)를 정의하는 단계  </li>
</ul>
<h3 id="2️⃣-논리적-데이터-모델링-logical-data-modeling">2️⃣ 논리적 데이터 모델링 (Logical Data Modeling)</h3>
<ul>
<li><strong>데이터베이스를 설계하기 위한 핵심 모델링 단계</strong>  </li>
<li><strong>재사용성이 가장 높은 모델링</strong>으로,  <ul>
<li>Key(키), 속성(Attribute), 관계(Relationship) 등을 상세하게 정의  </li>
</ul>
</li>
<li>DBMS에 <strong>독립적인</strong> 설계가 가능  </li>
</ul>
<h3 id="3️⃣-물리적-데이터-모델링-physical-data-modeling">3️⃣ 물리적 데이터 모델링 (Physical Data Modeling)</h3>
<ul>
<li><strong>실제 데이터베이스로 구현할 수 있도록 성능과 가용성을 고려하는 단계</strong>  </li>
<li>데이터의 <strong>물리적 저장 구조(테이블, 컬럼, 인덱스 등)</strong>를 정의  </li>
<li>DBMS의 특성을 반영하여 최적화 진행  </li>
</ul>
<hr>
<h2 id="🔹-데이터의-독립성-data-independence">🔹 데이터의 독립성 (Data Independence)</h2>
<blockquote>
<p><strong>&quot;ANSI-SPARC 아키텍처는 DBMS의 추상적인 설계 표준이다.&quot;</strong><br>✅ <strong>총 3단계의 스키마 구조를 갖고 있음</strong>  </p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/17e29daf-7924-4eb2-b65c-6a5ee2f38b13/image.png" alt="ANSI-SPARC 아키텍처"></p>
<h3 id="1️⃣-외부-스키마-external-schema">1️⃣ 외부 스키마 (External Schema)</h3>
<ul>
<li><strong>사용자 관점 (User View)</strong>  </li>
<li>사용자가 데이터베이스를 보는 관점에 따라 <strong>각기 다른 스키마</strong>를 정의  </li>
<li>여러 개의 외부 스키마가 존재할 수 있음 (Multiple User&#39;s View)  </li>
</ul>
<h3 id="2️⃣-개념-스키마-conceptual-schema">2️⃣ 개념 스키마 (Conceptual Schema)</h3>
<ul>
<li><strong>통합된 관점 (Community View of DB)</strong>  </li>
<li>데이터베이스 전체 구조를 나타내는 스키마  </li>
<li><strong>모든 사용자</strong>가 공유하는 <strong>데이터의 구조 및 관계</strong>를 정의  </li>
</ul>
<h3 id="3️⃣-내부-스키마-internal-schema">3️⃣ 내부 스키마 (Internal Schema)</h3>
<ul>
<li><strong>물리적인 관점 (Physical Representation)</strong>  </li>
<li>데이터의 <strong>저장 방식, 인덱스, 컬럼 정의</strong> 등을 포함  </li>
<li>실제 물리적인 저장 구조를 나타냄  </li>
</ul>
<h3 id="🔸-데이터-독립성이-보장하는-2가지-원칙">🔸 데이터 독립성이 보장하는 2가지 원칙</h3>
<ol>
<li><strong>논리적 독립성 (Logical Independence)</strong>  <ul>
<li><strong>개념 스키마</strong>가 변경되어도 <strong>외부 스키마(사용자 관점)는 영향을 받지 않음</strong>  </li>
</ul>
</li>
<li><strong>물리적 독립성 (Physical Independence)</strong>  <ul>
<li><strong>내부 스키마</strong>(저장 구조)가 변경되어도 <strong>외부/개념 스키마는 영향을 받지 않음</strong>  </li>
</ul>
</li>
</ol>
<hr>
<h2 id="🔹-erd-entity-relationship-diagram">🔹 ERD (Entity Relationship Diagram)</h2>
<blockquote>
<p><strong>&quot;어떤 개체(Entity)들이 존재하고, 그들 간에 어떤 관계(Relationship)가 있는지를 시각적으로 표현한 다이어그램&quot;</strong></p>
</blockquote>
<p>✅ <strong>ERD의 주요 구성 요소</strong></p>
<ul>
<li><strong>Entity(개체):</strong> 데이터를 저장하는 주요 단위 (예: 고객, 주문, 제품)  </li>
<li><strong>Attribute(속성):</strong> 개체가 가지는 정보 (예: 고객 이름, 주문 날짜)  </li>
<li><strong>Relationship(관계):</strong> 개체 간의 연관성 (예: 고객은 주문을 한다)  </li>
</ul>
<p>📝 <strong>이 단계에서는 개념을 큰 틀에서 이해하는 것이 중요함!</strong>  </p>
<hr>
<p>📖 <strong>참고 도서:</strong> 《SQL개발자 SQLD 과외노트》 - 정미나, 시대에듀 </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CodingTest_Day1_Array]]></title>
            <link>https://velog.io/@00kang_jh/CodingTestDay1Array</link>
            <guid>https://velog.io/@00kang_jh/CodingTestDay1Array</guid>
            <pubDate>Thu, 02 Jan 2025 14:12:23 GMT</pubDate>
            <description><![CDATA[<p>이번 주제는 Array 배열입니다. 우리는 이미 잘 알고 있기에 복습한다고 생각하시면 좋을 것 같습니다.</p>
<hr>
<h2 id="array배열의-기본-개념">Array(배열)의 기본 개념</h2>
<h4 id="배열이란">배열이란?</h4>
<p><strong>&quot;동일한 타입의 데이터를 연속된 메모리 공간에 저장하는 자료구조&quot;</strong> 라고 정의할 수 있습니다.</p>
<h4 id="배열의-장점과-단점">배열의 장점과 단점</h4>
<p>장점 : 빠른 접근 속도 (<strong>O(1)</strong>로 인덱스 접근이 가능합니다.)
단점 : 크기가 고정되어 있으며, 삽입 or 삭제 시 성능이 저하됩니다.</p>
<hr>
<h2 id="array배열의-연산">Array(배열)의 연산</h2>
<h4 id="배열-생성-및-초기화">배열 생성 및 초기화</h4>
<pre><code class="language-java">int[] arr = new int[5]; // 크기가 5인 배열 생성
int[] arr2 = {1, 2, 3, 4, 5}; // 초기화된 배열</code></pre>
<h4 id="요소-접근-및-수정">요소 접근 및 수정</h4>
<pre><code class="language-java">arr[0] = 10; // 배열 첫 번째 요소를 10으로 수정
System.out.println(arr[0]); // 출력: 10</code></pre>
<h4 id="다차원-배열">다차원 배열</h4>
<pre><code class="language-java">int[][] matrix = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};
System.out.println(matrix[0][1]); // 출력: 2</code></pre>
<h4 id="가변-배열-jagged-array">가변 배열 (Jagged Array)</h4>
<pre><code class="language-java">int[][] jagged = new int[3][];
jagged[0] = new int[2]; // 첫 번째 행의 길이: 2
jagged[1] = new int[3]; // 두 번째 행의 길이: 3
jagged[2] = new int[1]; // 세 번째 행의 길이: 1</code></pre>
<hr>
<h2 id="array배열의-알고리즘적-활용">Array(배열)의 알고리즘적 활용</h2>
<h4 id="정렬오름차순내림차순">정렬(오름차순/내림차순)</h4>
<pre><code class="language-java">Arrays.sort(arr); // 오름차순 정렬
Arrays.sort(arr, Collections.reverseOrder()); // 내림차순 정렬 (Integer[] 필요)</code></pre>
<h4 id="탐색linear-search-binary-search-최대값최소값-찾기">탐색(Linear Search, Binary Search, 최대값/최소값 찾기)</h4>
<pre><code class="language-java">//선형 탐색(Linear Search)
int key = 3;
for (int i = 0; i &lt; arr.length; i++) {
    if (arr[i] == key) {
        System.out.println(&quot;Found at index: &quot; + i);
    }
}</code></pre>
<pre><code class="language-java">//이진 탐색 (Binary Search)
int index = Arrays.binarySearch(arr, key); // 정렬된 배열에 사용
System.out.println(&quot;Index: &quot; + index);
</code></pre>
<pre><code class="language-java">//최대값 찾기, 최소값은 if문만 변경해서 작성하면 됨.
int max = arr[0];
for (int i = 1; i &lt; arr.length; i++) {
    if (arr[i] &gt; max) max = arr[i];
}
System.out.println(&quot;Max: &quot; + max);
</code></pre>
<hr>
<h2 id="array와-arraylist의-차이">Array와 ArrayList의 차이</h2>
<ul>
<li>배열은 크기가 고정되지만, ArrayList는 동적으로 크기가 변경됩니다.</li>
<li>배열은 기본 자료형을 저장할 수 있지만, ArrayList는 객체만 저장 가능합니다.</li>
</ul>
<hr>
<p>빠르고 간단하게 Array에 대해서 review 해봤습니다. 오늘도 error / issue 없는 하루 되시길 바랍니다. 감사합니다 :) </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Coding Test 공부 개요]]></title>
            <link>https://velog.io/@00kang_jh/Coding-Test-%EA%B3%B5%EB%B6%80-%EA%B0%9C%EC%9A%94</link>
            <guid>https://velog.io/@00kang_jh/Coding-Test-%EA%B3%B5%EB%B6%80-%EA%B0%9C%EC%9A%94</guid>
            <pubDate>Wed, 01 Jan 2025 06:01:44 GMT</pubDate>
            <description><![CDATA[<p>4년의 학부생활은 마쳤고 취업을 위해서 할 수 있는 것들 중 하나인 coding Test 준비를 해보려고 합니다. coding Test는 어쩌면 가장 개발자다운 면접이라고 생각합니다. 또 가장 흔하게도 보이는 방식이면서도 가장 어려운 방식이기도 합니다. 무엇이든 짧은 시간에 이루어내는 것이 어렵습니다. 하지만 이 어려운 걸 이번 기회에 해보려고 합니다. 뭐든 노력해서 안되는 것은 없다. </p>
<ol>
<li>자료구조 공부 및 문제 풀이</li>
<li>빅오 표기법 (big-O nation) 익히기</li>
<li>알고리즘 공부 및 문제 풀이</li>
<li>알고리즘 분석</li>
</ol>
<p>다음과 같은 순서로 진행하려고 합니다.</p>
<hr>
<h3 id="1-자료구조data-structure-공부-및-문제-풀이">1. 자료구조(Data structure) 공부 및 문제 풀이</h3>
<p>앞으로 공부할 자료구조는 <strong>Array, String, Stack, Queue, Priority Queue, Deque, Hash</strong> 정도 입니다. 자바로 coding test를 준비할 예정이기 때문에 *<em>이론 공부 -&gt; 자바 내장 클래스의 메소드 공부 -&gt; 백준 문제 풀이 *</em> 순서로 진행할 예정입니다. </p>
<h4 id="--이론-공부-간단-요약">- 이론 공부 간단 요약</h4>
<pre><code>Array: 배열의 구조와 특징, 탐색 및 정렬 알고리즘
String: 문자열의 처리 방법, 다양한 문자열 메소드
Stack: LIFO 구조, 다양한 활용 예시
Queue: FIFO 구조, 일반 큐와 원형 큐의 차이점
Priority Queue: 우선순위 큐의 개념과 활용
Deque: 양쪽 끝에서 삽입과 삭제가 가능한 자료구조
Hash: 해시 테이블의 원리, 충돌 해결 방식</code></pre><h4 id="--자바-내장-클래스--간단-요약">- 자바 내장 클래스  간단 요약</h4>
<pre><code>Array: Arrays 클래스의 메소드 (정렬, 검색 등)
String: String 클래스의 주요 메소드
Stack: Stack 클래스 (push, pop, peek 등)
Queue: Queue 인터페이스와 LinkedList 또는 ArrayDeque 구현
Priority Queue: PriorityQueue 클래스
Deque: Deque 인터페이스와 ArrayDeque 클래스
Hash: HashMap, HashSet 클래스의 메소드</code></pre><p>백준 문제 풀이의 경우에는 하루 평균 3~5문제 풀 예정입니다.</p>
<hr>
<h3 id="2-빅오-표기법-big-o-nation-익히기">2. 빅오 표기법 (big-O nation) 익히기</h3>
<p>알고리즘 공부하기 앞서서 빅오 표기법을 공부하려고 합니다. 해당 표기법은 알고리즘에서 가장 기초가 되는 부분으로 알고리즘의 시간 복잡도, 공간 복잡도를 나타낼 수 있는 수학적 기법입니다. 공부해야하는 이유는 효율성 평가, 최적화, 문제 해결, 면접 준비에 필요하고 필수적이기 때문입니다.</p>
<pre><code>효율성 평가: 알고리즘의 성능을 평가하고 다른 알고리즘과 비교할 수 있습니다.
최적화: 성능을 개선하기 위해 어떤 부분을 최적화해야 하는지 이해할 수 있습니다.
문제 해결: 특정 문제에 적합한 알고리즘을 선택하는 데 도움을 줍니다.
면접 준비: 많은 코딩 테스트 인터뷰에서 알고리즘의 복잡도를 물어보므로 필수적인 지식입니다.</code></pre><p>기본 개념 정리 후 실제 코드를 보면서 시간 복잡도, 공간 복잡도를 구해보면서 공부할 예정입니다.</p>
<hr>
<h3 id="3-알고리즘-공부-및-문제-풀이">3. 알고리즘 공부 및 문제 풀이</h3>
<p>앞으로 공부할 알고리즘에 종류에서 정<strong>렬(quick sort, merge sort), 탐색(binary search, DFS, BFS), 재귀, DP, Graph</strong>를 공부할 예정입니다. 해당 공부는 <strong>이론 공부 -&gt; 백준 문제(실버) -&gt; 백준 문제(골드)</strong> 순서로 진행할 예정입니다. 하루 평균 3~5문제정도 풀이할 예정입니다.</p>
<h4 id="--이론-공부-간단-요약-1">- 이론 공부 간단 요약</h4>
<ol>
        <li>
            <strong>정렬 알고리즘</strong>
            <ul>
                <li>퀵 정렬: 평균 O(n log n), 분할 정복 방식.</li>
                <li>병합 정렬: O(n log n), 안정적인 정렬.</li>
            </ul>
        </li>
        <li>
            <strong>탐색 알고리즘</strong>
            <ul>
                <li>이진 탐색: O(log n), 정렬된 배열에서 특정 원소 찾기.</li>
                <li>DFS (깊이 우선 탐색): 재귀적 또는 스택을 이용한 탐색.</li>
                <li>BFS (너비 우선 탐색): 큐를 이용하여 레벨별 탐색.</li>
            </ul>
        </li>
        <li>
            <strong>재귀</strong>
            <ul>
                <li>자기 자신을 호출하여 문제를 해결하는 기법.</li>
                <li>기본 구조와 종료 조건 이해.</li>
            </ul>
        </li>
        <li>
            <strong>동적 프로그래밍 (DP)</strong>
            <ul>
                <li>문제를 더 작은 하위 문제로 나누어 해결.</li>
                <li>메모이제이션 또는 타뷸레이션 기법 사용.</li>
            </ul>
        </li>
        <li>
            <strong>그래프</strong>
            <ul>
                <li>그래프 표현 방법 (인접 행렬, 인접 리스트).</li>
                <li>그래프 탐색 (DFS, BFS)과 최단 경로 알고리즘 이해.</li>
            </ul>
        </li>
    </ol>

<hr>
<h3 id="4-알고리즘-분석">4. 알고리즘 분석</h3>
<p>알고리즘 분석은 주어진 문제를 해결하기 위해 작성한 코드의 성능을 평가하는 과정입니다. 이를 통해 알고리즘의 효율성을 이해하고, 최적화할 수 있는 기회를 제공합니다. 앞으로의 알고리즘 분석은 다음과 같은 방법으로 진행할 예정입니다:</p>
<ol>
    <li>
        <strong>시간 복잡도 계산</strong>
        <ul>
            <li>코드의 실행 시간이 입력 크기에 따라 어떻게 변화하는지를 분석합니다.</li>
            <li>반복문이나 재귀 호출의 횟수를 세고, Big O 표기법으로 표현합니다.</li>
            <li>예를 들어, 중첩 반복문이 있는 경우 O(n<sup>2</sup>)로 표현할 수 있습니다.</li>
        </ul>
    </li>
    <li>
        <strong>공간 복잡도 계산</strong>
        <ul>
            <li>코드가 사용하는 메모리의 양을 분석합니다.</li>
            <li>변수, 배열, 객체 등에서 사용하는 메모리의 양을 고려합니다.</li>
            <li>재귀 호출로 인해 사용하는 스택 메모리를 포함하여 평가합니다.</li>
        </ul>
    </li>
    <li>
        <strong>다른 사람의 코드 분석</strong>
        <ul>
            <li>다른 사람의 알고리즘을 분석하여 다양한 접근 방식을 배웁니다.</li>
            <li>코드의 시간 복잡도와 공간 복잡도를 이해합니다.</li>
            <li>자신의 코드와 비교하여 개선할 수 있는 부분을 찾아봅니다.</li>
        </ul>
    </li>
</ol>

<p>이러한 단계들을 통해 알고리즘 분석의 기초를 다지고, 문제 해결 능력을 향상시키는 데 중점을 둘 것입니다. 알고리즘의 성능을 체계적으로 평가함으로써, 더 효율적인 코드 작성과 문제 해결 전략을 개발할 수 있도록 노력하겠습니다.</p>
<hr>
<p>happy new year :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[코딩 테스트 필수 문법 (2)]]></title>
            <link>https://velog.io/@00kang_jh/%EC%BD%94%EB%94%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%84%EC%88%98-%EB%AC%B8%EB%B2%95-2</link>
            <guid>https://velog.io/@00kang_jh/%EC%BD%94%EB%94%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%84%EC%88%98-%EB%AC%B8%EB%B2%95-2</guid>
            <pubDate>Thu, 14 Nov 2024 01:54:05 GMT</pubDate>
            <description><![CDATA[<h3 id="컬렉션-프레임-워크collection-framework">컬렉션 프레임 워크(Collection Framework)</h3>
<hr>
<p>여러 개의 값을 저장하고 그 값을 쉽고, 효율적으로 처리해주는 표준화 클래스의 집합 입니다. 쉽게 말하자면 Data structure인 리스트, 큐, 스택, 해시맵 등을 직접 구현하지 않고 쉽게 사용할 수 있습니다. 대표적으로 <code>ArrayList</code> , <code>Stack</code>, <code>Queue</code>, <code>ArrayDeque</code>, <code>HashMap</code> 등이 있습니다.</p>
<p><strong>컬렉션 프레임워크의 주요 클래스</strong></p>
<ul>
<li><strong>ArrayList</strong>
ArrayList는 가변 크기를 가지며, 요소를 인덱스에 따라 저장하는 리스트입니다.<pre><code class="language-java">import java.util.ArrayList;
</code></pre>
</li>
</ul>
<p>public class ArrayListExample {
    public static void main(String[] args) {
        // Create
        ArrayList<String> list = new ArrayList&lt;&gt;();
        list.add(&quot;Apple&quot;);
        list.add(&quot;Banana&quot;);</p>
<pre><code>    // Read
    System.out.println(list.get(0)); // Apple

    // Update
    list.set(1, &quot;Cherry&quot;);
    System.out.println(list.get(1)); // Cherry

    // Delete
    list.remove(0);
    System.out.println(list); // [Cherry]
}</code></pre><p>}</p>
<pre><code>- **Stack**
Stack은 `LIFO(Last In First Out)` 구조로, 요소를 쌓아 올리는 형식입니다.

```java
import java.util.Stack;

public class StackExample {
    public static void main(String[] args) {
        // Create
        Stack&lt;String&gt; stack = new Stack&lt;&gt;();
        stack.push(&quot;A&quot;);
        stack.push(&quot;B&quot;);

        // Read
        System.out.println(stack.peek()); // B

        // Update (Stack에서는 update가 아닌 pop 후 push로 처리)
        stack.pop(); // 제거
        stack.push(&quot;C&quot;); // 새 요소 추가

        // Delete
        stack.pop(); // C 제거
        System.out.println(stack); // [A]
    }
}</code></pre><ul>
<li><strong>Queue</strong>
Queue는 <code>FIFO(First In First Out)</code> 구조로, 먼저 들어온 요소가 먼저 나갑니다.</li>
</ul>
<pre><code class="language-java">import java.util.LinkedList;
import java.util.Queue;

public class QueueExample {
    public static void main(String[] args) {
        // Create
        Queue&lt;String&gt; queue = new LinkedList&lt;&gt;();
        queue.add(&quot;1&quot;);
        queue.add(&quot;2&quot;);

        // Read
        System.out.println(queue.peek()); // 1

        // Update (Queue는 직접적으로 업데이트 불가, 제거 후 추가)
        queue.poll(); // 1 제거
        queue.add(&quot;3&quot;); // 새 요소 추가

        // Delete
        queue.poll(); // 2 제거
        System.out.println(queue); // [3]
    }
}</code></pre>
<ul>
<li><strong>ArrayDeque</strong>
ArrayDeque는 덱(Double Ended Queue)으로, 양쪽 끝에서 요소를 추가하거나 제거할 수 있습니다.</li>
</ul>
<pre><code class="language-java">import java.util.ArrayDeque;

public class ArrayDequeExample {
    public static void main(String[] args) {
        // Create
        ArrayDeque&lt;String&gt; deque = new ArrayDeque&lt;&gt;();
        deque.add(&quot;X&quot;);
        deque.add(&quot;Y&quot;);

        // Read
        System.out.println(deque.peekFirst()); // X

        // Update (Update가 아닌 제거 후 추가)
        deque.pollFirst(); // X 제거
        deque.addFirst(&quot;Z&quot;); // 새 요소 추가

        // Delete
        deque.pollLast(); // Y 제거
        System.out.println(deque); // [Z]
    }
}</code></pre>
<ul>
<li><strong>HashMap</strong>
HashMap은 키-값 쌍으로 데이터를 저장하며, 키를 통해 값을 빠르게 조회할 수 있습니다.</li>
</ul>
<pre><code class="language-java">import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        // Create
        HashMap&lt;String, Integer&gt; map = new HashMap&lt;&gt;();
        map.put(&quot;Apple&quot;, 1);
        map.put(&quot;Banana&quot;, 2);

        // Read
        System.out.println(map.get(&quot;Apple&quot;)); // 1

        // Update
        map.put(&quot;Apple&quot;, 3); // 값 업데이트
        System.out.println(map.get(&quot;Apple&quot;)); // 3

        // Delete
        map.remove(&quot;Banana&quot;);
        System.out.println(map); // {Apple=3}
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[코딩 테스트 필수 문법 (1)]]></title>
            <link>https://velog.io/@00kang_jh/%EC%BD%94%EB%94%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%84%EC%88%98-%EB%AC%B8%EB%B2%951</link>
            <guid>https://velog.io/@00kang_jh/%EC%BD%94%EB%94%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%84%EC%88%98-%EB%AC%B8%EB%B2%951</guid>
            <pubDate>Thu, 14 Nov 2024 01:31:24 GMT</pubDate>
            <description><![CDATA[<h3 id="프리미티브-타입과-레퍼런스-타입">프리미티브 타입과 레퍼런스 타입</h3>
<hr>
<p><strong>프리미티브 타입(Primitive type)</strong>
원시 타입 은 정수, 실수, 문자, 논리 리터럴 등의 실제 데이터 값을 저장하는 타입입니다.</p>
<p><code>int</code>, <code>long</code>, <code>double</code>, <code>float</code>, <code>boolean</code>, <code>byte</code>, <code>short</code>, <code>char</code></p>
<p><strong>레퍼런스 타입(Reference Type)</strong>
참조 타입 은 객체(Object)를 참조(주소를 저장) 하는 타입으로 메모리 번지 값을 통해 객체를 참조하는 타입이다.</p>
<p><code>Integer</code>, <code>Long</code>, <code>Double</code>, <code>Float</code>, <code>Boolean</code>, <code>Byte</code>, <code>Short</code>, <code>Char</code></p>
<blockquote>
<p>연산 속도는 Primitive type가 Reference Type보다 빠릅니다.</p>
</blockquote>
<ul>
<li><strong>비교 연산</strong><pre><code class="language-java">System.out.println(a==b); // 같은 값인지 비교, false
System.out.println(a!=b); // 같지 않은 값인지 비교, false
System.out.println(a&gt;b);  // 왼쪽 같이 더 큰지 비교, false
System.out.println(a&lt;b);  // 왼쪽 값이 더 작은지 비교, false
System.out.println(a&gt;=b); // 왼쪽 값이 더 크거나 같은지 비교, false
System.out.println(a&lt;=b); // 왼쪽 값이 더 작거나 같은지 비교, false</code></pre>
</li>
<li><strong>비트 연산</strong><pre><code class="language-java">int a = 13;
int b = 4;
System.out.println(a &amp; b); // AND, 4
System.out.println(a | b); // OR, 13
System.out.println(a ^ b); // XOR, 9
System.out.println(~ a); // NOT, -14
System.out.println(a &lt;&lt; 2); // 왼쪽 시프트 (a에 2^2를 곱한 것과 동일) / 52
System.out.println(a &gt;&gt; 1); // 오른쪽 시프트 (a에 2^1로 나눈 것과 동일) / 6</code></pre>
</li>
</ul>
<p>부동소수형 데이터는 표현 과정에서 오차가 발생할 수 있다. 이것을 <code>엡실론(epsilon)</code>이라고 하며 항상 생각해야합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OpenCV와 C++ 설치하기: Apple Silicon M2에서 VS Code 사용하기 🖥️]]></title>
            <link>https://velog.io/@00kang_jh/Computer-Vision-0</link>
            <guid>https://velog.io/@00kang_jh/Computer-Vision-0</guid>
            <pubDate>Mon, 14 Oct 2024 02:23:03 GMT</pubDate>
            <description><![CDATA[<p>OpenCV는 컴퓨터 비전 분야에서 널리 사용되는 라이브러리로, 이미지와 비디오 처리에 강력한 기능을 제공합니다. C++로 OpenCV를 사용하면 성능을 극대화할 수 있습니다. 이번 포스팅에서는 Apple Silicon M2에서 VS Code를 사용하여 OpenCV와 C++ 환경을 설정하는 방법을 자세히 알아보겠습니다.</p>
<h3 id="1개발환경-안내">1.개발환경 안내</h3>
<p>본인은 MacBook Air, Apple M2, macOS Sonoma 14.6.2 입니다.</p>
<h3 id="2-opencv와-c-소개">2. OpenCV와 C++ 소개</h3>
<p>OpenCV는 다양한 이미지 처리 기능을 제공하는 오픈 소스 라이브러리입니다. C++는 OpenCV와 함께 사용할 때 성능이 뛰어나기 때문에 많은 개발자들이 선호합니다. OpenCV를 통해 이미지 필터링, 객체 인식, 얼굴 인식 등 다양한 작업을 수행할 수 있습니다.</p>
<h3 id="3-apple-silicon-m2에서의-환경-설정">3. Apple Silicon M2에서의 환경 설정</h3>
<p>Apple Silicon M2는 ARM 아키텍처를 기반으로 하여, 기존의 Intel 기반 Mac과는 다른 환경을 제공합니다. 따라서, M2에서 OpenCV와 C++를 설치하기 위해서는 몇 가지 추가적인 설정이 필요합니다.</p>
<h3 id="4-vs-code-설치-및-설정">4. VS Code 설치 및 설정</h3>
<p>먼저, Visual Studio Code(VS Code)를 설치해야 합니다. VS Code는 다양한 언어를 지원하는 강력한 코드 편집기입니다. Apple Silicon에 맞는 버전을 다운로드하려면 VS Code 공식 웹사이트로 가서 Apple Silicon 버전을 선택하여 설치합니다.</p>
<p>설치가 완료되면, VS Code를 실행하고 C++ 확장 프로그램을 설치합니다. 이를 통해 C++ 개발 환경을 더욱 편리하게 사용할 수 있습니다.</p>
<h3 id="5-homebrew-설치-및-opencv-설치">5. Homebrew 설치 및 OpenCV 설치</h3>
<p>Homebrew는 macOS에서 패키지를 관리하는 도구입니다. Homebrew를 설치하려면 터미널을 열고 아래의 명령어를 입력합니다:</p>
<blockquote>
<p>bash
/bin/bash -c &quot;$(curl -fsSL <a href="https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;">https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;</a></p>
</blockquote>
<p>설치가 완료되면, OpenCV를 설치할 수 있습니다. 터미널에 다음 명령어를 입력하여 OpenCV를 설치합니다:</p>
<blockquote>
<p>bash
brew install opencv</p>
</blockquote>
<p>설치가 완료되면, OpenCV의 경로를 확인하고 환경 변수를 설정해야 합니다. 아래의 명령어를 입력하여 OpenCV의 설치 경로를 확인합니다:</p>
<blockquote>
<p>bash
brew info opencv</p>
</blockquote>
<p>이제 VS Code에서 OpenCV를 사용할 수 있도록 설정합니다. VS Code의 설정 파일에 OpenCV의 경로를 추가합니다.</p>
<h3 id="6-opencv와-c-프로젝트-설정">6. OpenCV와 C++ 프로젝트 설정</h3>
<p>이제 OpenCV와 C++ 프로젝트를 설정할 차례입니다. VS Code에서 새로운 C++ 프로젝트를 생성하고, CMakeLists.txt 파일을 생성하여 OpenCV 라이브러리를 포함시킵니다. 아래는 기본적인 CMakeLists.txt의 예시입니다:</p>
<blockquote>
<p>cmake
cmake_minimum_required(VERSION 3.10)
project(<span style="color:blue">프로젝트명</span>) <br> 
find_package(OpenCV REQUIRED)
add_executable(<span style="color:blue">프로젝트명</span>   <span style="color:green">파일명</span>)
target_link_libraries (<span style="color:blue">프로젝트명</span> ${OpenCV_LIBS}) </p>
</blockquote>
<p>이 코드는 지정한 이미지를 불러와서 화면에 표시하는 간단한 예제입니다. 이미지를 불러올 수 없는 경우 오류 메시지를 출력합니다.</p>
<h3 id="7-프로젝트-실행">7. 프로젝트 실행</h3>
<p>CMakeLists.txt를 완성한 후, 우리가 원하는 프로젝트를 실행할 차례입니다. VScode 터미널에서 실행하게 됩니다. 다음 명령어들을 입력하면 됩니다.</p>
<blockquote>
<p>cd build
make
./<span style="color:blue">프로젝트명</span></p>
</blockquote>
<p>다음과 같이 입력하시게 되면 CMakeLists.txt에서 설정한 파일이 실행됨을 알 수 있습니다. 실행한 프로젝트를 종료하시려면 <code>control + z</code> 누르시면 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 멀쩡한 사각형 Java]]></title>
            <link>https://velog.io/@00kang_jh/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%A9%80%EC%A9%A1%ED%95%9C-%EC%82%AC%EA%B0%81%ED%98%95-Java</link>
            <guid>https://velog.io/@00kang_jh/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%A9%80%EC%A9%A1%ED%95%9C-%EC%82%AC%EA%B0%81%ED%98%95-Java</guid>
            <pubDate>Tue, 08 Oct 2024 07:05:47 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이번 문제는 프로그래머스 Level 2 [멀쩡한 사각형] (<a href="https://school.programmers.co.kr/learn/courses/30/lessons/62048">https://school.programmers.co.kr/learn/courses/30/lessons/62048</a>) 입니다.</p>
</blockquote>
<hr>
<h3 id="문제-설명">문제 설명</h3>
<p>가로 길이가 <strong>W</strong>cm, 세로 길이가 <strong>H</strong>cm인 직사각형 종이가 있습니다. 종이에는 가로, 세로 방향과 평행하게 격자 형태로 선이 그어져 있으며, 모든 격자칸은 1cm x 1cm 크기입니다. 이 종이를 격자 선을 따라 1cm × 1cm의 정사각형으로 잘라 사용할 예정이었는데, 누군가가 <strong>이 종이를 대각선 꼭지점 2개를 잇는 방향으로 잘라 놓았습니다.</strong> 그러므로 현재 직사각형 종이는 크기가 같은 직각삼각형 2개로 나누어진 상태입니다. 새로운 종이를 구할 수 없는 상태이기 때문에, 이 종이에서 원래 종이의 가로, 세로 방향과 평행하게 1cm × 1cm로 잘라 사용할 수 있는 만큼만 사용하기로 하였습니다.</p>
<p>가로의 길이 W와 세로의 길이 H가 주어질 때, 사용할 수 있는 정사각형의 개수를 구하는 solution 함수를 완성해 주세요.</p>
<blockquote>
<p><strong>한줄로 요약하면!!</strong>
가로/세로 값이 해당 사이즈의 직사각형을 반절로 접었을 때, 그 대각선을 지나는 칸을 제외한 나머지 칸을 구하면 됩니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/a9d1689b-8cdb-4c61-b7e0-02bdc85818bf/image.png" alt=""></p>
<hr>
<h3 id="제한사항">제한사항</h3>
<p>W, H : 1억 이하의 자연수</p>
<hr>
<h3 id="입출력-예시">입출력 예시</h3>
<p>w = 8 , h = 12 일때, 전체 칸은 96칸 입니다.
그 직사각형의 대각선을 그립니다. 해당 대각선이 지나는 칸이 총 16칸이기 때문에 그 만큼을 제외한 80칸이 return 됩니다. </p>
<hr>
<h3 id="코드-작성-전-생각-정리">코드 작성 전, 생각 정리</h3>
<ul>
<li>파라미터가 int 타입인데, return 타입이 long이라 계산할 때 타입 변환하기</li>
<li>다른 값들 통해서 규칙성을 찾기<ul>
<li>위의 사진을 보면, 총 4번의 동일한 패턴을 보인다. </li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>여기서 4라는 숫자가 어떻게 나올 수 있을까?</strong>
예제에서 8과 12일 때, 4라는 숫자는 여러가지로 생각해볼 수 있지만 최대공약수로 추측할 수 있다. 그 외의 경우에도 고려해보면 공약수가 없는 경우를 제외하면 모든 경우에 최대공약수로 결과값을 구하면 원하는 값과 동일하다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/eb912aaf-f1a1-4bc7-823a-27dfd5215f07/image.png" alt=""></p>
<ol>
<li><p>(2,3)일 때, 4개의 직사각형이 영향을 받음</p>
</li>
<li><p>(5,2)일 때, 6개의 직사각형이 영향을 받음</p>
<p>일반식으로 만들어보면!!
영향을 받는 직사각형의 수 = (너비) + (높이) - 1 이 되고,
결론적으로 전체 계산식을 작성하면
<strong>(너비) x (높이) - ((너비) + (높이) - 1) x (최대공약수)</strong>이 됩니다.</p>
</li>
</ol>
<hr>
<h3 id="코드자바">코드(자바)</h3>
<pre><code class="language-java">class Solution {
    public long solution(int w, int h) {
        long answer = 1;
        long big = gcd(w,h);
        long weight = (long)w;
        long height = (long)h;

        answer = weight*height - ((weight/big)+(height/big)-1)*big;

        return answer;
    }

    public static long gcd(int a, int b) {
        while (b != 0) {
            int temp = b;
            b = a % b;
            a = temp;
        }
        return (long)a;
    }
}
</code></pre>
<hr>
<h3 id="개인-소감">개인 소감</h3>
<p>규칙성을 찾는데 시간이 걸렸다. 매번 코딩 테스트 문제만 풀이하면서는 &#39;어떤 라이브러리의 어떤 메소드를 사용하면 좋을까?&#39; 이런 고민들을 많이 했었다. 너무나도 많은 라이브러리에 메소드를 기억하기 어려워서 찾아서 공부하고, 찾아서 코드에 작성하는 형식이였다. 이번 문제는 다른 문제들과 다르게 오랜만에 라이브러리와 메소드를 고민하는 것보단 고등학교 시절로 돌아가 수학 문제를 풀던 때로 돌아간 기분이였다. 뭐든 안하면 잊어버리고 머리가 굳기 마련이다. 굳지 않고 말랑말랑한 상태 유지를 위해서 열심히 노력하자!!</p>
<hr>
<p><img src="https://velog.velcdn.com/images/00kang_jh/post/3192bb25-ca03-4bb8-8855-390e3730e93e/image.png"  width="40%" height="40%"><p></p>
]]></description>
        </item>
    </channel>
</rss>