<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>good-jinu.log</title>
        <link>https://velog.io/</link>
        <description>안녕하세요</description>
        <lastBuildDate>Sat, 21 Mar 2026 06:19:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>good-jinu.log</title>
            <url>https://velog.velcdn.com/images/good-jinu/profile/a9b7c478-d4b0-4053-877e-9a7adb17fc0c/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. good-jinu.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/good-jinu" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[첫 직장에서 맛본 'iframe Monolithic'이라는 재앙]]></title>
            <link>https://velog.io/@good-jinu/%EC%B2%AB-%EC%A7%81%EC%9E%A5%EC%97%90%EC%84%9C-%EB%A7%9B%EB%B3%B8-iframe-SPA%EB%9D%BC%EB%8A%94-%EC%9E%AC%EC%95%99</link>
            <guid>https://velog.io/@good-jinu/%EC%B2%AB-%EC%A7%81%EC%9E%A5%EC%97%90%EC%84%9C-%EB%A7%9B%EB%B3%B8-iframe-SPA%EB%9D%BC%EB%8A%94-%EC%9E%AC%EC%95%99</guid>
            <pubDate>Sat, 21 Mar 2026 06:19:00 GMT</pubDate>
            <description><![CDATA[<p>처음으로 제대로 된 프론트엔드 직장을 다녔던 때를 떠올리면 아직도 마음이 쓰립니다.</p>
<p>완전히 코딩 초보는 아니었어요. 학교에서 풀스택 프로젝트도 꽤 해봤죠.
하지만 실무 경험은 전혀 없었습니다. 실제 트래픽도 없었고, 사용자 이탈을 걱정하며 QA가 압박하는 상황도 겪어본 적이 없었죠.</p>
<p>회사에 들어가 보니, 이커머스 홈페이지는 체감상 정말 느렸습니다.
QA는 매주 같은 메시지를 남겼어요.</p>
<blockquote>
<p>&quot;홈페이지 또 느려요 😭&quot;
&quot;히어로 영역 + 상품 그리드 로딩 너무 오래 걸려요&quot;
&quot;3초 넘게 걸려서 유저들이 떠나요&quot;</p>
</blockquote>
<p>리포트를 열어보면 끔찍한 워터폴이 보였고, 저는 그저 멍해질 수밖에 없었습니다.</p>
<p>스택만 보면 최신이었어요: S3 + CloudFront + SPA.
“모듈화”를 위해 추천 영역이나 배너 같은 부분은 iframe으로 구성되어 있었습니다.
이론상으로는 플러그 앤 플레이 구조였죠. 하지만 실제로는 무겁고 깨지기 쉬운 시스템이었습니다.</p>
<p>뭔가 잘못됐다는 건 느꼈지만, 그걸 설명할 언어가 없었습니다.
<strong>각 iframe이 사실상 거대한 중복 의존성 그래프를 드러내고 있다는 걸 그땐 몰랐죠.</strong></p>
<hr>
<h2 id="모놀리식-구조의-달콤한-단순함">모놀리식 구조의 달콤한 단순함</h2>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/d9d9e71b-cd9c-4f4f-8630-29d508db9ecf/image.png" alt=""></p>
<p>하나의 SPA는 처음 만들 때는 정말 깔끔해 보입니다.</p>
<p>하나의 앱, 하나의 배포.
상품 리스트, 상세 페이지, 이벤트, 프로모션… 전부 한 곳에 넣으면 되죠.
다른 곳에서 재사용하고 싶으면 iframe으로 감싸서 넣으면 끝.</p>
<p>빠르고 실용적인 선택처럼 보입니다.</p>
<p>하지만 그게 함정입니다.</p>
<p>iframe은 단순히 UI를 임베드하는 게 아닙니다.
<strong>런타임 전체를 임베드하는 것입니다.</strong></p>
<p>우리는 단순히 &quot;배너&quot;를 넣은 게 아니라,
거대한 React 애플리케이션을 작은 창 안에 또 하나 띄우고 있었던 겁니다.</p>
<p>각 “모듈”은 사실상 새로운 앱 인스턴스를 브라우저에서 다시 부팅하게 만들고 있었어요.</p>
<p>이 시스템은 기술적으로는 최신이었지만, 구조적으로는 과부하 상태였습니다.
<strong>iframe이 문제를 만든 게 아니라, 문제를 숨길 수 없게 만든 것이었습니다.</strong></p>
<hr>
<h2 id="1-무게-문제-작은-ui에-너무-많은-코드">1. 무게 문제: 작은 UI에 너무 많은 코드</h2>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/d86bc585-9a1c-4b67-9cdc-c550b93cccee/image.png" alt=""></p>
<p>이 문제는 보통 여기서 가장 먼저 드러납니다.</p>
<p>유저는 단순한 프로모션 페이지를 열었을 뿐인데,
브라우저는 전체 쇼핑몰 애플리케이션을 다운로드합니다.</p>
<p>상품 목록 로직, 상세 페이지, 이벤트 처리, 라우팅, 공통 컴포넌트…
시간이 지나며 쌓인 모든 것들이 포함됩니다.</p>
<p>즉, 키오스크 하나 보려고 쇼핑몰 전체를 로딩하는 셈입니다.</p>
<p>모놀리식 SPA는 기능이 늘어날수록 번들이 커지기 때문에 이런 문제가 생깁니다.
결과적으로:</p>
<ul>
<li>JavaScript 용량 증가</li>
<li>파싱 및 실행 시간 증가</li>
<li>첫 인터랙션 지연</li>
<li>저사양 기기에서 메모리 압박</li>
</ul>
<p>iframe은 이 불일치를 <strong>눈에 보이게 만들었습니다.</strong></p>
<p>작은 화면 하나를 위해 전체 앱을 초기화해야 했고,
그 비용을 페이지 내에서 여러 번 반복해서 지불하게 만든 거죠.</p>
<p><strong>핵심 문제:</strong>
UI는 작아 보이지만, 실제 런타임 비용은 거대하다.
그리고 iframe은 이 거짓말을 드러냈다.</p>
<hr>
<h2 id="2-보이지-않는-콘텐츠-문제-크롤러가-못-보는-구조">2. 보이지 않는 콘텐츠 문제: 크롤러가 못 보는 구조</h2>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/c2d2134e-27b7-4fb5-b7f4-dac73a196c46/image.png" alt=""></p>
<p>두 번째 문제는 더 미묘해서 더 오래 방치되기 쉽습니다.</p>
<p>문제의 시작은 클라이언트 렌더링 SPA였습니다.
검색 엔진이 JavaScript를 실행할 수는 있지만,
핵심 콘텐츠를 여기에 의존하는 건 위험한 선택입니다.</p>
<p>iframe은 여기에 한 단계 더 복잡성을 추가했습니다.</p>
<p>SPA 자체도 인덱싱이 불안정한데,
iframe으로 콘텐츠를 분산시키면서 더 어려워진 겁니다.</p>
<p>중요한 포인트는 이겁니다:</p>
<ul>
<li>SPA는 기본적으로 콘텐츠를 “숨기고”</li>
<li>iframe은 그걸 “더 고치기 어렵게 만든다”</li>
</ul>
<p>iframe을 제거해도 문제가 해결되진 않습니다.
<strong>핵심은 서버에서 콘텐츠를 렌더링하는 것(SSR)</strong> 입니다.</p>
<hr>
<h2 id="내가-잘못-이해했던-부분">내가 잘못 이해했던 부분</h2>
<p>한동안 저는 이걸 단순히 “성능 문제”라고 생각했습니다.</p>
<p>그래서 네트워크 지연, 이미지 용량, CDN 캐시 같은 것들을 살폈죠.</p>
<p>물론 중요했습니다.
하지만 본질은 아니었습니다.</p>
<p>진짜 문제는 <strong>아키텍처의 형태</strong>였습니다.</p>
<p>앱은 너무 많은 책임을 가지고 있었고,
너무 많은 코드가 공유되고 있었으며,
하나의 배포 단위에 너무 많은 것이 들어 있었습니다.</p>
<p>iframe은 문제를 만든 게 아니라,
그걸 <strong>여러 번 실행하게 만들어서 드러낸 것뿐</strong>이었습니다.</p>
<p>그때 깨달았습니다.
문제를 잘못 보고 있었다는 것을.</p>
<p>그래서 매니저에게 요청했습니다.
단순한 최적화가 아니라, 구조적인 개선을 하자고요.</p>
<ul>
<li>번들 줄이기</li>
<li>사용하지 않는 코드 제거</li>
<li>페이지 분리</li>
<li>빌드 파이프라인 개선</li>
<li>모든 페이지가 모든 기능을 들고 다니지 않도록 만들기</li>
</ul>
<p>이 대화는 코드만큼 중요했습니다.</p>
<hr>
<h2 id="실제로-도움이-된-것들">실제로 도움이 된 것들</h2>
<p>우리는 완전히 새로 만들지 않았습니다.
마이크로 프론트엔드로 전환하지도 않았습니다.</p>
<p>대신 시스템을 <strong>작고, 깔끔하게 만들었습니다.</strong></p>
<h3 id="1-tree-shaking--dead-code-제거">1. Tree shaking &amp; dead code 제거</h3>
<p>사용되지 않는 코드들을 제거했습니다.</p>
<p>이건 사소해 보이지만,
죽은 코드도 여전히 다운로드되고, 파싱되고, 실행됩니다.</p>
<h3 id="2-swc-도입">2. SWC 도입</h3>
<p>빌드 속도를 개선했습니다.</p>
<p>이건 단순히 시간을 절약한 게 아니라,
개발 방식 자체를 바꿨습니다.</p>
<p>빌드가 빠르면:</p>
<ul>
<li>더 자주 리팩토링하고</li>
<li>코드 분리를 시도하고</li>
<li>구조 개선을 두려워하지 않게 됩니다</li>
</ul>
<h3 id="3-페이지-및-코드-분리">3. 페이지 및 코드 분리</h3>
<p>필요한 것만 로딩하도록 했습니다.</p>
<blockquote>
<p>&quot;지금 필요 없는 기능은 다운로드하지 않는다&quot;</p>
</blockquote>
<p>이 단순한 원칙이 큰 변화를 만들었습니다.</p>
<hr>
<h2 id="결과">결과</h2>
<p>결과는 명확했습니다.</p>
<ul>
<li>배포 시간: 10분 → 2분</li>
<li>로딩 시간: 3초 이상 → 1초 이하</li>
</ul>
<p>이건 단순한 숫자가 아닙니다.</p>
<p>팀의 행동 자체를 바꿉니다.</p>
<ul>
<li>빠른 배포 → 빠른 피드백</li>
<li>빠른 피드백 → 더 높은 자신감</li>
<li>높은 자신감 → 더 좋은 아키텍처 결정</li>
</ul>
<hr>
<h2 id="핵심-교훈">핵심 교훈</h2>
<p>브라우저가 로딩해야 하는 것을 줄여라.</p>
<p>iframe이 문제인 게 아닙니다.
SPA가 문제인 것도 아닙니다.</p>
<p>문제는 이겁니다:</p>
<blockquote>
<p><strong>거대한 모놀리스를 여러 번 로딩하는 구조</strong></p>
</blockquote>
<p>좋은 구조는 보통 다음을 포함합니다:</p>
<ul>
<li>경험 단위로 페이지를 분리</li>
<li>SSR 또는 프리렌더링 사용</li>
<li>라우트/기능 단위로 코드 분할</li>
<li>iframe은 정말 필요한 경우에만 사용</li>
<li>불필요한 런타임 공유 최소화</li>
</ul>
<hr>
<h2 id="결론">결론</h2>
<p>이 프로젝트에서 가장 어려웠던 건 코드가 아니라 인정이었습니다.</p>
<p>아키텍처가 문제라는 걸 인정하는 것.</p>
<p>문제를 버그 단위로 보지 않고 패턴으로 보기 시작했을 때,
비로소 해결의 방향이 보였습니다.</p>
<ul>
<li>너무 많은 코드</li>
<li>너무 강한 결합</li>
<li>그리고 iframe으로 증폭된 구조적 문제</li>
</ul>
<hr>
<p>이글은 원문을 번역했습니다. 원문도 제가 작성했습니다: <a href="https://dev.to/deadlocker/i-joined-my-first-job-and-the-homepage-took-forever-to-load-e9o">https://dev.to/deadlocker/i-joined-my-first-job-and-the-homepage-took-forever-to-load-e9o</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[skills-npm을 이용한 AI 컨텍스트 동기화 자동화]]></title>
            <link>https://velog.io/@good-jinu/skills-npm%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-AI-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EB%8F%99%EA%B8%B0%ED%99%94-%EC%9E%90%EB%8F%99%ED%99%94</link>
            <guid>https://velog.io/@good-jinu/skills-npm%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-AI-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EB%8F%99%EA%B8%B0%ED%99%94-%EC%9E%90%EB%8F%99%ED%99%94</guid>
            <pubDate>Thu, 19 Mar 2026 14:03:52 GMT</pubDate>
            <description><![CDATA[<p>저는 현재 백엔드 레포, 프론트엔드 레포, AWS 테라폼 레포를 Claude Code로 관리하고있습니다. 서비스가 확장되면서 레포 간 상호 의존성이 생겼고, 일관성을 유지하기 위해 내부 SDK 도입을 검토하게 됐습니다. 그러면서 자연스럽게 문제가 따라왔습니다. 에이전트가 각 레포를 열 때마다 SDK 사용법과 레포 간 연동 규약을 모릅니다. <code>CLAUDE.md</code>에 적어두면 되긴 한데, SDK가 업데이트되면 그 내용은 자동으로 바뀌지 않습니다. 누군가 수동으로 챙겨야 하죠.</p>
<p><code>skills-npm</code>은 이 문제를 꽤 우아하게 해결합니다. 핵심은 단순합니다. AI 지침을 npm 패키지 안에 같이 실어서 배포하자는 것입니다.</p>
<hr>
<h2 id="문제-업데이트되지-않는-claudemd">문제: 업데이트되지 않는 CLAUDE.md</h2>
<p>에이전트에게 컨텍스트를 주는 방법은 크게 두 가지입니다. 직접 프롬프트에 넣거나, <code>CLAUDE.md</code> 같은 파일에 적어두는 것이죠. 두 번째 방식이 현실적으로 더 낫습니다. 그런데 문제가 있습니다.</p>
<ul>
<li><strong>버전 미스매치</strong>: <code>CLAUDE.md</code>에 &quot;이 SDK는 이렇게 써라&quot;라고 적어뒀는데, SDK가 업데이트되면 이 파일은 자동으로 바뀌지 않습니다. 누군가 수동으로 바꿔야 합니다. 근데 아무도 안 합니다. 저도 안 했습니다.</li>
<li><strong>레포 간 단절</strong>: 프론트엔드 레포를 열어놓은 에이전트는 백엔드 레포의 비즈니스 로직이나 테라폼으로 관리되는 인프라 구조를 모릅니다. 레포가 늘어날수록 개발자가 직접 설명해야 할 게 쌓입니다.</li>
</ul>
<p>본질적으로, AI에게 전달하는 컨텍스트가 코드와 분리된 채 관리되고 있다는 구조적 문제입니다.</p>
<hr>
<h2 id="기존-방식-git-기반-스킬-배포의-한계">기존 방식: Git 기반 스킬 배포의 한계</h2>
<p>현재 가장 많이 쓰이는 건 <a href="https://github.com/vercel-labs/skills"><code>vercel-labs/skills</code></a> 같은 Git 기반 스킬 CLI입니다. <code>npx skills add</code>로 Git 레포에서 스킬을 클론해서 프로젝트에 연결하는 방식이죠. 쓸 만하긴 한데 한계가 명확합니다.</p>
<ul>
<li><strong>툴이랑 따로 놀음</strong>: 스킬 버전이랑 실제 툴 버전이 별도로 관리됩니다. SDK가 1.0에서 2.0으로 올라갔는데 스킬은 여전히 1.0 기준이어도 아무도 모릅니다.</li>
<li><strong>팀 공유가 번거로움</strong>: 클론된 파일을 레포에 커밋하든지, 새 머신마다 다시 설치하든지 둘 중 하나입니다. 둘 다 별로입니다.</li>
</ul>
<hr>
<h2 id="skills-npm-npm-패키지에-ai-지침을-번들링">skills-npm: npm 패키지에 AI 지침을 번들링</h2>
<p><a href="https://github.com/antfu/skills-npm"><code>skills-npm</code></a>이 제안하는 컨벤션은 이겁니다. SDK 개발자가 <code>npm publish</code>할 때 AI 지침 파일도 같이 실어서 보낸다. 소비자는 <code>npm install</code>하면 자동으로 그 지침이 에이전트가 읽을 수 있는 위치로 심볼릭 링크됩니다.</p>
<p>SDK 버전이 2.0으로 올라가면 <code>SKILL.md</code>도 2.0에 맞게 업데이트됩니다. <code>npm install</code>만 하면 에이전트가 읽는 지침도 즉시 최신화되는 겁니다. 따로 관리할 게 없습니다.</p>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/d01238ef-03d0-434f-8f35-3ae5ea05aa8b/image.png" alt=""></p>
<hr>
<h2 id="skills-npm-생태계의-3-주체">skills-npm 생태계의 3 주체</h2>
<p><strong>공급자 (SDK 개발자)</strong></p>
<ul>
<li>SDK 프로젝트 내 <code>skills/&lt;스킬명&gt;/SKILL.md</code> 작성</li>
<li><code>package.json</code>의 <code>files</code> 필드에 <code>&quot;skills&quot;</code> 포함</li>
<li><a href="https://github.com/antfu/skills-npm/blob/main/PROPOSAL.md">skills-npm 에서 제안하는 방법</a>을 참고</li>
</ul>
<p><strong>소비자 (앱 개발자)</strong></p>
<ul>
<li><code>npm i -D skills-npm</code> 설치</li>
<li><code>prepare</code> 스크립트에 <code>skills-npm</code> 등록</li>
<li><code>npm install</code> 시 <code>skills/npm-&lt;패키지명&gt;-&lt;스킬명&gt;</code> 경로로 자동 심볼릭 링크 생성</li>
</ul>
<p><strong>AI 에이전트 (Cursor, Claude Code, Codex 등)</strong></p>
<ul>
<li>심볼릭 링크된 <code>skills/npm-*</code> 경로 스캔</li>
<li><code>SKILL.md</code>를 읽어 SDK 사용법과 시스템 간 연동 로직 파악</li>
</ul>
<p>심볼릭 링크(symlink)라는 게 낯설 수 있는데, 실제 파일을 복사하는 게 아니라 <code>node_modules</code> 안에 있는 원본을 가리키는 포인터를 만드는 겁니다. 그래서 <code>npm update</code>로 패키지가 바뀌면 에이전트가 읽는 내용도 자동으로 바뀝니다.</p>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/aed82d7d-9aa2-47fd-87d4-bf0ce70d76e8/image.png" alt=""></p>
<hr>
<h2 id="설정-방법">설정 방법</h2>
<h3 id="공급자-패키지-구조">공급자: 패키지 구조</h3>
<pre><code>my-sdk/
├── package.json
├── dist/
└── skills/
    └── data-integrity/   ← 스킬 이름
        └── SKILL.md</code></pre><p><code>package.json</code>에서 <code>files</code> 필드에 <code>skills</code>를 반드시 포함해야 합니다. 빠뜨리면 <code>npm publish</code>할 때 해당 디렉토리가 제외됩니다.</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;@my-org/my-sdk&quot;,
  &quot;version&quot;: &quot;1.3.0&quot;,
  &quot;files&quot;: [
    &quot;dist&quot;,
    &quot;skills&quot;,
    &quot;index.d.ts&quot;
  ]
}</code></pre>
<h3 id="소비자-설치-및-설정">소비자: 설치 및 설정</h3>
<pre><code class="language-bash">npm i -D skills-npm</code></pre>
<pre><code class="language-json">{
  &quot;scripts&quot;: {
    &quot;prepare&quot;: &quot;skills-npm&quot;
  }
}</code></pre>
<p><code>prepare</code> 훅을 쓰는 이유는 <code>npm install</code> 직후 자동으로 실행되기 때문입니다. 팀원이 클론하고 <code>npm install</code>만 해도 세팅이 끝납니다.</p>
<p><code>.gitignore</code>에 아래도 추가합니다. 심볼릭 링크를 커밋할 필요는 없습니다.</p>
<pre><code>skills/npm-*</code></pre><h3 id="고급-설정-skills-npmconfigts">고급 설정: <code>skills-npm.config.ts</code></h3>
<p>모노레포 환경이거나 특정 패키지의 스킬만 주입하고 싶을 때 씁니다.</p>
<pre><code class="language-ts">import { defineConfig } from &#39;skills-npm&#39;

export default defineConfig({
  source: &#39;package.json&#39;,
  agents: [&#39;cursor&#39;, &#39;claude-code&#39;],
  gitignore: true,
  include: [&#39;@my-org/*&#39;], // 사내 SDK 스킬만 주입, 외부 패키지 제외
})</code></pre>
<p><code>include</code>를 안 쓰면 <code>node_modules</code> 전체를 스캔합니다. 외부 패키지가 skills를 가지고 있는 경우 원치 않는 지침이 주입될 수 있으니, 조직 스코프(<code>@my-org/*</code>)로 필터링하는 게 안전합니다.</p>
<hr>
<h2 id="git-기반-vs-npm-기반-비교">Git 기반 vs npm 기반 비교</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th align="center">Git 기반 (<code>npx skills add</code>)</th>
<th align="center">npm 기반 (<code>skills-npm</code>)</th>
</tr>
</thead>
<tbody><tr>
<td>버전 관리</td>
<td align="center">툴과 별도 관리</td>
<td align="center">툴과 함께 번들</td>
</tr>
<tr>
<td>설치 방식</td>
<td align="center">클론 + 심볼릭 링크</td>
<td align="center"><code>npm install</code> + 심볼릭 링크</td>
</tr>
<tr>
<td>업데이트</td>
<td align="center">수동 재클론</td>
<td align="center"><code>npm update</code></td>
</tr>
<tr>
<td>팀 공유</td>
<td align="center">파일 커밋 또는 재클론</td>
<td align="center">패키지 의존성으로 공유</td>
</tr>
<tr>
<td>버전 고정</td>
<td align="center">어려움</td>
<td align="center">lockfile로 재현 가능</td>
</tr>
<tr>
<td>프라이빗 배포</td>
<td align="center">제한적</td>
<td align="center">프라이빗 레지스트리 활용 가능</td>
</tr>
</tbody></table>
<p>두 방식은 경쟁 관계가 아닙니다. Git 기반은 npm에 올라오지 않은 커뮤니티 스킬이나 빠른 실험에 여전히 편하고, npm 기반은 팀이 직접 만든 SDK에 지침을 번들링하거나 버전 고정이 중요한 환경에 적합합니다.</p>
<hr>
<h2 id="주의할-점">주의할 점</h2>
<p>몇 가지 트레이드오프가 있습니다.</p>
<p><strong>추가 패키징 작업이 생깁니다.</strong> SDK 릴리즈마다 <code>SKILL.md</code>도 챙겨야 합니다. 코드 변경 없이 지침만 업데이트하는 경우라도 버전을 올려야 합니다. 처음엔 번거롭습니다.</p>
<p><strong>토큰 관리는 여전히 필요합니다.</strong> <code>skills-npm</code>이 심볼릭 링크를 깔아준다고 해서 에이전트가 알아서 적절히 읽는 건 아닙니다. 로더 설정에 따라 모든 스킬이 한꺼번에 컨텍스트에 들어올 수도 있습니다. 스킬이 많아질수록 메타데이터만 먼저 노출하고 필요한 것만 로드하는 레이지 로딩 전략을 고민해야 합니다. 무지성으로 다 넣으면 토큰만 낭비됩니다.</p>
<p><strong>에이전트마다 지원 방식이 다릅니다.</strong> Cursor, Claude Code, Codex 스킬 파일을 읽는 방식이 완전히 동일하지 않습니다. <code>skills-npm.config.ts</code>의 <code>agents</code> 옵션으로 타깃 에이전트를 명시하는 게 좋습니다.</p>
<hr>
<h2 id="semver로-지침-변경을-관리하는-방법">semver로 지침 변경을 관리하는 방법</h2>
<p>지침도 코드처럼 semver로 관리하면 의도치 않은 변경을 막을 수 있습니다.</p>
<ul>
<li><strong>Patch (0.0.x)</strong>: 오탈자 수정, 예시 개선, 설명 명확화</li>
<li><strong>Minor (0.x.0)</strong>: 새 API 사용법 추가, 기존 내용은 건드리지 않음</li>
<li><strong>Major (x.0.0)</strong>: API 시그니처 변경, 기존 권장 방식 폐기, 스킬 이름 변경</li>
</ul>
<p>production 에이전트 워크플로라면 버전을 고정(<code>&quot;1.3.0&quot;</code>)하는 게 낫습니다. 범위(<code>&quot;^1.3.0&quot;</code>)로 쓰면 마이너 업데이트에 지침이 자동으로 바뀌는데, 에이전트가 갑자기 다른 패턴으로 코드를 짜기 시작하면 디버깅이 귀찮아집니다.</p>
<hr>
<h2 id="마치며">마치며</h2>
<p><code>skills-npm</code>이 해결하는 건 사실 단순합니다. &quot;AI 지침이 코드랑 같이 버전 관리되면 좋겠다&quot;는 것이죠. npm이 이미 그 인프라를 갖추고 있으니, 거기에 올라타는 겁니다.</p>
<p>아직 현장에서 이 컨벤션을 지원하는 npm 패키지 수가 많지는 않습니다. <code>@slidev/cli</code>, <code>eslint-vitest-rule-tester</code>, <code>@vueuse/skills</code> 등 몇 가지가 있고, 생태계는 만들어지는 중입니다. 직접 쓰는 내부 SDK가 있다면 <code>SKILL.md</code> 하나 추가하고 <code>package.json</code>에 <code>&quot;skills&quot;</code> 한 줄 넣는 것부터 시작해볼 만합니다. 앞으로 npm 패키지에 skills를 넣으며 관리를 해봐야겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[vercel/ai-chatbot] AI chatbot 프론트엔드는 어떤 구조로 이루어졌나]]></title>
            <link>https://velog.io/@good-jinu/vercelai-chatbot-AI-chatbot%EC%9D%80-%EC%96%B4%EB%96%A4-%EA%B5%AC%EC%A1%B0%EB%A1%9C-%EC%9D%B4%EB%A3%A8%EC%96%B4%EC%A1%8C%EB%82%98</link>
            <guid>https://velog.io/@good-jinu/vercelai-chatbot-AI-chatbot%EC%9D%80-%EC%96%B4%EB%96%A4-%EA%B5%AC%EC%A1%B0%EB%A1%9C-%EC%9D%B4%EB%A3%A8%EC%96%B4%EC%A1%8C%EB%82%98</guid>
            <pubDate>Tue, 06 May 2025 03:26:49 GMT</pubDate>
            <description><![CDATA[<p>AI 챗봇을 직접 구축해보기 위해서, Vercel에서 제공하는 <a href="https://github.com/vercel/ai-chatbot/tree/v3.0.19">ai-chatbot 템플릿 v3.0.19</a>을 활용해보았습니다. 해당 템플릿은 Next.js를 기반으로 하고 있으며, vercel의 ai-sdk를 사용하기 때문에 OpenAI, Google gemini등 다양한 API를 연동하여 기본적인 대화형 챗봇 기능을 구현할 수 있습니다.</p>
<h1 id="ai-chatbot-의-구조-분석">ai-chatbot 의 구조 분석</h1>
<p>ai-chatbot 은 보편적으로 사용되는 UI와 기능들을 갖추고 있습니다.</p>
<ul>
<li>로그인 및 회원가입</li>
<li>메시지 입력 후 챗봇 답변 받기</li>
<li>LLM 모델 선택</li>
<li>각 채팅 별 세션 분리</li>
<li>입력한 메시지 수정 기능</li>
<li>여러 Tool 제공<ul>
<li>날씨 조회</li>
<li>문서(document) 작성 기능</li>
</ul>
</li>
<li><strong>Artifact</strong> 기능<ul>
<li>문서(document) 작성<ul>
<li>Text kind</li>
<li>Code kind</li>
<li>Sheet kind</li>
<li>Image kind</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="구조-분석">구조 분석</h2>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/c42c29cd-3701-4420-8a65-6e35eac5ac7c/image.png" alt="ai-chatbot-architecture"></p>
<p>Next app 라우터로 auth 그룹과 chat 그룹을 <a href="https://nextjs.org/docs/app/building-your-application/routing/route-groups">Route group</a>으로 나눠놨습니다.
chat 그룹에는 챗봇 기능에 대한 컴포넌트들이 모여있습니다. Chat이라는 컴포넌트가 챗봇 기능을 담당하는 핵심적인 컴포넌트입니다. 구조는 다음과 같습니다:</p>
<h3 id="page-컴포넌트-분석">Page 컴포넌트 분석</h3>
<h4 id="역할">역할</h4>
<p><code>Page</code> 컴포넌트는 서버 사이드 페이지 컴포넌트로, 특정 채팅 ID에 해당하는 채팅 데이터를 가져와 클라이언트 컴포넌트인 <code>Chat</code>과 <code>DataStreamHandler</code>를 렌더링합니다. 인증, 권한 확인, 데이터 조회 및 변환을 처리합니다.</p>
<h4 id="주요-기능">주요 기능</h4>
<ol>
<li><strong>파라미터 처리</strong><ul>
<li><code>props.params.id</code>를 통해 동적 라우트에서 채팅 ID를 추출. (<a href="https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes">Next.js Dynamic route 참고</a>)</li>
</ul>
</li>
<li><strong>데이터 조회</strong><ul>
<li><code>getChatById(id)</code>: DB에서 채팅 데이터를 조회.</li>
<li><code>getMessagesByChatId(id)</code>: 해당 채팅의 메시지 목록을 조회.</li>
<li>조회된 채팅이 없으면 <code>notFound()</code>로 404 응답.</li>
</ul>
</li>
<li><strong>인증 및 권한 확인</strong><ul>
<li><code>auth()</code>를 호출해 사용자 세션을 확인.</li>
<li>세션이 없으면 <code>/api/auth/guest</code>로 리다이렉트.</li>
<li>채팅이 <code>private</code>인 경우:<ul>
<li>세션에 사용자 정보가 없거나, 사용자가 채팅 소유자가 아니면 <code>notFound()</code> 호출.</li>
</ul>
</li>
</ul>
</li>
<li><strong>데이터 변환</strong><ul>
<li><code>convertToUIMessages</code>: DB 메시지(<code>DBMessage</code>)를 클라이언트용 <code>UIMessage</code> 포맷으로 변환.<ul>
<li><code>id</code>, <code>parts</code>, <code>role</code>, <code>createdAt</code>, <code>experimental_attachments</code>를 매핑.</li>
<li><code>content</code>는 곧 deprecated될 예정이므로 빈 문자열로 설정.</li>
</ul>
</li>
</ul>
</li>
<li><strong>쿠키 기반 모델 선택</strong><ul>
<li><code>cookies()</code>를 사용해 <code>chat-model</code> 쿠키를 확인.</li>
<li>쿠키가 없으면 <code>DEFAULT_CHAT_MODEL</code>을 사용.</li>
<li>쿠키가 있으면 해당 값을 <code>selectedChatModel</code>로 전달.</li>
</ul>
</li>
</ol>
<h4 id="특징">특징</h4>
<ul>
<li>서버 사이드에서 실행되며, 클라이언트로 데이터를 전달하기 전에 인증과 권한을 철저히 확인.</li>
<li>쿠키를 활용해 사용자 선호 모델을 동적으로 반영.</li>
<li>메시지 데이터를 클라이언트 포맷으로 변환해 <code>Chat</code> 컴포넌트에 전달.</li>
</ul>
<hr>
<h3 id="chat-컴포넌트-분석">Chat 컴포넌트 분석</h3>
<h4 id="역할-1">역할</h4>
<p><code>Chat</code> 컴포넌트는 클라이언트 사이드에서 동작하는 채팅 UI 컴포넌트로, 사용자와 AI 간의 대화를 관리하고 표시합니다. <code>@ai-sdk/react</code>의 <code>useChat</code> 훅을 사용해 메시지 상태를 관리하며, 메시지 입력, 투표, 아티팩트 등을 처리합니다.</p>
<h4 id="주요-기능-1">주요 기능</h4>
<ol>
<li><strong>상태 관리</strong><ul>
<li><code>useChat</code> 훅(<a href="https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat">ai-sdk 문서 참고</a>):<ul>
<li><code>messages</code>, <code>setMessages</code>: 메시지 목록 관리.</li>
<li><code>input</code>, <code>setInput</code>: 사용자 입력 관리.</li>
<li><code>handleSubmit</code>, <code>append</code>: 메시지 전송 및 추가.</li>
<li><code>status</code>, <code>stop</code>, <code>reload</code>: 채팅 상태 및 제어.</li>
<li>커스텀 설정:<ul>
<li><code>id</code>: 채팅 ID.</li>
<li><code>initialMessages</code>: 초기 메시지 목록.</li>
<li><code>experimental_throttle</code>: 100ms로 요청 제한.</li>
<li><code>generateId</code>: UUID 생성.</li>
<li><code>experimental_prepareRequestBody</code>: 요청 본문에 <code>id</code>, <code>message</code>, <code>selectedChatModel</code> 포함.</li>
<li><code>onFinish</code>: 채팅 완료 시 사이드바 히스토리 캐시 갱신.</li>
<li><code>onError</code>: 에러 발생 시 토스트 알림 표시.</li>
</ul>
</li>
</ul>
</li>
<li><code>useState</code>:<ul>
<li><code>attachments</code>: 첨부 파일 상태 관리.</li>
</ul>
</li>
<li><code>useSWR</code>(<a href="https://swr.vercel.app/ko/docs/typescript.ko#useswr">SWR 문서 참고</a>):<ul>
<li><code>/api/vote?chatId=${id}</code>에서 투표 데이터 조회 (메시지가 2개 이상일 때).</li>
</ul>
</li>
<li><code>useArtifactSelector</code>: 아티팩트 표시 여부 확인.</li>
</ul>
</li>
<li><strong>UI 구성</strong><ul>
<li><code>ChatHeader</code>: 채팅 상단 바 (모델, 공개 상태, 읽기 전용 여부 표시).</li>
<li><code>Messages</code>: 메시지 목록 표시 및 투표, 메시지 수정, 리로드 기능.</li>
<li><code>MultimodalInput</code>: 텍스트 및 첨부 파일 입력 폼 (읽기 전용이 아닌 경우 표시).</li>
<li><code>Artifact</code>: 추가 콘텐츠 (예: 이미지, 파일) 표시 및 관리.</li>
</ul>
</li>
<li><strong>이벤트 처리</strong><ul>
<li>메시지 전송: <code>handleSubmit</code> 또는 <code>append</code> 호출.</li>
<li>입력 중단: <code>stop</code> 호출.</li>
<li>메시지 리로드: <code>reload</code> 호출.</li>
<li>첨부 파일 관리: <code>setAttachments</code>로 상태 업데이트.</li>
<li>캐시 갱신: <code>mutate</code>로 사이드바 채팅 히스토리 갱신.</li>
</ul>
</li>
</ol>
<h4 id="특징-1">특징</h4>
<ul>
<li>클라이언트 사이드에서 동작하며, 실시간 메시지 및 첨부 파일 관리.</li>
<li><code>useChat</code> 훅을 활용해 AI 대화 상태를 효율적으로 관리.</li>
<li>SWR을 사용해 투표 데이터를 비동기적으로 로드.</li>
<li>읽기 전용 모드와 아티팩트 표시 여부를 동적으로 처리.</li>
<li>에러 핸들링 및 사용자 피드백(토스트) 제공.</li>
</ul>
<hr>
<h3 id="요약-비교">요약 비교</h3>
<ul>
<li><strong>Page</strong>: 서버 사이드에서 데이터 조회, 인증, 권한 확인, 데이터 변환을 처리하며 <code>Chat</code> 컴포넌트에 필요한 데이터를 준비.</li>
<li><strong>Chat</strong>: 클라이언트 사이드에서 사용자 인터랙션과 UI 렌더링을 담당하며, AI 대화와 실시간 상태를 관리.</li>
<li><strong>연결점</strong>: <code>Page</code>가 <code>Chat</code>에 <code>initialMessages</code>, <code>selectedChatModel</code>, <code>session</code> 등을 전달해 초기 상태를 설정.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Chromium] 브라우저의 원리 알아보기 - Chromium 아키텍처]]></title>
            <link>https://velog.io/@good-jinu/Chromium-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EC%9D%98-%EC%9B%90%EB%A6%AC-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-Chromium-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</link>
            <guid>https://velog.io/@good-jinu/Chromium-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EC%9D%98-%EC%9B%90%EB%A6%AC-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-Chromium-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</guid>
            <pubDate>Sun, 04 May 2025 15:50:58 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/good-jinu/post/73b677e2-032d-438a-8a55-73978c02211d/image.jpg" alt=""></p>
<blockquote>
<p>브라우저 렌더링 과정에 대해서 설명해주세요</p>
</blockquote>
<p>익숙한 질문인가요? 웹 개발자 기술 면접의 단골 질문이지만, 저는 지금까지 &#39;대충&#39; 알고 넘어갔었습니다. 이번 기회에 브라우저를 제대로 알고자 <a href="https://www.chromium.org/chromium-projects/">Chromium project</a>를 분석해보려고합니다.
Chromium을 분석 대상으로 선정한 이유는 현대 웹 브라우저 생태계에서의 압도적인 영향력 때문입니다. 현재 시장 점유율이 가장 높은 Google Chrome을 비롯하여 Microsoft Edge, Opera, Naver Whale, Brave, Vivaldi 등 수많은 주요 웹 브라우저들이 Chromium의 오픈 소스 코드를 기반으로 개발되고 있습니다.
이 글에서는 브라우저의 동작 방식부터 Chromium의 실제 구조까지 파헤쳐보겠습니다.</p>
<h1 id="🤔-브라우저는-어떻게-동작할까">🤔 브라우저는 어떻게 동작할까?</h1>
<p>먼저 브라우저가 어떻게 동작하는지 알아보겠습니다.
<a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/How_browsers_work">MDN의 How browsers work 문서</a>에 따르면 브라우저의 렌더링 단계를 다음과 같이 설명합니다.</p>
<h2 id="⚡️-핵심-렌더링-단계">⚡️ 핵심 렌더링 단계</h2>
<ol>
<li>파싱 (Parsing)<ul>
<li>HTML → DOM 트리 변환 (HTMLParser)</li>
<li>CSS → CSSOM 트리 변환 (StyleEngine 처리)</li>
</ul>
</li>
<li>렌더 트리 생성<ul>
<li>DOM + CSSOM 결합 → 화면에 표시될 요소만 선택</li>
<li>display: none 요소는 제외, visibility: hidden은 포함</li>
</ul>
</li>
<li>레이아웃 (Reflow)<ul>
<li>뷰포트 크기 기반으로 픽셀 단위 위치 계산</li>
<li>상대 단위(%, rem) → 절대 픽셀 값으로 변환</li>
</ul>
</li>
<li>페인트 (Paint)<ul>
<li>그리기 명령(PaintRecord) 생성</li>
<li>레이어 분할: 3D transform, opacity 등의 속성은 별도 레이어로 렌더링</li>
</ul>
</li>
<li>합성 (Compositing)<ul>
<li>GPU 가속으로 레이어 병합 (컴포지터 스레드 담당)</li>
<li>스크롤, 애니메이션은 리페인트 없이 처리 가능</li>
</ul>
</li>
</ol>
<blockquote>
<p>브라우저는 DOM/CSSOM → 렌더 트리 → 레이아웃 → 페인트 → 합성 과정을 거칩니다.</p>
</blockquote>
<h1 id="🌐-chromium은-무엇인가">🌐 Chromium은 무엇인가?</h1>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/9e163e3d-71c4-452d-9003-4e7093fae8d4/image.png" alt=""></p>
<p>Chromium은 Google이 주도하는 오픈 소스 웹 브라우저 프로젝트입니다. 우리가 흔히 사용하는 Google Chrome 브라우저는 바로 이 Chromium 프로젝트의 소스 코드를 기반으로 만들어졌습니다. (Chrome 과 Chromium은 같은게 아닙니다!)</p>
<p>주요 특징은 다음과 같습니다:</p>
<ol>
<li>핵심 엔진 제공 (Blink, V8)<ul>
<li>Blink 렌더링 엔진 (WebKit에서 파생):<ul>
<li><a href="https://www.chromium.org/blink/">Blink 소개</a>: Blink가 Chromium 프로젝트의 렌더링 엔진임을 설명합니다.</li>
<li><a href="https://developer.mozilla.org/ko/docs/Glossary/Blink">MDN 웹 문서 용어 사전 - Blink</a>: Blink가 WebKit의 WebCore 라이브러리 포크로 시작되었음을 설명합니다.</li>
<li><a href="https://en.wikipedia.org/wiki/Blink_(browser_engine)">Wikipedia - Blink (browser engine)</a>: Blink가 Chromium 프로젝트의 일부이며, WebKit에서 포크(fork)된 배경을 설명합니다.</li>
</ul>
</li>
<li>V8 JavaScript 엔진:<ul>
<li><a href="https://v8.dev/">V8 공식 웹사이트</a></li>
<li><a href="https://en.wikipedia.org/wiki/V8_(JavaScript_engine)">Wikipedia - V8 (JavaScript engine)</a>: V8 엔진이 Google Chrome 브라우저를 위해 개발되었고 Chromium 프로젝트의 일부임을 설명합니다.</li>
</ul>
</li>
</ul>
</li>
<li>Chrome과의 차이 (독점 기능, 코덱 등)<ul>
<li><a href="https://www.browserstack.com/guide/difference-between-chrome-and-chromium">Chrome vs Chromium 비교</a>: 자동 업데이트, 미디어 코덱(MP3, H.264, AAC) 및 Flash 지원(과거), 오류 보고 등 Chrome에 추가된 기능과 Chromium과의 차이를 설명합니다.</li>
<li><a href="https://bugbug.io/blog/software-testing/chrome-vs-chromium/">Chrome vs Chromium 주요 차이점</a>: Chrome이 Chromium 기반에 자동 업데이트, Google 동기화, 미디어 코덱 등 독점 코드를 추가했다고 설명합니다.</li>
<li><a href="https://support.google.com/chrome/thread/272064373/chrome-v-s-chromium">Google Chrome과의 차이</a>: Chrome은 독점 라이선스를 가지며 MP3, H.264, AAC 코덱 및 Adobe Flash(과거)를 지원하지만 Chromium은 그렇지 않다고 설명합니다.</li>
</ul>
</li>
<li>빠른 개발 주기 (기능 실험 및 테스트)<ul>
<li><a href="https://chromium.googlesource.com/chromium/src/+/HEAD/docs/process/release_cycle.md">Chromium 공식 문서</a> - Chrome 릴리스 주기: 현재 Chrome이 4주마다 새로운 메이저 버전을 안정 채널(Stable Channel)에 출시하는 빠른 주기를 설명합니다. 새로운 기능은 메인 브랜치에서 개발되고 테스트 단계를 거쳐 배포됩니다.</li>
<li><a href="https://www.chromium.org/blink/launching-features/">Chromium 공식 문서</a> - 기능 출시 프로세스: 새로운 기능이 Chromium 내에서 프로토타입으로 시작하여 플래그(flag) 뒤에서 개발되고, 개발자 테스트(Dev Trials), 오리진 트라이얼(Origin Trial) 등을 거쳐 최종적으로 출시되는 과정을 설명합니다. 이는 Chrome 안정 버전에 포함되기 전에 Chromium에서 먼저 구현되고 테스트됨을 보여줍니다.</li>
</ul>
</li>
</ol>
<h1 id="🏰-chromium-아키텍처-탐구-탭-하나가-멈춰도-전체-브라우저는-괜찮은-이유">🏰 Chromium 아키텍처 탐구: 탭 하나가 멈춰도 전체 브라우저는 괜찮은 이유</h1>
<p><em>참고 문서 <a href="https://www.chromium.org/developers/design-documents/multi-process-architecture/">https://www.chromium.org/developers/design-documents/multi-process-architecture/</a></em></p>
<p>혹시 웹 서핑 중 특정 웹 페이지 하나 때문에 브라우저 전체가 멈추거나 꺼져버리는 답답한 경험을 해보신 적 있나요? 2000년대 중반의 웹 브라우저들은 종종 이런 문제를 겪곤 했습니다. 마치 오래된 싱글태스킹 운영체제에서 응용 프로그램 하나가 시스템 전체를 마비시키는 것처럼, 웹 페이지나 플러그인 하나가 브라우저 전체를 멈추게 만들 수 있었습니다.</p>
<p>하지만 오늘날 우리가 사용하는 Chrome과 같은 최신 브라우저들은 훨씬 안정적입니다. 그 비결은 바로 Chromium의 멀티 프로세스 아키텍처(Multi-Process Architecture) 에 있습니다. Chromium은 이 아키텍처를 통해 마치 현대 운영체제가 여러 응용 프로그램을 독립된 프로세스로 분리하여 안정성과 보안을 확보하는 것과 같은 이점을 웹 브라우징 환경에 가져왔습니다.</p>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/0203a7ab-bc2f-47c3-8ce7-b85ed60513ff/image.png" alt=""></p>
<h2 id="핵심-아이디어-분리하고-보호">핵심 아이디어: 분리하고 보호</h2>
<p>Chromium은 브라우저의 기능을 여러 개의 독립적인 <strong>프로세스(Process)</strong> 로 나눕니다.</p>
<ol>
<li>브라우저 프로세스 (Browser Process):<ul>
<li>브라우저의 &#39;두뇌&#39; 역할을 합니다.</li>
<li>사용자 인터페이스(UI)를 담당하고, 다른 모든 프로세스(렌더러, GPU 등)를 관리합니다.</li>
<li>각 렌더러 프로세스와 통신하며 상태를 관리하는 RenderProcessHost 객체를 가집니다.</li>
</ul>
</li>
<li>렌더러 프로세스 (Renderer Process):<ul>
<li>실제 웹 페이지를 화면에 그리는(렌더링) 작업을 담당합니다. 각 탭이나 프레임은 일반적으로 별도의 렌더러 프로세스에서 실행됩니다. (물론, 특정 조건 하에서는 프로세스를 공유하기도 합니다.)</li>
<li>HTML 파싱과 레이아웃을 위해 Blink 렌더링 엔진을 사용합니다.
각 렌더러 프로세스는 부모인 브라우저 프로세스와 통신하고 전역 상태를 관리하는 RenderProcess 객체를 가집니다.</li>
<li>각 문서(프레임)는 RenderFrame 객체에 해당하며, 이는 브라우저 프로세스의 RenderFrameHost와 통신합니다.</li>
</ul>
</li>
</ol>
<h2 id="멀티-프로세스-아키텍처의-장점">멀티 프로세스 아키텍처의 장점</h2>
<p>이렇게 프로세스를 분리함으로써 Chromium은 여러 가지 중요한 이점을 얻습니다.</p>
<ol>
<li><strong>안정성 (Stability)</strong>: 만약 하나의 탭(렌더러 프로세스)에서 오류가 발생하거나 멈추더라도, 해당 탭만 영향을 받고 브라우저 전체가 다운되지 않습니다. 문제가 생긴 탭에는 &quot;앗, 이런!&quot; 화면이 표시되고, 사용자는 해당 탭만 새로고침하여 복구할 수 있습니다.</li>
<li><strong>보안 (Security)</strong>: 각 렌더러 프로세스는 샌드박스(Sandbox) 라는 제한된 환경에서 실행됩니다. 이는 렌더러 프로세스가 사용자의 파일 시스템, 네트워크, 입력 장치 등에 직접 접근하는 것을 막아줍니다. 만약 악의적인 웹사이트가 렌더러 프로세스를 장악하더라도, 샌드박스 덕분에 시스템 전체에 미치는 피해를 크게 줄일 수 있습니다.</li>
<li><strong>자원 관리 (Resource Management)</strong>: 사용자가 보지 않는 숨겨진 탭이나 백그라운드 탭(렌더러 프로세스)의 우선순위를 낮출 수 있습니다. 시스템 메모리가 부족할 경우, 운영체제는 이런 우선순위가 낮은 프로세스의 메모리를 먼저 디스크로 옮깁니다(스왑 아웃). 이를 통해 사용자가 현재 보고 있는 활성 탭이 더 빠르고 부드럽게 동작하도록 돕습니다. 메모리가 충분할 때는 이 과정이 눈에 띄지 않으므로 성능 저하 없이 효율적인 메모리 관리가 가능합니다.</li>
</ol>
<h1 id="👩🏽🎤-blink-는-어떻게-작동하는가">👩🏽‍🎤 Blink 는 어떻게 작동하는가</h1>
<p><a href="https://docs.google.com/document/d/1aitSOucL0VHZa9Z2vbRJSyAIsAz24kX8LFByQ5xQnUg/edit?tab=t.0">How Blink works</a> 문서에서는 Blink의 작동 방식에 대해서 다음과 같이 설명합니다. 자세한 설명은 <a href="https://docs.google.com/document/d/1aitSOucL0VHZa9Z2vbRJSyAIsAz24kX8LFByQ5xQnUg/edit?tab=t.0">문서</a>를 참고바랍니다.</p>
<h2 id="blink의-역할">Blink의 역할</h2>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/783c205a-3b10-4323-8e75-4a5e6c465ef0/image.png" alt=""></p>
<p><a href="https://www.chromium.org/blink/">Blink</a>는 웹 브라우저 탭 안에서 웹 콘텐츠를 렌더링하는 핵심 엔진입니다. 주요 역할은 다음과 같습니다.</p>
<ul>
<li>웹 표준 구현: HTML, CSS, Web IDL 등 웹 플랫폼의 다양한 기술 표준을 해석하고 처리합니다. (e.g. <a href="https://html.spec.whatwg.org/multipage/?">HTML standard</a>) </li>
<li>JavaScript 실행: V8 JavaScript 엔진을 내장하여 웹 페이지의 동적인 기능을 수행합니다.</li>
<li>리소스 요청: 웹 페이지에 필요한 이미지, 스타일 시트 등의 자원을 네트워크를 통해 가져옵니다.</li>
<li>DOM 트리 구축: 다운로드한 HTML 코드를 바탕으로 웹 페이지의 구조를 나타내는 DOM (Document Object Model) 트리를 만듭니다.</li>
<li>스타일 및 레이아웃 계산: CSS를 해석하여 각 요소의 시각적인 스타일을 결정하고, DOM 트리를 기반으로 화면에 어떻게 배치할지 계산합니다.</li>
<li>그래픽 렌더링: 계산된 스타일과 레이아웃 정보를 바탕으로 실제 화면에 콘텐츠를 그립니다 (<a href="https://chromium.googlesource.com/chromium/src/+/HEAD/cc/README.md">Chrome Compositor 활용</a>).</li>
</ul>
<p>Blink는 웹 페이지의 코드를 해석하고 시각적으로 표현하여 사용자가 웹 콘텐츠를 볼 수 있도록 하는 핵심적인 렌더링 기능을 제공하는 엔진입니다. Chromium, Opera, Android WebView와 같은 다양한 웹 브라우저 환경에서 <a href="https://chromium.googlesource.com/chromium/src/+/HEAD/content/public/README.md">content public APIs</a>를 통해 사용됩니다.</p>
<h2 id="blink와-프로세스-구조">Blink와 프로세스 구조</h2>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/267e269b-cc44-42f6-a1c7-2a3e2bce5acb/image.png" alt=""></p>
<p>Chromium은 여러 개의 프로세스를 사용하는 구조입니다. 크게 하나의 브라우저 프로세스와 여러 개의 샌드박스 처리된 렌더러 프로세스로 나뉩니다. Blink는 바로 이 렌더러 프로세스 안에서 동작합니다.</p>
<h3 id="렌더러-프로세스는-왜-이렇게-많을까요">렌더러 프로세스는 왜 이렇게 많을까요?</h3>
<p>가장 큰 이유는 보안 때문입니다. 서로 다른 웹사이트의 정보가 담긴 메모리 공간을 분리하는 것이 중요합니다. 이를 <strong>사이트 격리(Site Isolation)</strong>라고 부릅니다. 이상적으로는 각 렌더러 프로세스가 하나의 웹사이트만을 담당해야 합니다.</p>
<p>하지만 현실은 조금 다릅니다. 사용자가 너무 많은 탭을 열거나 기기의 메모리가 부족할 때는 하나의 렌더러 프로세스가 여러 개의 다른 웹사이트 (iframe이나 탭)를 함께 처리하기도 합니다. 따라서 렌더러 프로세스, iframe, 탭 사이에는 1:1 관계가 없습니다.</p>
<h3 id="샌드박스-안에서-blink는-뭘-할-수-있을까요">샌드박스 안에서 Blink는 뭘 할 수 있을까요?</h3>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/621998e8-37a7-45e0-9ff7-8575f88743b2/image.png" alt=""></p>
<p>렌더러 프로세스는 샌드박스라는 제한된 환경에서 실행되기 때문에, 시스템 자원(파일 접근, 오디오 재생 등)에 직접 접근하거나 사용자 정보(쿠키, 비밀번호 등)를 함부로 사용할 수 없습니다. Blink가 이러한 작업을 수행하려면 브라우저 프로세스에 요청해야 합니다.</p>
<h3 id="브라우저-프로세스와-렌더러-프로세스는-어떻게-소통할까요">브라우저 프로세스와 렌더러 프로세스는 어떻게 소통할까요?</h3>
<p>이 둘 사이의 통신은 Mojo라는 기술을 통해 이루어집니다. 과거에는 Chromium IPC라는 방식을 사용했지만, 현재는 <a href="https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md">Mojo</a>로 대체되고 있습니다. Chromium 내부적으로는 브라우저 프로세스를 여러 개의 &quot;서비스&quot;로 나누는 작업(Servicification)이 진행 중이며, Blink 입장에서는 Mojo를 통해 이러한 서비스들과 편리하게 상호작용할 수 있습니다.</p>
<h2 id="렌더러-프로세스-내의-스레드-구조">렌더러 프로세스 내의 스레드 구조</h2>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/3f5c87a4-8f42-44d7-9450-9e702b3199f7/image.png" alt=""></p>
<p>하나의 렌더러 프로세스 안에는 다음과 같은 종류의 스레드가 존재합니다.</p>
<ul>
<li>메인 스레드 (Main Thread): Blink에서 가장 중요한 작업들이 대부분 이 스레드에서 처리됩니다. JavaScript 실행 (Worker 제외), DOM 조작, CSS 처리, 스타일 계산, 레이아웃 계산 등이 모두 메인 스레드에서 이루어집니다. Blink는 이 메인 스레드의 성능을 최대한으로 끌어올리도록 매우 잘 최적화되어 있습니다. (대부분의 작업이 단일 스레드에서 처리된다고 가정합니다.)</li>
<li>워커 스레드 (Worker Threads, N개): <a href="https://html.spec.whatwg.org/multipage/workers.html#workers">웹 워커(Web Workers)</a>, <a href="https://w3c.github.io/ServiceWorker/">서비스 워커(ServiceWorker)</a>, <a href="https://html.spec.whatwg.org/multipage/worklets.html">워크릿(Worklets)</a>과 같은 별도의 작업을 병렬로 처리하기 위해 Blink가 필요에 따라 여러 개의 워커 스레드를 생성할 수 있습니다.</li>
<li>내부 스레드 (Internal Threads, 몇 개): Blink와 V8 엔진 내부적으로 특정 작업을 처리하기 위해 사용되는 스레드들입니다. 예를 들어, 웹 오디오 처리, 데이터베이스 작업, 가비지 컬렉션(GC) 등이 이러한 내부 스레드에서 실행될 수 있습니다.</li>
</ul>
<p><strong>스레드 간 통신 방식:</strong></p>
<p>서로 다른 스레드끼리 정보를 주고받기 위해서는 메시지 전달 방식을 사용해야 합니다. PostTask API를 통해 작업을 특정 스레드의 작업 큐에 추가하는 방식으로 통신합니다. 공유 메모리 방식은 성능상의 특별한 이유가 있는 극히 일부 경우를 제외하고는 권장되지 않습니다. 따라서 Blink 코드베이스에서 MutexLock과 같은 동기화 메커니즘을 많이 찾아볼 수 없는 이유이기도 합니다.</p>
<h2 id="렌더링-파이프라인">렌더링 파이프라인</h2>
<p>그래서 어떻게 렌더링 하는가에 대한 것이 웹 프론트엔드 개발자에게 중요할 것입니다. 렌더링 각 단계에 대한 자세한 설명은 <a href="https://docs.google.com/presentation/d/1boPxbgNrTU0ddsc144rcXayGA_WF53k96imRH8Mp34Y/edit#slide=id.p">이 문서</a>를 참고하세요.</p>
<p>전체적인 렌더링 흐름은 다음과 같습니다:</p>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/3525da7b-ead9-4c74-b1f6-da145a77c4c3/image.png" alt=""></p>
<p>조금 더 자세한 다이어그램:</p>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/01463491-e559-41d5-9079-911859ed4c85/image.png" alt=""></p>
<h1 id="👾-chromium-에-대해-알아보며">👾 Chromium 에 대해 알아보며...</h1>
<p>처음에는 단순히 &quot;브라우저가 어떻게 화면을 그리는지&quot;에 대한 호기심으로 시작했던 여정이었습니다. 렌더링 과정의 각 단계를 하나씩 따라가다 보니, 그 뒤에 숨겨진 Chromium의 거대한 아키텍처와 혁신적인 아이디어들을 마주하게 되었습니다.</p>
<p>Chromium에 대해 더 알아본 뒤에 <a href="https://firefox-source-docs.mozilla.org/">Firefox</a>와 최근에 개발이 시작된 <a href="https://github.com/LadybirdBrowser/ladybird">ladybird</a> 프로젝트 등도 살펴봐야 겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 카테고리별 그룹화된 테이블 구현]]></title>
            <link>https://velog.io/@good-jinu/React-%EC%B9%B4%ED%85%8C%EA%B3%A0%EB%A6%AC%EB%B3%84-%EA%B7%B8%EB%A3%B9%ED%99%94%EB%90%9C-%ED%85%8C%EC%9D%B4%EB%B8%94-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@good-jinu/React-%EC%B9%B4%ED%85%8C%EA%B3%A0%EB%A6%AC%EB%B3%84-%EA%B7%B8%EB%A3%B9%ED%99%94%EB%90%9C-%ED%85%8C%EC%9D%B4%EB%B8%94-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sat, 30 Nov 2024 16:12:46 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>요약
테이블 데이터에서 특정 열들을 그룹으로 묶어서 보여주는 컴포넌트를 구현하는 과정에 관한 글입니다.
테이블을 구현하는데에 그루핑한 데이터를 표현하는 방법을 찾는 분들께 아이디어가 될 수 있습니다.</p>
</blockquote>
<p>이번 글에서는 <a href="https://velog.io/@good-jinu/React-SheetJS-%EC%97%91%EC%85%80-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%98%EA%B3%A0-%ED%85%8C%EC%9D%B4%EB%B8%94%EB%A1%9C-%EB%82%98%ED%83%80%EB%82%B4%EB%8A%94-%EB%B0%A9%EB%B2%95">엑셀 파일을 테이블로 나타내기</a>에 이어서, 로드한 데이터를 그루핑하여 테이블로 나타내는 법을 공유드리려고 합니다.</p>
<p>상품 판매내역 엑셀 파일에서 상품을 그루핑하여 한번에 상품 요소들만 봐야하는 경우가 있습니다. 데이터를 그룹화하여 테이블 형태로 보여주는 React 컴포넌트를 설계하고 구현하는 방법에 대해 자세히 다루어 보겠습니다.</p>
<h2 id="🤔-문제-정의">🤔 문제 정의</h2>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/252de619-4f49-4e20-8ec0-9ee5438f0967/image.png" alt="판매내역 데이터"></p>
<p>쇼핑몰 운영자는 상품 판매내역 파일을 받아서 확인합니다. 문제는 위의 이미지처럼 상품들이 판매된 순서대로 나열되어 있어서 상품들을 구조화해서 확인하기가 어렵습니다.
그래서 같은 카테고리로 묶고 각 옵션도 상품에 따라 묶어서 아래와 같이 나타내려고 합니다.</p>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/702f8342-787d-4945-9e5a-d1b1cad4648f/image.png" alt="그룹화된 테이블"></p>
<p>단순히 나열되어 있던 형태에서 각 카테고리 및 상품에 어떤 요소가 속해있는지 더욱 보기 쉬워졌습니다.</p>
<p>이때, column 값은 사용자가 정의함에 따라 무엇이든 될 수 있습니다. 왼쪽에 있는 column일수록 상위 그룹입니다. 그룹화해야하는 column의 수가 최대 10개라고 가정할 때 테이블 컴포넌트를 어떻게 설계하고 구현해야 할까요?</p>
<h3 id="👀-데이터를-그룹화하여-보여주는-플로우">👀 데이터를 그룹화하여 보여주는 플로우</h3>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/7964d3f7-e573-41e2-983e-140012e0c695/image.png" alt=""></p>
<p>사용자가 엑셀 파일을 업로드하고 이를 그루핑 테이블로 변환하는 과정은 다음과 같습니다.</p>
<ol>
<li>엑셀 파일 업로드</li>
<li>엑셀 파일을 JSON 데이터로 변환</li>
<li>사용자가 그루핑하기를 원하는 column 선택</li>
<li>그루핑 대상 column으로 그루핑 데이터 생성</li>
<li>그루핑 데이터를 그루핑 테이블을 통하여 렌더링</li>
</ol>
<p>여기서 4~5번 플로우를 구현해야 하므로 해당 부분을 좀더 구체화하도록 하겠습니다.
<br/></p>
<hr>
<h2 id="🔎-그루핑-테이블-스펙">🔎 그루핑 테이블 스펙</h2>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/5c1be1d6-6b1d-4560-8db3-41d369298dfb/image.png" alt=""></p>
<p>그루핑 테이블과 원본 데이터 테이블에 비해서 다음의 특징을 가집니다.</p>
<ul>
<li>중복되는 값이 존재하지 않습니다.</li>
<li>같은 그룹일 경우에는 그룹 대상 값의 셀이 하위 셀들을 모두 포함하도록 행병합되어 나타납니다.<br/>

</li>
</ul>
<hr>
<h2 id="🌚-데이터-구조-설계">🌚 데이터 구조 설계</h2>
<p>기본 테이블에 보여줄 데이터는 JSON으로 변환되기 때문에 다음과 같이 정의합니다.</p>
<pre><code class="language-typescript">// table/tableData.ts

export type TableData = Record&lt;string, string&gt;[]; // key-value 배열</code></pre>
<p>그렇다면 그루핑 데이터는 어떻게 해야할까요?
그루핑 데이터를 설계하기 위해서는 그루핑 테이블을 만들기 위해서 어떤 정보가 필요한지 살펴봐야합니다. 그루핑 데이터를 컴포넌트로 작성하면 다음과 같습니다.</p>
<pre><code class="language-tsx">function GroupedTable(): React.ReactElement {
    return (
        &lt;table&gt;
            &lt;thead&gt;
            &lt;tr&gt;
                &lt;th&gt;카테고리&lt;/th&gt;
                &lt;th&gt;상품&lt;/th&gt;
                &lt;th&gt;옵션&lt;/th&gt;
            &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
            &lt;tr&gt;
                &lt;td rowSpan={2}&gt;의류&lt;/td&gt;
                &lt;td&gt;후드&lt;/td&gt;
                &lt;td&gt;사이즈=M&lt;/td&gt;
            &lt;/tr&gt;
            &lt;tr&gt;
                &lt;td&gt;셔츠&lt;/td&gt;
                &lt;td&gt;사이즈=L&lt;/td&gt;
            &lt;/tr&gt;
            &lt;tr&gt;
                &lt;td rowSpan={5}&gt;음식&lt;/td&gt;
                &lt;td rowSpan={2}&gt;감바스&lt;/td&gt;
                &lt;td&gt;팩=4팩&lt;/td&gt;
            &lt;/tr&gt;
            &lt;tr&gt;
                &lt;td&gt;팩=2팩&lt;/td&gt;
            &lt;/tr&gt;
            &lt;tr&gt;
                &lt;td rowSpan={3}&gt;닭강정&lt;/td&gt;
                &lt;td&gt;양념=없음&lt;/td&gt;
            &lt;/tr&gt;
            &lt;tr&gt;
                &lt;td&gt;양념=간장&lt;/td&gt;
            &lt;/tr&gt;
            &lt;tr&gt;
                &lt;td&gt;양념=매운맛&lt;/td&gt;
            &lt;/tr&gt;
            &lt;/tbody&gt;
        &lt;/table&gt;
    );
}</code></pre>
<p>첫번째로 테이블 헤더에 column이 필요합니다.
두번째로 테이블 바디에 보여줄 각 column에 해당하는 값들이 필요합니다.
마지막으로 각 테이블 셀에 rowSpan 값을 넣고있는 것을 확인할 수 있습니다. rowSpan은 해당 셀을 행으로 몇 칸을 차지하게 할 것인지에 대한 속성값입니다.</p>
<p>rowSpan값을 보면 각 그룹들의 최하위 요소들의 개수를 넣고 있습니다. (1개라면 넣지 않습니다. - 그러면 default로 1이 세팅됩니다.)</p>
<p>각 요소들을 그룹화하고 최하단 요소의 개수를 세어서 rowSpan 값으로 지정한다는 점에서 트리 자료구조가 적합하다고 판단했습니다.</p>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/9e13d34d-c047-4d8a-bba8-827a6ddbd02b/image.png" alt=""></p>
<p>위의 트리형태에 중복제거와 그룹별 하위 노드 조회를 빠르게 하기 위해 key value 형태로 하위 노드를 관리하도록 했습니다.</p>
<pre><code class="language-typescript">export interface IGroupedTableDataNode {
    get childrenNodes(): Record&lt;string, IGroupedTableDataNode&gt;;
    // key 값은 column에 해당하는 value &lt;ex) 의류, 음식&gt; 중복되면 안되므로 key값으로 설정
    // value는 그룹에 해당되는 하위 노드

    get values(): string[]; // 노드의 column에 해당하는 value들 배열
    get countLeafNodes(): number; // 말단 노드의 개수

    insert: (value: string) =&gt; void; // 새로운 하위 노드 삽입
}</code></pre>
<p>정의한 데이터 타입 IGroupedTableDataNode를 다음의 플로우에서 활용합니다.</p>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/bf48cb8f-4f1d-48e5-af3c-b2240b378dde/image.png" alt=""></p>
<p>상위컴포넌트가 그룹화 객체 변환 함수에 원본데이터 TableData를 입력하면 함수는 IGroupedTableDataNode를 반환하고 이를 그루핑 테이블에 전달하면 그루핑 테이블 컴포넌트가 생성되게 됩니다.
<br/></p>
<hr>
<h2 id="🛠️-구현">🛠️ 구현</h2>
<p>앞에서 정의한 데이터들을 종합해서 코드를 작성하면 다음과 같습니다.</p>
<pre><code class="language-typescript">// table/TableData.ts
export type TableData = Record&lt;string, string&gt;[];

export interface TableCellInfo {
    // 테이블 셀에 데이터
    value: string;
    rowSpan: number;
}

export type TableRow = TableCellInfo[];
export type TableRows = TableRow[];

export interface IGroupedTableDataNode {
    get childrenNodes(): Record&lt;string, IGroupedTableDataNode&gt;;
    // key 값은 column에 해당하는 value &lt;ex) 의류, 음식&gt; 중복되면 안되므로 key값으로 설정
    // value는 그룹에 해당되는 하위 노드

    get values(): string[]; // 노드의 column에 해당하는 value들 배열
    countLeafNodes(): number; // 말단 노드의 개수

    insert: (value: string) =&gt; void; // 새로운 하위 노드 삽입
}

export class GroupedTableDataNode implements IGroupedTableDataNode {
    private _childrenNodes: Record&lt;string, IGroupedTableDataNode&gt;;

    constructor() {
        this._childrenNodes = {};
    }

    get childrenNodes(): Record&lt;string, IGroupedTableDataNode&gt; {
        return this._childrenNodes;
    }

    get values(): string[] {
        return Object.keys(this._childrenNodes);
    }

    countLeafNodes(): number {
        // 트리의 말단 노드의 개수를 세어주는 메서드
        // 재귀 함수
        // 재귀 탈출 조건은 하위 노드가 없는 것 -&gt; 없을 경우 1반환
        if (this.values.length === 0) {
            return 1;
        }

        let cnt = 0;

        for (const node of Object.values(this._childrenNodes)) {
            cnt += node.countLeafNodes();
        }

        return cnt;
    }

    insert(value: string): void {
        // 새로운 하위 노드 삽입
        if (!this._childrenNodes[value]) {
            // value에 하위 노드가 존재하지 않을 경우 새로운 노드 삽입
            this._childrenNodes[value] = new GroupedTableDataNode();
        }
    }
}</code></pre>
<p>그루핑 테이블 컴포넌트를 소비하는 상위 컴포넌트를 작성하겠습니다.
테스트를 위해서 사용자가 변환하려는 원본데이터와 그루핑하려는 열 이름 순열도 같이 정의하겠습니다.</p>
<p>상위 컴포넌트 코드 예시는 다음과 같습니다.</p>
<pre><code class="language-tsx">// App.tsx
import React from &#39;react&#39;;
import &#39;./App.css&#39;;
import GroupedTable from &quot;./table/GroupedTable&quot;;
import {IGroupedTableDataNode, TableData} from &quot;./table/TableData&quot;;
import {groupingTableData} from &quot;./table/groupingTableData&quot;;

const data: TableData = [
    {
        &#39;카테고리&#39;: &#39;의류&#39;,
        &#39;상품&#39;: &#39;후드&#39;,
        &#39;옵션&#39;: &#39;사이즈=M&#39;
    },
    {
        &#39;카테고리&#39;: &#39;음식&#39;,
        &#39;상품&#39;: &#39;감바스&#39;,
        &#39;옵션&#39;: &#39;팩=4팩&#39;,
    },
    {
        &#39;카테고리&#39;: &#39;음식&#39;,
        &#39;상품&#39;: &#39;닭강정&#39;,
        &#39;옵션&#39;: &#39;양념=없음&#39;
    },
    {
        &#39;카테고리&#39;: &#39;음식&#39;,
        &#39;상품&#39;: &#39;감바스&#39;,
        &#39;옵션&#39;: &#39;팩=2팩&#39;
    },
    {
        &#39;카테고리&#39;: &#39;의류&#39;,
        &#39;상품&#39;: &#39;후드&#39;,
        &#39;옵션&#39;: &#39;사이즈=M&#39;
    },
    {
        &#39;카테고리&#39;: &#39;음식&#39;,
        &#39;상품&#39;: &#39;닭강정&#39;,
        &#39;옵션&#39;: &#39;양념=간장&#39;,
    },
    {
        &#39;카테고리&#39;: &#39;음식&#39;,
        &#39;상품&#39;: &#39;닭강정&#39;,
        &#39;옵션&#39;: &#39;양념=매운맛&#39;,
    },
    {
        &#39;카테고리&#39;: &#39;의류&#39;,
        &#39;상품&#39;: &#39;셔츠&#39;,
        &#39;옵션&#39;: &#39;사이즈=L&#39;,
    },
];

const columns: string[] = [&#39;카테고리&#39;, &#39;상품&#39;, &#39;옵션&#39;];

function App() {
      // data는 원본 데이터, columns는 그루핑하려는 열 이름 순열
    const groupedData: IGroupedTableDataNode = groupingTableData(data, columns);

    return (
        &lt;div className=&quot;App&quot;&gt;
            &lt;section style={{padding: &#39;30px&#39;}}&gt;
                &lt;GroupedTable groupedData={groupedData} columns={columns} /&gt;
            &lt;/section&gt;
        &lt;/div&gt;
    );
}</code></pre>
<br/>

<p>원본데이터를 그루핑테이블 컴포넌트로 전달하기 이전에 변환을 해야합니다.
변환 로직을 수행하는 그룹화 객체 변환 함수 코드입니다.</p>
<pre><code class="language-typescript">// table/groupingTableData.ts

import {GroupedTableDataNode, IGroupedTableDataNode, TableData} from &quot;./TableData&quot;;

export function groupingTableData(tableData: TableData, columns: string[]): IGroupedTableDataNode {
    // 루트 노드 생성
    const root: IGroupedTableDataNode = new GroupedTableDataNode();

    for (const row of tableData) {
        // row 행 하나씩 트리에 삽입
        let curNode: IGroupedTableDataNode = root;

        for (const column of columns) {
            const value = row[column];

            curNode.insert(value);
            curNode = curNode.childrenNodes[value];

            if (!curNode) {
                // curNode가 null이나 undefined일 경우
                throw new Error(&#39;IGroupedTableDataNode 노드가 생성되지 않았습니다.&#39;);
            }
        }
    }

    return root;
}</code></pre>
<p>최종적으로 데이터를 보여주는 그루핑 테이블 컴포넌트 코드입니다.</p>
<pre><code class="language-tsx">// table/GroupedTable

import React from &#39;react&#39;;
import &#39;./DataTable.css&#39;;
import {IGroupedTableDataNode, TableRow, TableRows} from &quot;./TableData&quot;;

// HTMLTableElement 의 data와 children props를 제외하고 상속
export interface GroupedTableProps extends Omit&lt;React.HTMLProps&lt;HTMLTableElement&gt;, &#39;data&#39; | &#39;children&#39;&gt; {
    groupedData: IGroupedTableDataNode;
    columns: string[];
}

function GroupedTable(props: GroupedTableProps): React.ReactElement {
    const {groupedData, columns} = props;

    const tableCells: TableRows = groupedDataToTableRows(groupedData);

    return (
        &lt;table {...props}&gt;
            &lt;thead&gt;
            &lt;tr&gt;
                {
                    columns.map(column =&gt; (
                        &lt;th key={column}&gt;{column}&lt;/th&gt;
                    ))
                }
            &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
            {
                tableCells.map((row, index) =&gt; (
                    &lt;tr key={index}&gt;
                        {
                            row.map((d, index) =&gt; (
                                &lt;td key={index}
                                    rowSpan={d.rowSpan}&gt;
                                    {d.value}
                                &lt;/td&gt;
                            ))
                        }
                    &lt;/tr&gt;
                ))
            }
            &lt;/tbody&gt;
        &lt;/table&gt;
    );
}

export default GroupedTable;

function groupedDataToTableRows(groupedData: IGroupedTableDataNode, initTableRow: TableRow = []): TableRows {
    // TODO 그루핑 데이터를 테이블 행들로 변경하는 함수
      // 아래에 구현 설명이 있습니다.
}</code></pre>
<h3 id="그루핑-데이터를-테이블-view-데이터로-변환">그루핑 데이터를 테이블 view 데이터로 변환</h3>
<p>테이블에 보여져야하는 데이터를 첫번째 행부터 순차적으로 만들면 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/dd3c72e6-2406-4f91-b2fd-52158ccacfa4/image.png" alt=""></p>
<pre><code class="language-typescript">// 첫번째 행
const data = [
  [
    {value: &#39;의류&#39;, rowSpan: 2},
    {value: &#39;후드&#39;, rowSpan: 1},
    {value: &#39;사이즈=M&#39;, rowSpan: 1}
  ]
]</code></pre>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/c3ba0118-d030-47d8-9435-9e849ec58ac0/image.png" alt=""></p>
<pre><code class="language-typescript">// 두번째 행
const data = [
  [
    {value: &#39;의류&#39;, rowSpan: 2},
    {value: &#39;후드&#39;, rowSpan: 1},
    {value: &#39;사이즈=M&#39;, rowSpan: 1}
  ],
  [
    {value: &#39;셔츠&#39;, rowSpan: 1},
    {value: &#39;사이즈=L&#39;, rowSpan: 1}
  ]
]</code></pre>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/b7617aa3-9ef0-4ee6-a8de-a827a33a3dba/image.png" alt=""></p>
<pre><code class="language-typescript">// 세번째 행
const data = [
  [
    {value: &#39;의류&#39;, rowSpan: 2},
    {value: &#39;후드&#39;, rowSpan: 1},
    {value: &#39;사이즈=M&#39;, rowSpan: 1}
  ],
  [
    {value: &#39;셔츠&#39;, rowSpan: 1},
    {value: &#39;사이즈=L&#39;, rowSpan: 1}
  ],
  [
    {value: &#39;음식&#39;, rowSpan: 5},
    {value: &#39;감바스&#39;, rowSpan: 2},
    {value: &#39;팩=4팩&#39;, rowSpan: 1}
  ],
]</code></pre>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/1a7fc1b1-9de0-4832-b727-02d377b3c8f0/image.png" alt=""></p>
<pre><code class="language-typescript">// 네번째 행
const data = [
  [
    {value: &#39;의류&#39;, rowSpan: 2},
    {value: &#39;후드&#39;, rowSpan: 1},
    {value: &#39;사이즈=M&#39;, rowSpan: 1}
  ],
  [
    {value: &#39;셔츠&#39;, rowSpan: 1},
    {value: &#39;사이즈=L&#39;, rowSpan: 1}
  ],
  [
    {value: &#39;음식&#39;, rowSpan: 5},
    {value: &#39;감바스&#39;, rowSpan: 2},
    {value: &#39;팩=4팩&#39;, rowSpan: 1}
  ],
  [
    {value: &#39;팩=2팩&#39;, rowSpan: 1}
  ],
]</code></pre>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/e7e8d934-7c01-4739-af43-6b7dc1461607/image.png" alt=""></p>
<pre><code class="language-typescript">// 네번째 행
const data = [
  [
    {value: &#39;의류&#39;, rowSpan: 2},
    {value: &#39;후드&#39;, rowSpan: 1},
    {value: &#39;사이즈=M&#39;, rowSpan: 1}
  ],
  [
    {value: &#39;셔츠&#39;, rowSpan: 1},
    {value: &#39;사이즈=L&#39;, rowSpan: 1}
  ],
  [
    {value: &#39;음식&#39;, rowSpan: 5},
    {value: &#39;감바스&#39;, rowSpan: 2},
    {value: &#39;팩=4팩&#39;, rowSpan: 1}
  ],
  [
    {value: &#39;팩=2팩&#39;, rowSpan: 1}
  ],
  [
    {value: &#39;닭강정&#39;, rowSpan: 3},
    {value: &#39;양념=없음&#39;, rowSpan: 1}
  ],
]</code></pre>
<p>위의 데이터의 형태로 바뀌어야 <code>GroupedTable</code>컴포넌트에서 데이터를 의도한대로 나타낼 수 있습니다.</p>
<p>그루핑 데이터를 위의 코드처럼 view를 위한 데이터로 변환해주는 <code>groupedDataToTableRows</code> 함수입니다.</p>
<pre><code class="language-typescript">// table/GroupedTable

function groupedDataToTableRows(groupedData: IGroupedTableDataNode, initTableRow: TableRow = []): TableRows {
    // 그루핑 데이터를 테이블 행들로 변경하는 함수
    // 재귀 함수

    const result: TableRows = [];

    if (groupedData.values.length === 0) {
        // 하위 노드가 없다면 재귀 함수 탈출
        return [initTableRow];
    }

    for (let i = 0; i &lt; groupedData.values.length; i++) {
        const val = groupedData.values[i];

        if (i === 0) {
            // 첫번째라면 initTableRow를 포함해서 행 생성
            const childNode = groupedData.childrenNodes[val];
            const newRow = groupedDataToTableRows(childNode, initTableRow.concat([{value: val, rowSpan: childNode.countLeafNodes()}]));
            result.push(...newRow);
            continue;
        }

        // 두번째 이상의 value라면 initTableRow (이전 값)들을 제외한 현재 노드와 하위노드 추가
        const childNode = groupedData.childrenNodes[val];
        const newRow = groupedDataToTableRows(childNode, [{value: val, rowSpan: childNode.countLeafNodes()}]);
        result.push(...newRow);
    }

    return result;
}</code></pre>
<br/>

<hr>
<h2 id="😎-결과">😎 결과</h2>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/ba2f0896-c968-40ae-99db-0de5e49508a1/image.png" alt=""></p>
<p>4개의 column으로 데이터를 만들어서 테스트를 하면 다음과 같이 나타납니다.</p>
<pre><code class="language-typescript">const data: TableData = [
    {
        &#39;카테고리&#39;: &#39;상의&#39;,
        &#39;상품&#39;: &#39;후드&#39;,
        &#39;색상&#39;: &#39;블랙&#39;,
        &#39;사이즈&#39;: &#39;L&#39;
    },
    {
        &#39;카테고리&#39;: &#39;상의&#39;,
        &#39;상품&#39;: &#39;셔츠&#39;,
        &#39;색상&#39;: &#39;화이트&#39;,
        &#39;사이즈&#39;: &#39;M&#39;
    },
    {
        &#39;카테고리&#39;: &#39;상의&#39;,
        &#39;상품&#39;: &#39;후드&#39;,
        &#39;색상&#39;: &#39;블랙&#39;,
        &#39;사이즈&#39;: &#39;S&#39;
    },
    {
        &#39;카테고리&#39;: &#39;하의&#39;,
        &#39;상품&#39;: &#39;슬랙스&#39;,
        &#39;색상&#39;: &#39;블랙&#39;,
        &#39;사이즈&#39;: &#39;M&#39;
    },
    {
        &#39;카테고리&#39;: &#39;하의&#39;,
        &#39;상품&#39;: &#39;슬랙스&#39;,
        &#39;색상&#39;: &#39;화이트&#39;,
        &#39;사이즈&#39;: &#39;S&#39;
    },
    {
        &#39;카테고리&#39;: &#39;하의&#39;,
        &#39;상품&#39;: &#39;청바지&#39;,
        &#39;색상&#39;: &#39;블루&#39;,
        &#39;사이즈&#39;: &#39;M&#39;
    },
    {
        &#39;카테고리&#39;: &#39;상의&#39;,
        &#39;상품&#39;: &#39;후드&#39;,
        &#39;색상&#39;: &#39;레드&#39;,
        &#39;사이즈&#39;: &#39;L&#39;
    },
    {
        &#39;카테고리&#39;: &#39;상의&#39;,
        &#39;상품&#39;: &#39;후드&#39;,
        &#39;색상&#39;: &#39;레드&#39;,
        &#39;사이즈&#39;: &#39;M&#39;
    },
    {
        &#39;카테고리&#39;: &#39;하의&#39;,
        &#39;상품&#39;: &#39;슬랙스&#39;,
        &#39;색상&#39;: &#39;블랙&#39;,
        &#39;사이즈&#39;: &#39;L&#39;
    },
];

const columns: string[] = [&#39;카테고리&#39;, &#39;상품&#39;, &#39;색상&#39;, &#39;사이즈&#39;];</code></pre>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/ab3a3d00-6d1d-449b-8aad-0fd93ad860a5/image.png" alt=""></p>
<br/>

<hr>
<h2 id="🤓-인사이트">🤓 인사이트</h2>
<ul>
<li>트리와 DFS를 FE개발하면서 자주 사용하지 않았는데 이번 문제와 같은 상황에서는 배열만으로 구현하는 로직에 비해 쉽게 해결할 수 있었습니다. 상위 요소와 하위 요소간의 관계를 갖는 상황에서 트리를 더 활용해볼 수 있을 것 같습니다.</li>
<li>데이터 간의 변환이 자주 일어났습니다. 변환하는 기능들을 좀 더 쉽게 해줄 모듈(커스텀 훅 또는 라이브러리)가 필요합니다.</li>
<li>핵심로직이 설명없이 보면 이해하기가 힘들었습니다. 동료 또는 미래의 나를 위해서 트리 구조와 DFS 알고리즘처럼 이해가 필요한 코드는 자세한 설명 문서 또는 코멘트가 필요하다는 것을 느꼈습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React, SheetJS] 엑셀 파일을 업로드하고 테이블로 나타내는 방법]]></title>
            <link>https://velog.io/@good-jinu/React-SheetJS-%EC%97%91%EC%85%80-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%98%EA%B3%A0-%ED%85%8C%EC%9D%B4%EB%B8%94%EB%A1%9C-%EB%82%98%ED%83%80%EB%82%B4%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@good-jinu/React-SheetJS-%EC%97%91%EC%85%80-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%98%EA%B3%A0-%ED%85%8C%EC%9D%B4%EB%B8%94%EB%A1%9C-%EB%82%98%ED%83%80%EB%82%B4%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Mon, 22 Jul 2024 02:08:40 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/good-jinu/post/4665ad3a-9805-46c8-b6f3-df3344c8c13b/image.png" alt=""></p>
<p>엑셀 파일을 업로드하고 이를 테이블로 보여줘야하는 요구사항이 생겼습니다.</p>
<h2 id="💻코드">💻코드</h2>
<p>하나의 파일에 해당 기능을 모두 구현한 코드입니다.</p>
<pre><code class="language-tsx">import React, {useState} from &#39;react&#39;;
import * as XLSX from &#39;xlsx&#39;;

const ExcelReader: React.FC = () =&gt; {
  const [data, setData] = useState&lt;Record&lt;string, string&gt;[]&gt;([]);

  const tableHead = Array.from(data.reduce((acc, cur) =&gt; {
    // 키값을 모두 Set 객체에 add
    Object.keys(cur).forEach(k =&gt; acc.add(k));
    return acc;
  }, new Set&lt;string&gt;()));

  const handleFileUpload = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    // 파일을 업로드 했을 때의 처리
    const file = e.target.files?.[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = (evt) =&gt; {
        const bstr = evt.target?.result as string;
        const wb = XLSX.read(bstr, {type: &#39;binary&#39;});
        const wsname = wb.SheetNames[0];
        const ws = wb.Sheets[wsname];
        const data: Record&lt;string, string&gt;[] = XLSX.utils.sheet_to_json(ws);
        setData(data);
      };
      reader.readAsBinaryString(file);
    }
  };

  return (
    &lt;div&gt;
      &lt;input type=&#39;file&#39; onChange={handleFileUpload} /&gt;
      &lt;table&gt;
        &lt;thead&gt;
        &lt;tr&gt;
          {tableHead.map((value) =&gt; (
            &lt;td key={value}&gt;{value}&lt;/td&gt;
          ))}
        &lt;/tr&gt;
        &lt;/thead&gt;
        &lt;tbody&gt;
        {data.map((row, i) =&gt; (
          &lt;tr key={i}&gt;
            {tableHead.map((key) =&gt; (
              &lt;td key={key}&gt;{row[key] ?? &#39;&#39;}&lt;/td&gt;
            ))}
          &lt;/tr&gt;
        ))}
        &lt;/tbody&gt;
      &lt;/table&gt;
    &lt;/div&gt;
  );
};

export default ExcelReader;</code></pre>
<p>이 코드는 엑셀 파일을 업로드하고, 그 내용을 웹 페이지의 테이블로 나타내는 기본적인 기능을 구현한 것입니다.</p>
<p>화면은 다음과 같이 나타납니다.
<img src="https://velog.velcdn.com/images/good-jinu/post/c6279a9c-80db-45ef-9aa8-4a5aa9f6f7d3/image.png" alt="">
[파일 선택전]</p>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/62733401-b7ff-4e19-8d0c-cab9f71ea6c3/image.png" alt="">
[파일 선택 후]</p>
<h2 id="🤓코드-단계적-설명">🤓코드 단계적 설명</h2>
<p>React에서 엑셀 파일을 업로드하고, 그 내용을 웹 페이지의 테이블로 나타내는 작업은 다음과 같은 단계로 구성했습니다.:</p>
<ol>
<li><p><strong>파일 업로드</strong>: 사용자가 엑셀 파일을 웹 페이지에 업로드할 수 있도록 input 태그를 사용하여 파일 업로드 기능을 구현합니다.</p>
</li>
<li><p><strong>파일 읽기</strong>: 업로드된 파일을 읽기 위해 FileReader 객체를 사용합니다. 이 객체는 웹 애플리케이션에서 비동기적으로 데이터를 읽을 수 있게 해줍니다.</p>
</li>
<li><p><strong>엑셀 파싱</strong>: &#39;xlsx&#39; 라이브러리를 사용하여 엑셀 파일을 파싱합니다. 이 라이브러리는 엑셀 파일을 JSON 형식으로 변환해줍니다.</p>
</li>
<li><p><strong>테이블 렌더링</strong>: 파싱된 데이터를 기반으로 React 컴포넌트 내에서 테이블을 렌더링합니다.</p>
</li>
</ol>
<h2 id="🌵코드-분리-및-리팩토링">🌵코드 분리 및 리팩토링</h2>
<p>파일 업로드 컴포넌트와 데이터를 보여주는 테이블 컴포넌트를 나누면 다음과 같이 만들 수 있습니다.</p>
<pre><code class="language-tsx">// FileUpload.tsx
import React from &#39;react&#39;;
import * as XLSX from &#39;xlsx&#39;;

interface FileUploadProps {
  onFileUpload: (data: Record&lt;string, string&gt;[]) =&gt; void;
}

const FileUpload: React.FC&lt;FileUploadProps&gt; = ({onFileUpload}) =&gt; {
  const handleFileUpload = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const file = e.target.files?.[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = (evt) =&gt; {
        const bstr = evt.target?.result as string;
        const wb = XLSX.read(bstr, {type: &#39;binary&#39;});
        const wsname = wb.SheetNames[0];
        const ws = wb.Sheets[wsname];
        const data: Record&lt;string, string&gt;[] = XLSX.utils.sheet_to_json(ws);
        onFileUpload(data);
      };
      reader.readAsBinaryString(file);
    }
  };

  return &lt;input type=&#39;file&#39; onChange={handleFileUpload} /&gt;;
};

export default FileUpload;</code></pre>
<pre><code class="language-tsx">// DataTable.tsx
import React from &#39;react&#39;;

interface DataTableProps {
  data: Record&lt;string, string&gt;[];
}

const DataTable: React.FC&lt;DataTableProps&gt; = ({data}) =&gt; {
  const tableHead = Array.from(data.reduce((acc, cur) =&gt; {
    Object.keys(cur).forEach(k =&gt; acc.add(k));
    return acc;
  }, new Set&lt;string&gt;()));

  return (
    &lt;table&gt;
      &lt;thead&gt;
      &lt;tr&gt;
        {tableHead.map((value) =&gt; (
          &lt;td key={value}&gt;{value}&lt;/td&gt;
        ))}
      &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
      {data.map((row, i) =&gt; (
        &lt;tr key={i}&gt;
          {tableHead.map((key) =&gt; (
            &lt;td key={key}&gt;{row[key] ?? &#39;&#39;}&lt;/td&gt;
          ))}
        &lt;/tr&gt;
      ))}
      &lt;/tbody&gt;
    &lt;/table&gt;
  );
};

export default DataTable;</code></pre>
<pre><code class="language-tsx">// ExcelReader.tsx
import React, {useState} from &#39;react&#39;;
import FileUpload from &#39;./FileUpload&#39;;
import DataTable from &#39;./DataTable&#39;;

const ExcelReader: React.FC = () =&gt; {
  const [data, setData] = useState&lt;Record&lt;string, string&gt;[]&gt;([]);

  return (
    &lt;div&gt;
      &lt;FileUpload onFileUpload={setData} /&gt;
      &lt;DataTable data={data} /&gt;
    &lt;/div&gt;
  );
};

export default ExcelReader;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[코틀린의 자료형]]></title>
            <link>https://velog.io/@good-jinu/%EC%BD%94%ED%8B%80%EB%A6%B0%EC%9D%98-%EC%9E%90%EB%A3%8C%ED%98%95</link>
            <guid>https://velog.io/@good-jinu/%EC%BD%94%ED%8B%80%EB%A6%B0%EC%9D%98-%EC%9E%90%EB%A3%8C%ED%98%95</guid>
            <pubDate>Mon, 01 Jan 2024 06:58:03 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/good-jinu/post/9017256d-9ed2-4406-b0ad-da918d78cdf3/image.jpg" alt=""></p>
<p>코틀린은 자료형에 대해 명시적으로 선언할 수도 있고, 타입 추론을 통해 생략할 수도 있습니다.</p>
<p>코틀린의 자료형은 기본형과 참조형으로 구분할 수 있습니다. 기본형은 정수, 실수, 문자, 불리언 등의 원시 타입을 의미하며, 참조형은 배열, 컬렉션, 클래스, 인터페이스 등의 객체 타입을 의미합니다.</p>
<h2 id="🚄배열-생성-함수">🚄배열 생성 함수</h2>
<ul>
<li><strong>arrayListOf</strong>는 수정 가능한 리스트를 생성하는 함수입니다. 내부적으로는 배열로 데이터를 저장하는 ArrayList 클래스를 사용합니다. arrayListOf 함수는 매개변수로 임의의 개수의 값을 받아서 리스트에 추가합니다. arrayListOf 함수로 생성된 리스트는 <code>add</code>, <code>remove</code>, <code>set</code> 등의 메소드로 요소를 추가하거나 삭제하거나 변경할 수 있습니다.</li>
<li><strong>listOf</strong>는 읽기 전용 리스트를 생성하는 함수입니다. listOf 함수는 매개변수로 임의의 개수의 값을 받아서 리스트에 추가합니다. listOf 함수로 생성된 리스트는 읽기 전용이기 때문에 요소를 추가하거나 삭제하거나 변경할 수 없습니다. 만약 수정 가능한 리스트를 생성하고 싶다면 mutableListOf 함수를 사용할 수 있습니다.</li>
<li><strong>arrayOf</strong>는 배열을 생성하고 초기화하는 함수입니다. arrayOf 함수는 매개변수로 임의의 개수의 값을 받아서 배열에 저장합니다. arrayOf 함수로 생성된 배열은 대괄호를 사용해서 인덱스로 요소에 접근하거나 변경할 수 있습니다. 배열의 크기는 생성 시에 결정되며, 요소를 추가하거나 삭제할 수 없습니다. 만약 배열의 크기가 정해지지 않은 경우에는 <code>arrayOfNulls</code> 함수를 사용할 수 있습니다.</li>
</ul>
<pre><code class="language-kotlin">fun main(args: Array&lt;String&gt;) {
    val arr: ArrayList&lt;Int&gt; = arrayListOf&lt;Int&gt;()
    arr.add(3)
    arr.add(5)
    arr.add(5)
    arr[1] = 4
    println(arr)
//    output: [3, 4, 5]

    val lis: List&lt;Int&gt; = listOf&lt;Int&gt;(244, 222, 111)
//    lis.add X 추가 불가
//    lis.set X 수정 불가
//    lis[2] = 55 X 수정 불가
    println(lis)
//    output: [233, 222, 111]

    val arra: Array&lt;Int&gt; = arrayOf&lt;Int&gt;(11, 11, 22)
//    arra.add 추가 불가
    arra[1] = 4
    println(arra[0])
    println(arra[1])
    println(arra[2])
    println(arra.size)
//    output: 11
//            4
//            22
//            3
}</code></pre>
<p>코틀린에서 MutableList와 ArrayList는 둘 다 수정 가능한 리스트를 생성하는 함수입니다. 하지만 MutableList는 인터페이스이고, ArrayList는 클래스입니다1. MutableList는 ArrayList 외에도 다른 구현체를 가질 수 있습니다. 예를 들어, LinkedList도 MutableList의 구현체입니다2. ArrayList는 내부적으로 배열로 데이터를 저장하고, LinkedList는 노드로 데이터를 저장합니다3. 따라서 두 클래스는 추가, 삭제, 검색 등의 연산에 대한 성능 차이가 있습니다3.</p>
<p>MutableList와 ArrayList의 차이점을 요약하면 다음과 같습니다:</p>
<ul>
<li>MutableList는 인터페이스이고, ArrayList는 클래스입니다.</li>
<li>MutableList는 ArrayList 외에도 다른 구현체를 가질 수 있습니다.</li>
<li>ArrayList는 내부적으로 배열로 데이터를 저장하고, LinkedList는 노드로 데이터를 저장합니다.</li>
<li>ArrayList와 LinkedList는 추가, 삭제, 검색 등의 연산에 대한 성능 차이가 있습니다.</li>
</ul>
<pre><code class="language-kotlin">fun main(args: Array&lt;String&gt;) {
    val arr: ArrayList&lt;Int&gt; = arrayListOf&lt;Int&gt;()
    arr.add(3)
    arr.add(5)
    arr.add(5)
    arr[1] = 4
    println(arr)
//    output: [3, 4, 5]
    val mutLis: MutableList&lt;Int&gt; = mutableListOf&lt;Int&gt;()
    mutLis.add(10)
    mutLis.add(9)
    mutLis.add(8)
    mutLis[1] = 0
    println(mutLis)
//    output: [10, 0, 8]
}</code></pre>
<h2 id="🛎코틀린-컬렉션-인터페이스">🛎코틀린 컬렉션 인터페이스</h2>
<ul>
<li>컬렉션은 <strong>List, Set, Map</strong> 등의 인터페이스로 표현되며, 각각 <strong>순서가 있는 원소의 집합, 중복이 없는 원소의 집합, 키와 값의 쌍의 집합</strong>을 나타냅니다.</li>
<li>컬렉션은 <strong>변경 가능한(mutable) 컬렉션과 변경 불가능한(immutable) 컬렉션</strong>으로 구분할 수 있습니다. 변경 가능한 컬렉션은 원소를 추가하거나 삭제하거나 수정할 수 있으며, 변경 불가능한 컬렉션은 한 번 생성된 후에는 원소를 변경할 수 없습니다.</li>
<li>컬렉션을 생성하는 방법에는 <strong>listOf, setOf, mapOf 등의 함수와 mutableListOf, mutableSetOf, mutableMapOf 등의 함수</strong>가 있습니다. 전자는 변경 불가능한 컬렉션을 생성하고, 후자는 변경 가능한 컬렉션을 생성합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DB] CHARACTER SET utf8mb4로 설정을 해도 한글이 깨질 때]]></title>
            <link>https://velog.io/@good-jinu/DB-CHARACTER-SET-utf8mb4%EB%A1%9C-%EC%84%A4%EC%A0%95%EC%9D%84-%ED%95%B4%EB%8F%84-%ED%95%9C%EA%B8%80%EC%9D%B4-%EA%B9%A8%EC%A7%88-%EB%95%8C</link>
            <guid>https://velog.io/@good-jinu/DB-CHARACTER-SET-utf8mb4%EB%A1%9C-%EC%84%A4%EC%A0%95%EC%9D%84-%ED%95%B4%EB%8F%84-%ED%95%9C%EA%B8%80%EC%9D%B4-%EA%B9%A8%EC%A7%88-%EB%95%8C</guid>
            <pubDate>Mon, 25 Dec 2023 04:55:27 GMT</pubDate>
            <description><![CDATA[<p>원인은 데이터를 insert하기 바로전에 <code>SET Names utf4mb4</code>로 설정해서 insert를 해야하는데 그냥 insert를 해서 한글인 깨졌습니다.</p>
<h2 id="중요">중요</h2>
<p>insert할 때 <code>SET Names utf4mb4</code>가 되어 있도록 하기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[postMessage, message 이벤트] iframe과 소통하기]]></title>
            <link>https://velog.io/@good-jinu/postMessage-message-%EC%9D%B4%EB%B2%A4%ED%8A%B8-iframe%EA%B3%BC-%EC%86%8C%ED%86%B5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@good-jinu/postMessage-message-%EC%9D%B4%EB%B2%A4%ED%8A%B8-iframe%EA%B3%BC-%EC%86%8C%ED%86%B5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 23 Oct 2023 11:36:26 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/good-jinu/post/bf45f1e2-6847-4157-99ec-53d24c5211ad/image.png" alt="from: https://pixabay.com/vectors/email-newsletter-email-marketing-3249062/"></p>
<p>브라우저에서는 iframe과 부모가 도메인이 다르다면 서로의 컨텐츠를 직접 조작할 수 없도록 막아 놨기 때문에 메세지를 통해 iframe과 부모 같의 정보를 교환할 수 있습니다.</p>
<h1 id="✉️-iframe에서-부모로-메세지-보내기">✉️ iframe에서 부모로 메세지 보내기</h1>
<ul>
<li>부모 컨텐츠에서는 메세지 이벤트 리스너를 등록합니다.<pre><code class="language-html">&lt;script&gt;
window.addEventListener(&#39;message&#39;, e =&gt; {
    console.log(e.data);
});
&lt;/script&gt;
</code></pre>
</li>
</ul>
<iframe src="https://someurl.com/"/>
```
- 자식 컨텐츠에는 메세지 보내는 코드를 작성합니다. (postMessage)
```html
<!--url: https://someurl.com/-->
<script>
  window.parent.postMessage('this will show on console log', '*'); // '*' for any domain
</script>
```
부모 컨텐츠에서 자식으로부터 받은 메세지가 콘솔창에 나타나는 것을 확인할 수 있습니다.
`this will show on console log`]]></description>
        </item>
        <item>
            <title><![CDATA[Minified React error #418, #423]]></title>
            <link>https://velog.io/@good-jinu/Minified-React-error-418-423</link>
            <guid>https://velog.io/@good-jinu/Minified-React-error-418-423</guid>
            <pubDate>Sun, 12 Feb 2023 02:59:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/good-jinu/post/7a779050-6eb1-46af-adba-c72305a0136f/image.png" alt=""></p>
<h1 id="문제상황">문제상황</h1>
<p>Next js로 쿠키에 저장된 데이터를 화면에 나타내는 과정에서 위의 에러가 나타났습니다. <a href="https://reactjs.org/docs/error-decoder.html/?invariant=418">리액트 공식 홈페이지</a>에서 확인해보니 초기에 렌더링된 UI가 서버에서 렌더링된 것과 다르다는 내용의 에러였습니다.</p>
<blockquote>
<p>Hydration failed because the initial UI does not match what was rendered on the server.</p>
</blockquote>
<br/>

<h1 id="해결방법">해결방법</h1>
<pre><code class="language-javascript">// 에러가 발생한 코드

import Layout from &#39;@/components/Layout&#39;;
import { getCookie } from &#39;@/utils/cookieIO&#39;;
import { decodeJWT } from &#39;@/utils/jwt&#39;;

export default function Home() {
  return (
    &lt;&gt;
      &lt;Layout&gt;
        &lt;h1&gt;This is Body {decodeJWT(getCookie(&#39;access_token&#39;)).sub}&lt;/h1&gt;
      &lt;/Layout&gt;
    &lt;/&gt;
  )
}
</code></pre>
<p>위의 코드에서 초기의 UI가 SSR과 매칭되도록 하고 쿠키의 데이터를 화면에 출력하는 것은 useEffect를 한번 거쳐서 렌더링되도록 하였습니다.</p>
<pre><code class="language-javascript">// 수정 후

import React from &#39;react&#39;;
import Layout from &#39;@/components/Layout&#39;;
import { getCookie } from &#39;@/utils/cookieIO&#39;;
import { decodeJWT } from &#39;@/utils/jwt&#39;;

export default function Home() {
  const [id, setId] = React.useState&lt;string&gt;(&#39;&#39;);

  React.useEffect(() =&gt; {
    if(getCookie(&#39;access_token&#39;)) {
      setId(decodeJWT(getCookie(&#39;access_token&#39;)).sub);
    }
  }, [])
  return (
    &lt;&gt;
      &lt;Layout&gt;
        &lt;h1&gt;This is Body {id}&lt;/h1&gt;
      &lt;/Layout&gt;
    &lt;/&gt;
  )
}</code></pre>
<br/>

<ul>
<li>출력화면</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/7e3c3d01-e1ea-4f2a-8e18-944ba94dc76c/image.png" alt=""></p>
<h1 id="참조">참조</h1>
<p><a href="https://github.com/vercel/next.js/discussions/39425">https://github.com/vercel/next.js/discussions/39425</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVP란 무엇인가? (번역, 정리)]]></title>
            <link>https://velog.io/@good-jinu/MVP%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-%EB%B2%88%EC%97%AD-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@good-jinu/MVP%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-%EB%B2%88%EC%97%AD-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 25 Jun 2022 05:19:48 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/good-jinu/post/b551a89a-1017-4b4a-b8ce-be86b472607a/image.png" alt=""></p>
<blockquote>
<p>원문: <a href="https://www.productplan.com/glossary/minimum-viable-product/">https://www.productplan.com/glossary/minimum-viable-product/</a></p>
</blockquote>
<p>MVP(Minumum Viable Product)는 최소한의 기능으로 만들어진 제품입니다. MVP는 제품 제작팀이 사용자로부터 빠르게 피드백을 받고 빠르게 개선해나갈 수 있도록 해줍니다.</p>
<br/>

<h1 id="🤔-mvp의-목적">🤔 MVP의 목적</h1>
<p>MVP는 최소한의 노력으로 시장의 반응을 알 수 있다는데 의의가 있습니다. 따라서 다음과 같은 상황에서 MVP를 개발하고 출시할 수 있습니다:</p>
<ul>
<li>시장에 최대한 빠른 속도로 제품을 선보이고 싶을 때</li>
<li>제품 개발에 큰 돈을 지출하기 전에 시장의 반응을 알고 싶을 때</li>
<li>제품의 어떤 점이 좋은 반응을 보이고 어떤 점이 그렇지 않은지 알고 싶을 때</li>
</ul>
<p>이러한 점 덕분에 MVP는 성공하지 못할 제품에 많은 투자를 하게 되는 것을 방지할 수 있습니다.</p>
<br/>
<br/>

<h1 id="🤔-mvp는-어떻게-만드는가">🤔 MVP는 어떻게 만드는가?</h1>
<h2 id="1-비즈니스-목적에-맞도록-계획하라">1. 비즈니스 목적에 맞도록 계획하라</h2>
<p>어떤 기능을 중점적으로 만들지 얘기하기 전에, 팀 또는 회사의 목적에 맞는 제품을 개발하도록 해야합니다.</p>
<p>예를 들어 새로운 시장을 공략하려는 목적을 가지고 있다면 그에 맞는 MVP를 디자인해야하고, 현재 제품의 타겟 시장을 더 강화하려는 목적을 가지고 있다면 기존 사용자들을 위한 새 기능을 고려해야할 것입니다.</p>
<h2 id="2-고객를-위해-해결하거나-개선해야할-문제를-파악하라">2. 고객를 위해 해결하거나 개선해야할 문제를 파악하라</h2>
<p>비즈니스 목적을 파악했으므로 사용자를 위해서 해결해야할 문제에 대한 솔루션을 생각해야 합니다. 여기서 솔루션이란, 절대로 제품의 전반적인 비전을 담으면 안되고 그 비전의 부분만 반영해야합니다. 왜냐하면 MVP를 개발할 때는 적은 양의 기능들만 개발해야 하기 때문입니다.</p>
<p>MVP에 포함할 기능을 전략적으로 선택할 때는 다음 사항을 고려할 수 있습니다:</p>
<ul>
<li>시장 조사</li>
<li>경쟁 분석</li>
<li>피드백을 받았을 때 얼마나 빨리 특정 타입의 기능을 반복할 수 있는지</li>
<li>다양한 사용자 스토리, 에픽을 구현하기 위해서 비용이 얼마나 드는지</li>
</ul>
<h2 id="3-mvp기능을-기반으로-개발-계획을-세워라">3. MVP기능을 기반으로 개발 계획을 세워라</h2>
<p>MVP를 위해 중요한 전략적 요소, 기능들을 설정했다면 이를 구현하기 위한 개발 계획을 세울 차례입니다.</p>
<blockquote>
<p>Note: MVP의 V는 Viable, 즉, 실행 가능 또는 사용가능한 형태라는 것을 반드시 기억해야 합니다. 반쯤 만들어진 제품이 아닌 최소한의 기능을 갖춘 사용가능한 완제품이라는 것을 명심하고 개발을 하여야 합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[맛집 리뷰 감성 분류기 모듈 만들기]]></title>
            <link>https://velog.io/@good-jinu/%EB%A7%9B%EC%A7%91-%EB%A6%AC%EB%B7%B0-%EA%B0%90%EC%84%B1-%EB%B6%84%EB%A5%98%EA%B8%B0-%EB%AA%A8%EB%93%88-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@good-jinu/%EB%A7%9B%EC%A7%91-%EB%A6%AC%EB%B7%B0-%EA%B0%90%EC%84%B1-%EB%B6%84%EB%A5%98%EA%B8%B0-%EB%AA%A8%EB%93%88-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 06 Jun 2022 08:48:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/good-jinu/post/8d532fe9-03ae-4705-afb3-8888b1342fd1/image.jpg" alt=""></p>
<p>맛집 리뷰 감성 분류기를 다른 곳에서 사용할 수 있도록 모듈의 형태로 만들었습니다.</p>
<ol>
<li>리뷰 데이터와 긍정적 리뷰인지 부정적인지에 대한 데이터를 인터넷에서 수집합니다.</li>
<li>한국어 형태소 분석기 라이브러리로 글을 적절히 전처리하고 이를 정수 인코딩하고 정규화를 합니다.</li>
<li>keras 라이브러리를 이용해서 학습을 하고 모델을 만들어 냅니다.</li>
<li>모델 평가와 튜닝을 끝내고 나면 재사용, 확장이 가능하도록 모듈로 만듭니다.</li>
</ol>
<h1 id="🥚-데이터-수집">🥚 데이터 수집</h1>
<p>맛집 리뷰는 selenium로 크롤링해서 데이터를 얻었습니다.</p>
<ol>
<li>맛집 사이트에 각 맛집들에 대한 리스트가 나열되어 있는 페이지로 접속합니다.</li>
<li>리스트에 있는 각 맛집들의 링크를 받아와서 리스트의 형태로 저장합니다.</li>
<li>받아온 링크 리스트에서 하나씩 접속해서 해당 리뷰 페이지에 있는 리뷰 텍스트들을 수집합니다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/18acdfcf-4e99-4bef-a2ad-0c201dc9b215/image.JPG" alt=""></p>
<p>받아온 데이터입니다.</p>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>review</th>
      <th>label</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>순대국밥에 진심인 내가, 감히 최고라고 추천하고픈 곳\n\n1. 사실 국밥 분야는 ...</td>
      <td>1</td>
    </tr>
    <tr>
      <th>1</th>
      <td>수육과 깍두기가 맛있는 국밥집\n\n국밥정식을 주문했다\n\n놀랍게도 이집을 대치동...</td>
      <td>1</td>
    </tr>
    <tr>
      <th>2</th>
      <td>선릉-역삼을 혼자 가야만 하는 이유. 둘 이상 가면 무한대기를 해야함. 어차피 순대...</td>
      <td>1</td>
    </tr>
    <tr>
      <th>3</th>
      <td>평일 오후 6시에도 웨이팅이 있는, 순대국의 전통강자. 강남점도 가봤는데 여기가 훨...</td>
      <td>1</td>
    </tr>
    <tr>
      <th>4</th>
      <td>한국인의 소울푸드, 순대국밥의 맛을 가장 잘 나타내는 곳이 바로 농민백암순대 본점이...</td>
      <td>1</td>
    </tr>
    <tr>
      <th>...</th>
      <td>...</td>
      <td>...</td>
    </tr>
    <tr>
      <th>6389</th>
      <td>널찍한 실내와 편안한 의자와 분위기\n\n코지한 분위기는 아니지만 힙하고 큼직하다\...</td>
      <td>1</td>
    </tr>
    <tr>
      <th>6390</th>
      <td>초코초코한데 비쥬얼 만큼 달지않아요.</td>
      <td>1</td>
    </tr>
    <tr>
      <th>6391</th>
      <td>분위기가 힙스럽다. 층고는 한껏 높고, 앞에는 캠핑 의자가 놓여있다. 포틀랜드의 코...</td>
      <td>1</td>
    </tr>
    <tr>
      <th>6392</th>
      <td>기존 위치에서 스투시 2층으로 이전하면서 컨셉을 싹 바꿨네요.. 기존의 포틀랜드 감...</td>
      <td>0</td>
    </tr>
    <tr>
      <th>6393</th>
      <td>진심으로 평점이 왜 이렇게 높은지 모르겠음. 위치도 별로 좋은 거 같지도 않고 커피...</td>
      <td>0</td>
    </tr>
  </tbody>
</table>
<p>6394 rows × 2 columns</p>

<p>label이 0인 데이터는 부정적인 리뷰이고 1인 데이터는 긍정적인 데이터입니다.</p>
<p>중복되는 데이터와 리뷰가 1글자 밖에 안되는 의미없는 데이터들을 삭제합니다.</p>
<pre><code class="language-python">data = data.drop_duplicates(subset=[&#39;review&#39;])
df = data.drop(data[data[&#39;review&#39;].map(lambda x: len(x)&lt;2)].index)
df.shape

# output:
# (5392, 2)</code></pre>
<p>마지막으로 csv 파일로 저장합니다.</p>
<pre><code class="language-python">df.to_csv(&#39;review.csv&#39;, index=False)</code></pre>
<br/>
<br/>

<h1 id="🍳-데이터-정제하기">🍳 데이터 정제하기</h1>
<p>데이터를 학습하기 좋은 형태로 만들어 주어야 합니다. 한국어 형태소 분석, 토큰화, 정수 인코딩, 패딩 등을 해서 학습할 수 있는 형태로 만들어보겠습니다.</p>
<p>사용한 라이브러리입니다.</p>
<pre><code class="language-python">import pandas as pd
import numpy as np
import re
from konlpy.tag import Okt
from tqdm.notebook import tqdm
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences</code></pre>
<p>데이터를 불러와서 확인해보겠습니다.</p>
<pre><code class="language-python">df = pd.read_csv(&#39;review.csv&#39;)
df[&#39;review&#39;][666]


#output:
#&#39;정말정말 가고싶었던 세시셀라!! 판교점을 가게 될 줄은 몰랐는데 ㅠㅠ 
#결과는 실망입니다 머그포래빗 당근케익이 차라리 맛있었던 거 같아요\n
#음료로 시킨 애플레몬티는 맛있었어요&#39;</code></pre>
<br/>

<h2 id="👨🍳-한국어-전처리">👨‍🍳 한국어 전처리</h2>
<p>먼저 한국어를 제외한 영어, 숫자, 특수문자 등을 제거하고 한글과 공백만 남깁니다.</p>
<pre><code class="language-python"># 한글 제외한 문자 삭제
df[&#39;review&#39;] = df.review.map(lambda x: re.sub(&#39;[^ㄱ-ㅎㅏ-ㅣ가-힣]&#39;, &#39; &#39;, x))
df[&#39;review&#39;] = df.review.map(lambda x: re.sub(&#39;\s{2,}&#39;, &#39; &#39;, x))
df[&#39;review&#39;][666]


#output:
#&#39;정말정말 가고싶었던 세시셀라 판교점을 가게 될 줄은 몰랐는데 ㅠㅠ 
#결과는 실망입니다 머그포래빗 당근케익이 차라리 맛있었던 거 같아요 
#음료로 시킨 애플레몬티는 맛있었어요&#39;</code></pre>
<p>출력 결과를 확인해보면 위의 데이터에서는 <code>\n</code>가 제거되었습니다.</p>
<p>한국어만 남았으므로 형태소를 추출할 차례입니다. 저는 오픈 소스 한국어 분석기(과거 트위터 형태소 분석기)를 사용해서 형태소를 추출했습니다.</p>
<pre><code class="language-python">tagger = Okt()
X_data = []

#불용어 사전
s_w = set([&#39;은&#39;, &#39;는&#39;, &#39;이&#39;, &#39;가&#39;, &#39;를&#39;, &#39;들&#39;, &#39;에게&#39;, &#39;의&#39;, &#39;을&#39;, &#39;도&#39;, &#39;으로&#39;, &#39;만&#39;, &#39;라서&#39;, &#39;하다&#39;,
           &#39;아&#39;, &#39;로&#39;, &#39;저&#39;, &#39;즉&#39;, &#39;곧&#39;, &#39;제&#39;, &#39;좀&#39;, &#39;참&#39;, &#39;응&#39;, &#39;그&#39;, &#39;딱&#39;, &#39;어&#39;, &#39;네&#39;, &#39;예&#39;, &#39;게&#39;, &#39;고&#39;,
          &#39;하&#39;, &#39;에&#39;, &#39;한&#39;, &#39;어요&#39;, &#39;것&#39;, &#39;았&#39;, &#39;네요&#39;, &#39;듯&#39;, &#39;같&#39;, &#39;나&#39;, &#39;있&#39;, &#39;었&#39;, &#39;지&#39;, &#39;하고&#39;, &#39;먹다&#39;,
          &#39;습니다&#39;, &#39;기&#39;, &#39;시&#39;, &#39;과&#39;, &#39;수&#39;, &#39;먹&#39;, &#39;와&#39;, &#39;적&#39;, &#39;보&#39;, &#39;에서&#39;, &#39;곳&#39;, &#39;너무&#39;, &#39;정말&#39;, &#39;진짜&#39;,
          &#39;있다&#39;, &#39;다&#39;, &#39;더&#39;, &#39;인&#39;, &#39;집&#39;, &#39;면&#39;, &#39;내&#39;, &#39;라&#39;, &#39;원&#39;, &#39;요&#39;, &#39;또&#39;, &#39;하나&#39;, &#39;전&#39;, &#39;거&#39;, &#39;엔&#39;,
          &#39;이다&#39;, &#39;되다&#39;, &#39;까지&#39;, &#39;인데&#39;, &#39;정도&#39;, &#39;나오다&#39;, &#39;주문&#39;, &#39;시키다&#39;])

for i in tqdm(df[&#39;review&#39;]):
    tk_d = tagger.morphs(i, stem=True)  # clean_X의 형태소 추출
    tk_d = [w for w in tk_d if w not in s_w] # 불용어 제거
    X_data.append(&#39; &#39;.join(tk_d)) # 공백을 기준으로 문자열로 조인
X_data[666]

#output:
#&#39;가다 세시 셀라 판교 점 가게 줄 모르다 ㅠㅠ 결과 실망 머그 포 래빗 당 근 
#케익 차라리 맛있다 같다 음료 애플 레몬 티 맛있다&#39;</code></pre>
<br/>

<h2 id="👨🍳-토큰화와-정규화">👨‍🍳 토큰화와 정규화</h2>
<p>토큰화를 하기 Tokenizer의 fit_on_texts 메서드를 사용하겠습니다.</p>
<pre><code class="language-python">tk = Tokenizer()
tk.fit_on_texts(X_data)</code></pre>
<p>등장 빈도 수가 낮은 형태소들은 제거하기 위해서 4번 이상 등장한 단어의 개수를 구합니다.</p>
<pre><code class="language-python">lesswordlen = 0
for i in tk.word_counts:
    if tk.word_counts[i] &gt; 4:
        lesswordlen += 1
lesswordlen

#output:
#6639</code></pre>
<p>tokenizer로 텍스트를 정수 인코딩한 시퀀스 형태로 만듭니다. 그리고 시퀀스의 길이에 대한 정보를 출력해서 확인합니다.</p>
<pre><code class="language-python">tk.num_words = lesswordlen
trf_x = tk.texts_to_sequences(X_data)
pd.Series(map(lambda x: len(x), trf_x)).describe() # 시퀀스 길이 측정

#output:
#count    5392.000000
#mean       69.963279
#std        82.375268
#min         0.000000
#25%        22.000000
#50%        46.000000
#75%        90.000000
#max      1947.000000
#dtype: float64</code></pre>
<p>75%에 해당하는 시퀀스의 길이인 90을 패딩 길이로 설정해서 패딩합니다.</p>
<pre><code class="language-python">padded = pad_sequences(trf_x, maxlen=90)
padded.shape

#output:
#(5392, 90)</code></pre>
<p>토큰화를 진행하면서 모든 형태소 정보가 사라져서 시퀀스길이가 0인 데이터들은 제거해주어야 합니다. 따라서 시퀀스 길이가 0인 데이터를 탐색해서 제거한 후에 X 데이터를 만들어냅니다.</p>
<pre><code class="language-python"># 길이가 0이상인 인 시퀀스 탐색
zero_length = np.array(list(map(lambda x: len(x)&gt;0, trf_x)))

#길이가 0이상인 데이터만 X_data에 저장
X_data = padded[zero_length]
X_data.shape

#output:
#(5361, 90)</code></pre>
<p>31개의 데이터가 제거되었습니다. y 데이터도 똑같은 작업을 해줍니다.</p>
<pre><code class="language-python">y_data = df[&#39;label&#39;].to_numpy()[zero_length]</code></pre>
<br/>
<br/>

<h1 id="🍽️-데이터-나누기">🍽️ 데이터 나누기</h1>
<p>학습 데이터와 테스트 데이터를 나누는건 사이킷런의 <code>train_test_split</code> 함수를 사용했습니다.</p>
<pre><code class="language-python">from sklearn.model_selection import train_test_split</code></pre>
<p>여기서 주의해야할 점이 있습니다. 요식업 리뷰는 대부분 긍정적인 리뷰가 많습니다. 그래서 지금 사용하는 데이터도 확인해보면 긍정리뷰가 더 많다는것을 볼 수 있습니다.</p>
<pre><code class="language-python">df[&#39;label&#39;].groupby(df[&#39;label&#39;]).size()

#output:
#label
#0    1361
#1    4031
#Name: label, dtype: int64</code></pre>
<p>긍정 데이터가 3배 이상입니다. 따라서 학습 데이터와 테스트 데이터를 나눌 때 더 큰 편향이 발생하지 않도록 stratify 파라미터에 y 데이터를 넘겨줍니다.</p>
<pre><code class="language-python">X_train, X_test, y_train, y_test = train_test_split(X_data, y_data, test_size=0.2, shuffle=True, random_state=4, stratify=y_data)
X_train.shape, X_test.shape

#output:
#((4288, 90), (1073, 90))</code></pre>
<br/>
<br/>

<h1 id="🎓-학습하기">🎓 학습하기</h1>
<p>케라스로 학습모델을 만듭니다.</p>
<pre><code class="language-python">from tensorflow.keras.models import Sequential
from tensorflow.keras import layers
from tensorflow.keras.callbacks import EarlyStopping</code></pre>
<p>양방향 LSTM을 사용해서 학습모델을 만들었습니다.</p>
<pre><code class="language-python">model = Sequential()
model.add(layers.Embedding(lesswordlen + 1, 55, input_length=90))
model.add(layers.Bidirectional(layers.LSTM(64, dropout=0.25)))
model.add(layers.Dense(1, activation=&#39;sigmoid&#39;))</code></pre>
<p>평가지표 <code>metrics</code>는 이진 분류 평가지표인 AUC를 사용하겠습니다.</p>
<pre><code class="language-python">model.compile(optimizer=&#39;adam&#39;, loss=&#39;binary_crossentropy&#39;, metrics=[&#39;AUC&#39;])</code></pre>
<p>AUC 값이 가장 높은 모델을 선택하고 조기종료 콜백함수를 파라미터로 주고 학습을 진행합니다.</p>
<pre><code class="language-python">e_st = EarlyStopping(monitor=&quot;val_auc&quot;, patience=2, restore_best_weights=True, mode=&#39;max&#39;)
model.fit(X_train, y_train, epochs=20, validation_split=0.2, batch_size=256, callbacks=[e_st])</code></pre>
<p>테스트 데이터로 모델을 평가해보았습니다.</p>
<pre><code class="language-python">model.evaluate(X_test, y_test)

#output:
#34/34 [==============================] - 0s 14ms/step - loss: 0.2556 -
#auc: 0.9450</code></pre>
<br/>
<br/>

<h1 id="🧊-모델-파일로-저장하기">🧊 모델 파일로 저장하기</h1>
<p>전처리를 할 때 필요한 토크나이저와 예측모델을 저장합니다.</p>
<pre><code class="language-python">import pickle

with open(&#39;tokenizer.pickle&#39;, &#39;wb&#39;) as handle:
    pickle.dump(tk, handle, protocol=pickle.HIGHEST_PROTOCOL)

model.save(&#39;review.h5&#39;)</code></pre>
<br/>
<br/>


<h1 id="🍎-리뷰-감성-분류기-모듈-만들기">🍎 리뷰 감성 분류기 모듈 만들기</h1>
<p><code>restaurant_review_classifier.py</code>파일에 분류기를 클래스로 구현합니다.</p>
<p>먼저 필요한 라이브러리들을 임포트합니다.</p>
<pre><code class="language-python">import numpy as np
import re
import pickle
from konlpy.tag import Okt
import tensorflow as tf
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences</code></pre>
<p>객체 생성자는 모델과 토크나이저를 파일로부터 읽고 predict 메서드에서는 전처리를 한뒤에 예측 모델로 예측을 한 결과를 출력합니다.</p>
<pre><code class="language-python">class RestarantReviewClassifier:
    def __init__(self):
        # 토크나이저와 예측 모델 불러오기
        with open(&#39;tokenizer.pickle&#39;, &#39;rb&#39;) as handle:
            self.tokenizer = pickle.load(handle)
        self.model = tf.keras.models.load_model(&#39;review.h5&#39;)
        self.tagger = Okt()

    def predict(self, input_data):
        # 한국어 전처리
        input_data = list(map(lambda x: re.sub(&#39;[^ㄱ-ㅎㅏ-ㅣ가-힣]&#39;, &#39; &#39;, x), input_data))  # 한글 제외한 문자 삭제
        input_data = list(map(lambda x: re.sub(&#39;\s{2,}&#39;, &#39; &#39;, x), input_data))

        X_data = []
        # 불용어 사전
        s_w = set([&#39;은&#39;, &#39;는&#39;, &#39;이&#39;, &#39;가&#39;, &#39;를&#39;, &#39;들&#39;, &#39;에게&#39;, &#39;의&#39;, &#39;을&#39;, &#39;도&#39;, &#39;으로&#39;, &#39;만&#39;, &#39;라서&#39;, &#39;하다&#39;,
                &#39;아&#39;, &#39;로&#39;, &#39;저&#39;, &#39;즉&#39;, &#39;곧&#39;, &#39;제&#39;, &#39;좀&#39;, &#39;참&#39;, &#39;응&#39;, &#39;그&#39;, &#39;딱&#39;, &#39;어&#39;, &#39;네&#39;, &#39;예&#39;, &#39;게&#39;, &#39;고&#39;,
                &#39;하&#39;, &#39;에&#39;, &#39;한&#39;, &#39;어요&#39;, &#39;것&#39;, &#39;았&#39;, &#39;네요&#39;, &#39;듯&#39;, &#39;같&#39;, &#39;나&#39;, &#39;있&#39;, &#39;었&#39;, &#39;지&#39;, &#39;하고&#39;, &#39;먹다&#39;,
                &#39;습니다&#39;, &#39;기&#39;, &#39;시&#39;, &#39;과&#39;, &#39;수&#39;, &#39;먹&#39;, &#39;와&#39;, &#39;적&#39;, &#39;보&#39;, &#39;에서&#39;, &#39;곳&#39;, &#39;너무&#39;, &#39;정말&#39;, &#39;진짜&#39;,
                &#39;있다&#39;, &#39;다&#39;, &#39;더&#39;, &#39;인&#39;, &#39;집&#39;, &#39;면&#39;, &#39;내&#39;, &#39;라&#39;, &#39;원&#39;, &#39;요&#39;, &#39;또&#39;, &#39;하나&#39;, &#39;전&#39;, &#39;거&#39;, &#39;엔&#39;,
                &#39;이다&#39;, &#39;되다&#39;, &#39;까지&#39;, &#39;인데&#39;, &#39;정도&#39;, &#39;나오다&#39;, &#39;주문&#39;, &#39;시키다&#39;])
        for i in input_data:
            tk_d = self.tagger.morphs(i, stem=True)  # clean_X의 형태소 추출
            tk_d = [w for w in tk_d if w not in s_w] # 불용어 제거
            X_data.append(&#39; &#39;.join(tk_d))

        #시퀀스로 변환과 패딩작업
        X = self.tokenizer.texts_to_sequences(X_data)
        X = pad_sequences(X, maxlen=90)
        X = np.array(X)

        # 예측 결과를 반올림해서 출력
        return np.array(list(map(lambda x: np.round(x, 0), self.model.predict(X))))</code></pre>
<p>예측결과를 반올림해서 출력하는데 그 이유는 0.5 기준으로 높으면 긍정리뷰<code>1</code>이고 낮으면 부정리뷰<code>0</code>로 출력했기 때문입니다. threshold 파라미터를 추가해서 임계값을 설정하도로 코드를 수정할 수도 있지만 여기서는 하지 않았습니다.</p>
<br/>
<br/>

<h1 id="😃-모듈-사용-테스트">😃 모듈 사용 테스트</h1>
<pre><code class="language-python">from restaurant_review_classifier import RestarantReviewClassifier

x_data = [&#39;가면 갈수록 기분나빠지는 곳. 절대로, 두번 다시 안간다.&#39;,
            &#39;고기 맛은 최고!! 다만 정신놓고 먹다보면 지갑이 텅텅!&#39;,
            &#39;맛집이라고 해서 가봤는데  별로네요&#39;,
            &#39;가격에 비해 낙곱새에 낙지가 너무 적어요.&#39;]
clf = RestarantReviewClassifier()
clf.predict(x_data)

#output:
#array([[0.],
#       [1.],
#       [0.],
#       [0.]], dtype=float32)</code></pre>
<p>두번 째 입력값은 긍정리뷰, 나머지는 부정리뷰로 분류한 결과를 볼 수 있습니다. 결과는 <a href="https://github.com/good-jinu/NL-classifiers">깃헙 레포지토리</a>에 저장했습니다.</p>
<br/>
<br/>


<hr>
<h1 id="reference">Reference</h1>
<p>한국어 자연어 처리, 감성 분류에 대한 깊은 정보를 얻고 싶다면 다음 논문들을 참고하면 좋습니다.</p>
<ul>
<li><a href="http://www.riss.kr/search/detail/DetailView.do?p_mat_type=be54d9b8bc7cdb09&amp;control_no=24acf1d1caf37943ffe0bdc3ef48d419&amp;outLink=K">효과적인 감성분류를 위한 특징생성 및 분류기 설계 = Feature Generation and Classifier Design for Effective Sentiment Analysis</a></li>
<li><a href="https://scienceon.kisti.re.kr/srch/selectPORSrchArticle.do?cn=JAKO201006755720589&amp;SITE=CLICK">한국어 특성을 고려한 감성 분류 Sentiment Classification considering Korean Features</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[flask, react, yolo] 이미지를 보고 사람 수를 세어주는 웹 앱 프로토타입]]></title>
            <link>https://velog.io/@good-jinu/flask-react-yolo-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EB%B3%B4%EA%B3%A0-%EC%82%AC%EB%9E%8C-%EC%88%98%EB%A5%BC-%EC%84%B8%EC%96%B4%EC%A3%BC%EB%8A%94-%EC%9B%B9-%EC%95%B1-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@good-jinu/flask-react-yolo-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EB%B3%B4%EA%B3%A0-%EC%82%AC%EB%9E%8C-%EC%88%98%EB%A5%BC-%EC%84%B8%EC%96%B4%EC%A3%BC%EB%8A%94-%EC%9B%B9-%EC%95%B1-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85</guid>
            <pubDate>Sun, 05 Jun 2022 10:58:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/good-jinu/post/33f8e1f3-174f-428d-9763-0d7d913e5c6f/image.jpg" alt=""></p>
<p>현재 <code>혼잡도 검색 시스템</code> 프로젝트 진행 초기상태라서 시스템이 기획한대로 구현이 가능한지 확인할 필요가 있었습니다. 그래서 아주 핵심적인 기능인 서버가 이미지를 받아서 사람 수를 카운팅 해서 결과값을 클라이언트에게 보여주는 플로우가 가능한지, 문제점은 없는지 등을 보기 위한 아주 작은 프로토타입을 구현해보았습니다.</p>
<p><a href="https://github.com/good-jinu/capstone-design">github repository</a></p>
<br/>

<h1 id="🚶-시스템-플로우">🚶 시스템 플로우</h1>
<p><code>혼잡도 검색 시스템</code>은 관광지, 주차장, 카페 등과 같이 많은 사람들이 모이는 곳에 혼잡도를 카메라로 측정해서 소비자에게 원하는 장소의 혼잡도에 대한 정보를 제공하는 서비스입니다.</p>
<p>가장 핵심적인 기능을 그림으로 나타내면 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/32f21bf9-1c4d-40f1-b10b-24be4e6ee479/image.JPG" alt=""></p>
<blockquote>
<p><code>Cam</code>에서는 <code>Server</code>로 이미지 데이터를 보냅니다. 그리고 <code>Server</code>에서는 해당 이미지를 <code>Count people</code> 과정을 거쳐서 혼잡도 데이터를 얻습니다. 혼잡도 데이터는 <code>User</code>에게 보내집니다.</p>
</blockquote>
<br/>
<br/>

<h1 id="🌅-flask로-서버-구현">🌅 flask로 서버 구현</h1>
<p>이전에 만들었던 PeopleCounter 모듈을 사용하기 위해서 디렉토리 구조는 다음과 같이 만들었습니다.</p>
<pre><code>crowd-search-system/
│
├─object_detection
│  ├─darknetyolov4
│  │ __init__.py
│  │ personcounter.py
│
├─build
├─uploads
│ server.py</code></pre><ul>
<li><p><code>object_detection</code> 안에 <code>personcounter.py</code> 모듈이 있고 <code>server.py</code>에서는 이를 <code>import</code>할 수 있습니다.</p>
</li>
<li><p><code>build</code> 폴더는 정적 파일들 입니다.</p>
</li>
<li><p><code>uploads</code>는 클라이언트에서 업로드된 자료를 저장하는 폴더입니다.</p>
</li>
</ul>
<p>server.py는 어떻게 구현되었는지 살펴보겠습니다.</p>
<br/>

<h2 id="📌-react-웹-어플리케이션-배포">📌 React 웹 어플리케이션 배포</h2>
<p>React 앱은 예전에 하던 <a href="https://github.com/good-jinu/ghpage-template">토이 프로젝트</a>에서 가져와서 수정해서 썼습니다. <a href="https://good-jinu.github.io/ghpage-template/">웹앱의 대략적인 모습</a></p>
<p>기존의 코드에서 수정및 추가한 주요 부분은 서버에 이미지 업로드 기능, 서버로 부터 받은 혼잡도 데이터를 출력하는 기능입니다.</p>
<pre><code class="language-javascript">// path: frontend/src/components/MainSection.js

import React from &#39;react&#39;;
import axios from &#39;axios&#39;;
import &#39;./MainSection.css&#39;

class MainSection extends React.Component {
    constructor(){
        super();
        this.state = {
            selectedFile:&#39;&#39;, // 업로드할 파일
            peopleCount:&#39;&#39;, // 서버로부터 받아온 검출된 사람의 수
            timeCount:&#39;&#39;, // 서버로부터 받은 걸린 시간
            filechanged:false // 업로드할 파일이 새롭게 갱신되었는가
        }

        //업로드할 파일을 선택하면 호출되는 함수
        this.handleInputChange = this.handleInputChange.bind(this);
    }

    handleInputChange(event) {
        this.setState({
            selectedFile: event.target.files[0],
            filechanged: true
          });
    }

    submit(){
        //서버에 전송할 데이터 만들기
        const data = new FormData();
        data.append(&#39;file&#39;, this.state.selectedFile);
        console.log(this.state.selectedFile);
        let url = &quot;/upload&quot;;

        if(this.state.filechanged) {
            // 서버로부터 응답을 받기 전까지는 ... 출력
            this.setState({filechanged: false, peopleCount:&#39;...&#39;, timeCount:&#39;...&#39;});
            axios.post(url, data, {
            })
            .then(res =&gt; {
                // 사람 수 검출에 성공했을 경우
                this.setState({peopleCount:res.data[0], timeCount:res.data[1]});
            })
            .catch(error =&gt; {
                // 사람 수 검출에 실패했을 경우
                console.error(error);
                this.setState({peopleCount:&#39;?&#39;, timeCount:&#39;?&#39;});
            });
        }
        else {
            console.warn(&#39;already sent.&#39;);
        }
    }

    render() {
        return (
        &lt;section id=&quot;section&quot;&gt;
            &lt;nav&gt;
                nav area
            &lt;/nav&gt;
            &lt;article&gt;
                &lt;input type=&quot;file&quot; className=&quot;form-control&quot; name=&quot;upload_file&quot; onChange={this.handleInputChange} /&gt;
                &lt;br/&gt;
                &lt;button type=&quot;submit&quot; className=&quot;btn&quot; onClick={()=&gt;this.submit()}&gt;Send&lt;/button&gt;
                &lt;br/&gt;
                &lt;span&gt;count: {this.state.peopleCount}&lt;/span&gt;
                &lt;br/&gt;
                &lt;span&gt;time: {this.state.timeCount}&lt;/span&gt;
            &lt;/article&gt;
        &lt;/section&gt;
        );
    }
}

export default MainSection;</code></pre>
<blockquote>
<p>axios를 이용해서 http통신을 했으며 이미지를 올리고 검출된 사람 수와 걸린 시간을 받는 로직을 구현했습니다.</p>
</blockquote>
<br/>

<p>수정한 모습은 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/3aa1466e-5b12-4800-92de-da57a969d056/image.JPG" alt=""></p>
<blockquote>
<p>구현할려고 하는 핵심기능은 이미지 파일을 선택하고 Send 버튼을 누르면 그 이미지의 사람 수와 걸린 시간을 출력하는 것입니다.</p>
</blockquote>
<br/>

<p>React로 빌드된 파일들은 <code>build</code>에 저장되어 있습니다. 모두 정적 웹페이지로써 배포합니다.</p>
<pre><code class="language-python">from flask import Flask

app = Flask(__name__, static_url_path=&#39;/&#39;, static_folder=&#39;build&#39;)


@app.route(&#39;/&#39;)
def index_html(): # 루트에서는 index.html을 response로 보냄
     return app.send_static_file(&#39;index.html&#39;)

@app.errorhandler(404)
def not_found(e):  # SPA 이므로 404 에러는 index.html을 보냄으로써 해결한다.
    return index_html()

if __name__ == &#39;__main__&#39;:
    app.run(debug=True)</code></pre>
<p>정적파일들에 대한 url은 <code>/</code>루트로, 정적파일들은 <code>build</code>폴더 안에 있는 파일들로 배포합니다.</p>
<br/>

<h2 id="📌-서버에서-업로드-받기">📌 서버에서 업로드 받기</h2>
<p><code>server.py</code>에 업로드기능 까지 구현한 전체 코드입니다.</p>
<pre><code class="language-python">from flask import Flask, send_from_directory, request, jsonify, Response
import os
import time
from werkzeug.utils import secure_filename
from object_detection.personcounter import PeopleCounter
import threading

sema = threading.Semaphore(1)

UPLOAD_FOLDER = os.path.dirname(os.path.abspath(__file__)) + &#39;\\uploads&#39;
ALLOWED_EXTENSIONS = set([&#39;png&#39;, &#39;jpg&#39;, &#39;jpeg&#39;, &#39;gif&#39;])

pplcounter = PeopleCounter()

app = Flask(__name__, static_url_path=&#39;/&#39;, static_folder=&#39;build&#39;)
app.config[&#39;UPLOAD_FOLDER&#39;] = UPLOAD_FOLDER

def allowed_file(filename): # filename을 보고 지원하는 media type인지 판별
    return &#39;.&#39; in filename and \
           filename.rsplit(&#39;.&#39;, 1)[1] in ALLOWED_EXTENSIONS

@app.route(&#39;/upload&#39;, methods=[&#39;POST&#39;])
def upload_file():
    try:
        file = request.files[&#39;file&#39;]
        if file and allowed_file(file.filename.lower()):
            sta = time.time() # 시간 측정
            sema.acquire() # 세마포어 획득

            # 파일을 a.jpg/a.png/a.jpeg 형식으로 저장
            filename = &#39;a.&#39; + file.filename.rsplit(&#39;.&#39;, 1)[1]
            file.save(os.path.join(app.config[&#39;UPLOAD_FOLDER&#39;], filename))

            # 이미지로 부터 사람 수 예측
            res = pplcounter.count_people(os.path.join(app.config[&#39;UPLOAD_FOLDER&#39;], filename))
            sema.release() # 세마포어 릴리즈
            return jsonify([str(res), f&#39;{time.time() - sta:.2f}&#39;])
    except Exception as e:
        print(e)
    # unsupported media type (=http status 415)
    return Response(&#39;Error&#39;, status=415, mimetype=&#39;text/plain&#39;)

@app.route(&#39;/&#39;)
def index_html(): # 루트에서는 index.html을 response로 보냄
     return app.send_static_file(&#39;index.html&#39;)

@app.errorhandler(404)
def not_found(e):  # SPA 이므로 404 에러는 index.html을 보냄으로써 해결한다.
    return index_html()

if __name__ == &#39;__main__&#39;:
    app.run(debug=True)</code></pre>
<p>upload_file 함수에 대한 설명입니다.</p>
<ul>
<li>POST method로 request를 받습니다.</li>
<li>받은 파일이 적절한 media type인지 확인합니다.</li>
<li>시간 측정을 시작하고 사람 수 검출을 진행합니다.</li>
<li>성공하면 사람 수와 시간을 json형식으로 보냅니다.</li>
<li>실패하면 415 http status와 함께 Error라는 메세지를 보냅니다.</li>
</ul>
<br/>

<h2 id="📌-멀티-쓰레드-충돌-문제">📌 멀티 쓰레드 충돌 문제</h2>
<p>서버는 여러 클라이언트로부터 받는 요청을 비동기적으로 수행하고 이 과정에서 공유 자원의 동시 접근으로 인한 충돌을 발생시킵니다. 여기서는 객체 검출을 동시에 진행하다 보니 충돌이 발생했습니다.</p>
<p>이 문제를 해결하기 위해서 세마포어를 사용하였습니다. <a href="https://jhnyang.tistory.com/101">세마포어에 대한 이해</a></p>
<p>파이썬에서는 세마포어를 간단하게 사용하기위한 모듈이 존재합니다.</p>
<pre><code class="language-python">import threading
sema = threading.Semaphore(1)</code></pre>
<p>Semaphore 생성자에 넘겨준 1은 세마포어를 동시에 획득할 수 있는 쓰레드를 1개로 제한한다는 뜻입니다.</p>
<p>객체검출을 진행하는 업로드 모듈에서 세마포어를 사용한 코드입니다.</p>
<pre><code class="language-python">@app.route(&#39;/upload&#39;, methods=[&#39;POST&#39;])
def upload_file():
    try:
        file = request.files[&#39;file&#39;]
        if file and allowed_file(file.filename.lower()):
            sta = time.time() # 시간 측정
            sema.acquire() # 세마포어 획득

            # 파일을 a.jpg/a.png/a.jpeg 형식으로 저장
            filename = &#39;a.&#39; + file.filename.rsplit(&#39;.&#39;, 1)[1]
            file.save(os.path.join(app.config[&#39;UPLOAD_FOLDER&#39;], filename))

            # 이미지로 부터 사람 수 예측
            res = pplcounter.count_people(os.path.join(app.config[&#39;UPLOAD_FOLDER&#39;], filename))
            sema.release() # 세마포어 릴리즈
            return jsonify([str(res), f&#39;{time.time() - sta:.2f}&#39;])
    except Exception as e:
        print(e)
    # unsupported media type (=http status 415)
    return Response(&#39;Error&#39;, status=415, mimetype=&#39;text/plain&#39;)</code></pre>
<br/>
<br/>

<h1 id="🤗-결과">🤗 결과</h1>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/e731a34f-9faf-45fa-9dfd-fd7dec4c0887/image.jpg" alt=""></p>
<blockquote>
<p>모바일 기기에서 실행시켜본 결과</p>
</blockquote>
<h1 id="😵-해결해야할-문제들">😵 해결해야할 문제들</h1>
<ul>
<li>객체 검출을 하는데 시간이 오래 걸립니다.</li>
<li>시간이 오래 걸리면 timeout에 대한 처리도 해야합니다.</li>
</ul>
<p>일단 제가 사용한 환경에서는 yolo가 cpu-only로 동작하고 있습니다. cpu와 gpu의 뉴럴 네트워크에서 걸리는 시간 차이는 약 6배이상입니다.-&gt;[<a href="https://datamadness.github.io/TensorFlow2-CPU-vs-GPU">Reference</a>] 따라서 객체검출 시간 문제는 gpu 설정을 하면 해결될것으로 예상됩니다.</p>
<p>객체검출 뿐만 아니라 짧은 시간에 너무 많은 요청이 들어요면 그것 또한 문제가 됩니다. 이 때 클라이언트에서 영원히 응답을 기다릴 수는 없으므로 timeout에 대한 설정이 필요합니다.</p>
<br/>
<br/>

<h1 id="🔥-추가-사항">🔥 추가 사항</h1>
<p>yolo는 사람 뿐만 아니라 여러가지 물건, 동물을 검출할 수 있습니다. 따라서 자동차 수를 추출하도록 수정하면 주차장의 혼잡도를 구할 수 있습니다. 이런 식으로 검출 대상을 확대해서 유저에게 더 많은 정보를 제공하는 것을 추가로 고려해봐야 겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MNIST 데이터셋 분석 및 학습 모델 만들기]]></title>
            <link>https://velog.io/@good-jinu/MNIST-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%85%8B-%EB%B6%84%EC%84%9D-%EB%B0%8F-%ED%95%99%EC%8A%B5-%EB%AA%A8%EB%8D%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@good-jinu/MNIST-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%85%8B-%EB%B6%84%EC%84%9D-%EB%B0%8F-%ED%95%99%EC%8A%B5-%EB%AA%A8%EB%8D%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Wed, 01 Jun 2022 15:17:38 GMT</pubDate>
            <description><![CDATA[<p><a href="https://dacon.io/competitions/open/235596/overview/description">MNIST 숫자 이미지 문제</a></p>
<p>MNIST 데이터셋은 딥러닝을 처음 공부하는 사람들이 풀어보는 문제입니다. 숫자 손글씨 이미지에 대한 데이터와 라벨이 포함되어 있으며 60000개의 트레이닝 데이터와 10000개의 테스트 데이터가 있습니다.</p>
<h1 id="👀-데이터-살펴보기">👀 데이터 살펴보기</h1>
<p>keras의 내장 함수를 호출해서 mnist 데이터셋을 불러올 수 있습니다.</p>
<pre><code class="language-python">import tensorflow.keras
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()</code></pre>
<p>저의 경우에는 데이콘 문제 페이지에서 데이터를 받아와서 로드하는 방식으로 데이터셋을 불러왔습니다.</p>
<pre><code class="language-python">import pandas as pd
df = pd.read_csv(&#39;./mnist/train.csv&#39;, index_col=[&#39;index&#39;])</code></pre>
<p>csv 파일에서 불러왔기 때문에 데이터프레임 형식으로 되어있습니다. 데이터프레임을 살펴보면 첫번째 열에는 우리가 찾고자 하는 라벨이 있고 나머지 열은 이미지의 픽셀 데이터가 저장되어 있습니다</p>
<table class="dataframe" border="1">
  <thead>
    <tr style="text-align: right;">
      <th>index</th>
      <th>label</th>
      <th>px1</th>
      <th>px2</th>
      <th>...</th>
      <th>px783</th>
      <th>px784</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>5</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>2</th>
      <td>4</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>3</th>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>4</th>
      <td>9</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
    </tr>
  </tbody>
</table>

<br />
<br />

<h1 id="🪐-데이터-전처리">🪐 데이터 전처리</h1>
<p>먼저 입력 데이터 라벨 데이터를 나누고 이를 array형태로 바꿔줍니다.</p>
<pre><code class="language-python">X_array = df.iloc[:, 1:].to_numpy()
y_array = df.iloc[:,0].to_numpy()
X_array.shape, y_array.shape</code></pre>
<p>shape 확인 결과</p>
<pre><code>((60000, 784), (60000,))</code></pre><p>라벨 데이터를 원핫인코딩을 해줍니다.</p>
<pre><code class="language-python">from sklearn.preprocessing import OneHotEncoder
oh_enc = OneHotEncoder(dtype=int)

oh_enc.fit([[i] for i in range(10)])
y_array = oh_enc.transform(y_array.reshape(-1, 1)).toarray()</code></pre>
<br />

<p>x 데이터인 픽셀데이터는 0~255의 정수값으로 되어있는데 이를 Normalization을 해서 학습 성능을 높여줄 필요가 있습니다.</p>
<pre><code class="language-python">X_array = X_array.reshape(-1,28,28, 1).astype(float) / 255</code></pre>
<br />

<p>이미지 데이터를 시각화해서 확인해보겠습니다.</p>
<pre><code class="language-python">import matplotlib.pyplot as plt
plt.figure(figsize=(14, 14))
for i in range(1, 26):
    plt.subplot(5, 5, i)
    plt.imshow(X_array[i], &#39;gray&#39;)
    plt.axis(&#39;off&#39;)
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/08a043a9-6c63-4a2e-9297-743a7e45fd86/image.png" alt=""></p>
<br />
<br />

<h1 id="🎓-딥러닝-모델링">🎓 딥러닝 모델링</h1>
<p>이미지와 같은 2차원 데이터는 CNN을 거쳐서 학습을 시켜줘야 합니다. 그 이유는 일반적인 딥러닝 모델은 flatten된 1차원 형태의 데이터를 학습하는데 이렇게 되면 공간적 정보가 손실됩니다. 그래서 CNN을 사용하면 공간적 정보를 유지하며 특징을 뽑아낼 수 있게 됩니다.</p>
<p>CNN에서는 convolution 레이어와 pooling 레이어를 번갈아가며 거치고 마지막에는 flatten을 해서 일반 Dense 레이어를 통과할 수 있는 형태로 만들어 줍니다.</p>
<pre><code class="language-python">from tensorflow import keras
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, Flatten
from tensorflow.keras.layers import Conv2D, MaxPooling2D

model = Sequential()
model.add(Conv2D(32, kernel_size = (3, 3), activation=&#39;relu&#39;, input_shape=(28, 28, 1)))
model.add(MaxPooling2D((2, 2)))
model.add(Conv2D(64, (3, 3), activation=&#39;relu&#39;))
model.add(MaxPooling2D((2, 2)))
model.add(Conv2D(64, (3, 3), activation=&#39;relu&#39;))
model.add(MaxPooling2D((2, 2)))
model.add(Flatten())
model.add(Dense(64, activation=&#39;relu&#39;))
model.add(Dense(10, activation=&#39;softmax&#39;))

model.compile(optimizer=&#39;adam&#39;,
              loss=&#39;categorical_crossentropy&#39;,
              metrics=[&#39;accuracy&#39;])</code></pre>
<br />

<p>학습을 하기 전에 모델 평가를 위해서 학습 데이터와 테스트 데이터를 나누어 줍니다.</p>
<pre><code class="language-python">from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X_array, y_array, test_size=0.2, stratify=y_array, random_state=4)</code></pre>
<p>학습 데이터와 테스트 데이터 비율은 8:2이고 계층적 데이터 샘플링을 했습니다.</p>
<p>학습을 할 때에는 2번 연속 검증데이터 loss 값이 진전이 없다면 조기종료 시킵니다. 그리고 검증 데이터의 비율은 3:1이고 배치 사이즈는 256으로 설정했습니다.</p>
<pre><code class="language-python">from tensorflow.keras.callbacks import EarlyStopping

e_st = EarlyStopping(patience=2, restore_best_weights=True)
history = model.fit(x_train, y_train, epochs=10, validation_split=0.25, callbacks=[e_st], batch_size=256)</code></pre>
<p>학습이 잘 되었는지 확인하기 위해서 학습과정을 시각화해서 보겠습니다.</p>
<pre><code class="language-python">plt.plot(history.history[&#39;loss&#39;])
plt.plot(history.history[&#39;val_loss&#39;])
plt.title(&#39;Model loss&#39;)
plt.xlabel(&#39;Epoch&#39;)
plt.ylabel(&#39;Loss&#39;)
plt.legend([&#39;Train&#39;, &#39;Test&#39;], loc=&#39;upper left&#39;)
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/15497621-2985-4634-9414-41f82d5414fb/image.png" alt=""></p>
<p>학습 데이터와 검증 데이터의 loss값 차이가 크지 않은 것을 보니 과대적합이 되지 않았다고 판단 해볼 수 있습니다.</p>
<p>테스트 데이터로 성능을 확인해보겠습니다.</p>
<pre><code class="language-python">model.evaluate(x_test, y_test)</code></pre>
<p>결과</p>
<pre><code>375/375 [==============================] - 2s 5ms/step - loss: 0.0812 - accuracy: 0.9755
[0.08122505992650986, 0.9754999876022339]</code></pre><p>상당히 괜찮은 성능을 보입니다.</p>
<hr>
<p><a href="https://halfundecided.medium.com/%EB%94%A5%EB%9F%AC%EB%8B%9D-%EB%A8%B8%EC%8B%A0%EB%9F%AC%EB%8B%9D-cnn-convolutional-neural-networks-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-836869f88375">CNN 개념에 대한 글 링크</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 단어 퍼즐]]></title>
            <link>https://velog.io/@good-jinu/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%8B%A8%EC%96%B4-%ED%8D%BC%EC%A6%90</link>
            <guid>https://velog.io/@good-jinu/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%8B%A8%EC%96%B4-%ED%8D%BC%EC%A6%90</guid>
            <pubDate>Sun, 29 May 2022 15:00:34 GMT</pubDate>
            <description><![CDATA[<p>문제링크: <a href="https://programmers.co.kr/learn/courses/30/lessons/12983">https://programmers.co.kr/learn/courses/30/lessons/12983</a></p>
<br />

<h1 id="1-아이디어💡">1. 아이디어💡</h1>
<h2 id="11-문제분석">1.1. 문제분석</h2>
<ul>
<li>단어 조각들을 이용해서 주어진 타겟 단어를 완성해야한다.</li>
<li>단어 조각을 최소로 사용해야한다.</li>
</ul>
<br />
<br />

<h2 id="12-해결-방법">1.2. 해결 방법</h2>
<blockquote>
<p>다이나믹 프로그래밍</p>
</blockquote>
<br />

<ol>
<li>f(n)을 t 문자열의 n번째 문자까지를 단어 조각으로 만들 수 있을 때 필요한 최소의 단어 조각 수를 반환할때 점화식은 다음과 같다.</li>
</ol>
<pre><code>f(n) = min([f(n-5), f(n-4), f(n-3), f(n-2), f(n-1)]) + 1</code></pre><ol start="2">
<li><p>f(n)을 구할 때 f(n)이 가능한지 먼저 검사해야 한다. 입력이 다음과 같이 주어 졌을 때</p>
<pre><code>strs = [&quot;ba&quot;,&quot;na&quot;,&quot;n&quot;,&quot;a&quot;]
t = &quot;banana&quot;</code></pre></li>
<li><p>&quot;b&quot;를 완성할 수 없는 단어조각은 없지만 &quot;ba&quot;를 완성할 수 있는 단어조각이 있으므로 f(1)은 1이 된다. 그리고 그다음 &quot;n&quot;을 완성할 수 있으므로 f(2)는 2가 되고 f(3)은 &quot;na&quot; 또는 &quot;a&quot;을 통해서 완성될 수 있고 f(1)이 f(2) 보다 작으므로 <code>f(3) = f(1) + 1 = 3</code> 이 된다.</p>
</li>
</ol>
<h1 id="2-코드">2. 코드</h1>
<pre><code class="language-python">from collections import deque

def solution(strs, t):
    strs = set(strs)
    mem = [len(t)+1] * len(t) # 문자열의 n번째 문자까지 필요한 최소 단어 조각의 수 (메모이제이션)
    q = deque()
    q.append((0, 0)) # (n번째 문자의 인덱스, 사용한 단어개수)
    while q:
        counts = q.popleft()
        scr = counts[1] + 1 # 단어 한개 추가
        end_i = counts[0] + 5 if len(t) - counts[0] &gt;= 5 else len(t)
        for i in range(counts[0], end_i):
            if t[counts[0]:i + 1] in strs and mem[i] &gt; scr:
                if i + 1 == len(t): # t의 끝에 도착했다면 사용한 최소 단어개수 반환
                    return scr
                q.append((i + 1, scr))
                mem[i] = scr
    return -1</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 1167. 트리의 지름]]></title>
            <link>https://velog.io/@good-jinu/%EB%B0%B1%EC%A4%80-1167.-%ED%8A%B8%EB%A6%AC%EC%9D%98-%EC%A7%80%EB%A6%84</link>
            <guid>https://velog.io/@good-jinu/%EB%B0%B1%EC%A4%80-1167.-%ED%8A%B8%EB%A6%AC%EC%9D%98-%EC%A7%80%EB%A6%84</guid>
            <pubDate>Sun, 29 May 2022 14:52:54 GMT</pubDate>
            <description><![CDATA[<p>문제링크: <a href="https://www.acmicpc.net/problem/1167">https://www.acmicpc.net/problem/1167</a></p>
<br />

<h1 id="1-아이디어💡">1. 아이디어💡</h1>
<h2 id="11-문제분석">1.1. 문제분석</h2>
<ul>
<li>트리에서 가장 먼 두개의 노드의 거리를 구해야한다.</li>
<li>노드간에 거리가 주어진다.</li>
</ul>
<br />
<br />

<h2 id="12-해결-방법">1.2. 해결 방법</h2>
<blockquote>
<p>플로이드 와샬 알고리즘 사용해본 결과 -&gt; 메모리 초과</p>
<blockquote>
<p>모든 노드가 다른 모든 노드의 거리에 대한 정보를 저장하는 것은 공간 복잡도가 커짐</p>
</blockquote>
</blockquote>
<blockquote>
<p>트리는 최소 간선 수를 가진다는 것을 이용, 거리 탐색할 때는 BFS 사용</p>
</blockquote>
<br />

<ol>
<li>각 노드와 연결된 간선의 정보를 dictionary로 저장한다. 만약 입력이 다음과 같이 주어지면 </li>
</ol>
<pre><code>5
1 3 2 -1
2 4 4 -1
3 1 2 4 3 -1
4 2 4 3 3 5 6 -1
5 4 6 -1</code></pre><p>실제 노드번호는 (입력된 노드번호 - 1)로 저장되고 그 형태는 다음과 같다.</p>
<pre><code>{2: 2}
{3: 4}
{0: 2, 3: 3}
{1: 4, 2: 3, 4: 6}
{3: 6}</code></pre><br />

<p><img src="https://velog.velcdn.com/images/good-jinu/post/8c448854-baae-4001-8566-a7d81a54c300/image.JPG" alt=""></p>
<ol start="2">
<li>임의의 단말 노드를 하나 정해서 다른 모든 노드에 대한 거리 탐색을 해서 가장 먼 거리의 노드를 찾는다. ex) 0번 노드에서 탐색하면 4번노드까지 11의 최대 거리를 가진다.</li>
<li>찾은 최대 거리를 가지는 노드에서 다시 탐색을 해서 찾은 최대거리가 트리의 지름이다. ex) 4번 노드에서 다시 탐색하면 0번노드까지 11의 최대거리를 가진다.</li>
</ol>
<h1 id="2-코드">2. 코드</h1>
<pre><code class="language-python">import sys
from collections import deque
input = sys.stdin.readline

# 입력 받기
N = int(input())
table = [{} for _ in range(N)]
for _ in range(N):
    a = list(map(int, input().split()))[:-1]
    for i in range(1, len(a), 2):
        table[a[0] - 1][a[i] - 1] = a[i + 1]
    if len(a) == 3: # 트리의 말단 노드 leaf에 저장
        leaf = a[0] - 1

q = deque()
max_dst = 0 # 결과 값 저장하는 변수
for _ in range(2):
    # 큐에 (노드번호, 간선 가중치) 삽입
    q.append((list(table[leaf].keys())[0], list(table[leaf].values())[0]))

    # visited 초기화
    visited = [-1] * N
    visited[leaf] = 0
    visited[q[0][0]] = q[0][1]
    while q: # BFS로 다른 모든 노드로의 거리 계산
        a = q.popleft()
        for i in table[a[0]]:
            if visited[i] == -1 or visited[i] &gt; table[a[0]][i] + a[1]:
                q.append((i, table[a[0]][i] + a[1]))
                visited[i] = q[-1][1]

    # 가장 거리가 먼 노드의 인덱스와 거리값을 저장
    for i in range(len(visited)):
        if visited[i] &gt; max_dst:
            max_dst = visited[i]
            leaf = i
print(max_dst)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[yolo를 활용한 사람 수 추출하기]]></title>
            <link>https://velog.io/@good-jinu/yolo%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%82%AC%EB%9E%8C-%EC%88%98-%EC%B6%94%EC%B6%9C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@good-jinu/yolo%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%82%AC%EB%9E%8C-%EC%88%98-%EC%B6%94%EC%B6%9C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 27 May 2022 17:42:30 GMT</pubDate>
            <description><![CDATA[<p><code>대기열 혼잡도 검색 시스템</code> 프로젝트를 진행함에 있어서 사진으로부터 사람 수를 추출하는 모듈이 필요하게 되었다.</p>
<p> object detection에 있어서 가장 성능이 좋은 yolo를 활용해서 모듈을 만들어 보기로 하였다.</p>
<br />
<br />

<h1 id="yolo-설치하기윈도우-기준">yolo 설치하기(윈도우 기준)</h1>
<p> 자세한 설명은 <a href="https://pjreddie.com/darknet/install/">공식 페이지</a></p>
<ol>
<li><p>(option)gpu 사용 설정</p>
<ul>
<li><a href="https://developer.nvidia.com/cuda-downloads">CUDA</a> 설치하기</li>
<li><a href="https://developer.nvidia.com/rdp/cudnn-download">cuDNN</a> 설치하기</li>
<li>자세한 설치 방법은 <a href="https://2-54.tistory.com/4">https://2-54.tistory.com/4</a> 참고</li>
</ul>
<ol start="2">
<li>CMake <a href="https://cmake.org/download/">다운로드</a></li>
</ol>
<ul>
<li>설치를 하고 시스템 변수 <code>Path</code>에 설치한 cmake/bin 경로를 추가해주어야 한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/26d21d04-aed4-4f08-9b70-c79a50f969a8/image.JPG" alt=""></p>
</li>
</ol>
<ol start="3">
<li><p>darknet-yolov4 다운로드 (<a href="https://github.com/AlexeyAB/darknet">repository</a>)</p>
<ul>
<li><p>Releases 버전의 Assets에서 <code>yolov4.weights</code>와 <code>Source code (zip)</code>을 다운받고 소스코드의 압축을 풀고 소스코드 내부에 <code>yolov4.weights</code>파일을 넣는다. 그리고 <code>build.ps1</code>을 실행 시키고 현재 환경에 맞게 빌드를 진행한다.</p>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/6e2a290c-fc29-423c-a0a3-946e6da21b17/image.JPG" alt=""></p>
</li>
</ul>
</li>
</ol>
<br />
<br />

<h1 id="object-detection을-해보자-👀">object detection을 해보자 👀</h1>
<p>darknet_yolov4 폴더내에 보면 darknet_images.py 파일이 있는데 이를 실행시키고 이미지 경로를 입력하면 객체검출을 해준다.</p>
<ul>
<li>여러 사람들을 검출한 모습 (핸드백이랑 넥타이까지 검출했다.)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/9b1e675a-bc29-47d2-bfdc-6992c653e63d/image.png" alt=""></p>
<ul>
<li>고양이도 가능하다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/8cec0533-0795-4a3d-8111-1ccd427db70e/image.png" alt=""></p>
<br />
<br />

<h1 id="🐈-이미지로-부터-detetion하는-코드-구성">🐈 이미지로 부터 detetion하는 코드 구성</h1>
<p>먼저 detections 데이터를 반환하는 함수를 살펴보면 다음과 같다.</p>
<pre><code class="language-python">def image_detection(self, image_path, network, class_names, class_colors, thresh):
    # Darknet doesn&#39;t accept numpy images.
    # Create one with image we reuse for each detect
    width = darknet.network_width(network)
    height = darknet.network_height(network)
    darknet_image = darknet.make_image(width, height, 3)

    image = cv2.imread(image_path)
    image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    image_resized = cv2.resize(image_rgb, (width, height),
                                interpolation=cv2.INTER_LINEAR)

    darknet.copy_image_from_bytes(darknet_image, image_resized.tobytes())
    detections = darknet.detect_image(network, class_names, darknet_image, thresh=thresh)
    darknet.free_image(darknet_image)
    return detections</code></pre>
<ol>
<li>불러온 네트워크에 맞는 크기로 다크넷 이미지를 생성한다.</li>
</ol>
<pre><code class="language-python">width = darknet.network_width(network)
height = darknet.network_height(network)
darknet_image = darknet.make_image(width, height, 3)</code></pre>
<ol start="2">
<li>입력받은 이미지 경로를 통해 이미지를 불러오고 색상 표현 방식을 BGR에서 RGB로 바꾼다. 그 후에 전에 설정한 width와 height로 resize를 한다.</li>
</ol>
<pre><code class="language-python">image = cv2.imread(image_path)
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
image_resized = cv2.resize(image_rgb, (width, height),
                           interpolation=cv2.INTER_LINEAR)</code></pre>
<ol start="3">
<li>detect_image 함수를 호출해서 이미지로 부터 detections 데이터를 생성하고 반환한다.</li>
</ol>
<pre><code class="language-python">darknet.copy_image_from_bytes(darknet_image, image_resized.tobytes())
detections = darknet.detect_image(network, class_names, darknet_image, thresh=thresh)
darknet.free_image(darknet_image)
return detections</code></pre>
<br />

<ul>
<li>detection 데이터는 다음과 같이 생겼다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/0847a358-0fd4-4215-a9fa-142d04de2ced/image.png" alt=""></p>
<p>리스트에 객체들에 대한 정보가 튜플형태로 포함되어 있으며 각 객체의 형태는 다음과 같다.</p>
<pre><code>(&#39;객체&#39;, &#39;확률&#39;, &#39;객체 바운딩 박스 좌표&#39;)</code></pre><br />
<br />

<h1 id="🌃-darknet-api를-이용한-사람-수-추출하는-코드">🌃 darknet api를 이용한 사람 수 추출하는 코드</h1>
<p>PeopleCounter 라는 클래스를 만들어서 사진으로부터 사람 수를 셀 수 있도록 하였다. 다음은 메소드들에 대한 설명이다.</p>
<ul>
<li>image_detection(image_path, network, class_names, class_colors, thresh): 사진으로부터 detections 데이터를 생성해서 반환</li>
<li>count_people(self, image_name): 사진경로를 입력하면 사진에서 검출된 사람 수를 반환</li>
</ul>
<p>코드</p>
<pre><code class="language-python">from darknetyolov4 import darknet
import random
import cv2
import os

class PeopleCounter:
    def __init__(self):
        random.seed(3)  # deterministic bbox colors
        cd_path = os.path.dirname(os.path.abspath(__file__))
        self.network, self.class_names, self.class_colors = darknet.load_network(
            cd_path + &#39;/darknetyolov4/cfg/yolov4.cfg&#39;,
            cd_path + &#39;/darknetyolov4/cfg/coco.data&#39;,
            cd_path + &#39;/darknetyolov4/yolov4.weights&#39;,
            batch_size=1
        )

    def image_detection(self, image_path, network, class_names, class_colors, thresh):
        # Darknet doesn&#39;t accept numpy images.
        # Create one with image we reuse for each detect
        width = darknet.network_width(network)
        height = darknet.network_height(network)
        darknet_image = darknet.make_image(width, height, 3)

        image = cv2.imread(image_path)
        image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        image_resized = cv2.resize(image_rgb, (width, height),
                                interpolation=cv2.INTER_LINEAR)

        darknet.copy_image_from_bytes(darknet_image, image_resized.tobytes())
        detections = darknet.detect_image(network, class_names, darknet_image, thresh=thresh)
        darknet.free_image(darknet_image)
        return detections

    def count_people(self, image_name):
        detections = self.image_detection(
            image_name, self.network, self.class_names, self.class_colors, 0.3
        )
        cnt = 0
        for i in detections:
            if i[0] == &#39;person&#39;:
                cnt+=1
        return cnt</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 1463. 1로 만들기]]></title>
            <link>https://velog.io/@good-jinu/BOJ-1%EB%A1%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@good-jinu/BOJ-1%EB%A1%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Fri, 27 May 2022 02:39:00 GMT</pubDate>
            <description><![CDATA[<h1 id="boj-1로-만들기">[BOJ] 1로 만들기</h1>
<p><a href="https://www.acmicpc.net/problem/1463">https://www.acmicpc.net/problem/1463</a></p>
<br />
<br />

<h2 id="1-아이디어💡">1. 아이디어💡</h2>
<h3 id="11-문제분석">1.1. 문제분석</h3>
<ul>
<li>3가지의 액션을 적절하게 사용해서 n을 1로 만들어야한다.</li>
<li>1로 만들 때 최소한의 액션을 사용하고 액션을 한 횟수를 출력한다.</li>
</ul>
<br />

<h3 id="12-해결-방법">1.2. 해결 방법</h3>
<blockquote>
<p>다이나믹 프로그래밍으로 해결한다.</p>
</blockquote>
<p>n을 1로 만들기 위한 최소값은 n/3, n/2, n-1 을 1로 만들기 위한 최소값들 중에 최소값에다가 1을 더한 값이다. </p>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/de7932db-c7a7-4144-8149-b831c4792185/image.JPG" alt=""></p>
<br />

<ol>
<li>n/3, n/2, n-1의 값을 알 수 없으므로 2부터 차례대로 계산해서 리스트에 값을 저장한다.</li>
<li>n의 값을 구하면 그 값을 출력한다.</li>
</ol>
<br />
<br />

<h2 id="2-테스트-케이스">2. 테스트 케이스</h2>
<p>다이나믹 프로그래밍을 하지 않으면 실패하는 테스트 케이스들</p>
<p>입력</p>
<pre><code>80</code></pre><p>출력</p>
<pre><code>6</code></pre><br />

<p>입력</p>
<pre><code>25</code></pre><p>출력</p>
<pre><code>5</code></pre><h2 id="2-코드">2. 코드</h2>
<pre><code class="language-python">import sys
input = sys.stdin.readline

n = int(input())
cnt = {1:0}
for i in range(2, n+1):
    if i % 3 == 0:
        cnt[i] = cnt[i//3] + 1
    if i % 2 == 0:
        if i in cnt:
            cnt[i] = min(cnt[i], cnt[i//2] + 1)
        else:
            cnt[i] = cnt[i//2] + 1
    if i in cnt:
        cnt[i] = min(cnt[i], cnt[i-1] + 1)
    else:
        cnt[i] = cnt[i-1]+1

print(cnt[n])
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 1043. 거짓말]]></title>
            <link>https://velog.io/@good-jinu/0r3u6r5h</link>
            <guid>https://velog.io/@good-jinu/0r3u6r5h</guid>
            <pubDate>Fri, 27 May 2022 02:34:41 GMT</pubDate>
            <description><![CDATA[<h1 id="boj-거짓말">[BOJ] 거짓말</h1>
<p><a href="https://www.acmicpc.net/problem/1043">https://www.acmicpc.net/problem/1043</a></p>
<br />
<br />

<h2 id="1-아이디어💡">1. 아이디어💡</h2>
<h3 id="11-문제분석">1.1. 문제분석</h3>
<ul>
<li>진실을 아는 사람이 참석하는 파티에서는 거짓말을 하면 안된다.</li>
<li>진실을 아는 사람이 참석한 파티에 참석한 진실을 모르는 사람이 참석한 또 다른 파티에서도 거짓말을 하면 안된다.</li>
<li>파티를 사람들 간에 관계라고 했을 때, 진실을 아는 사람과 관계가 여러단계 건너 있더라도 연결되어 있다면 그 사람이 있는 파티에서는 거짓말을 하면 안된다.</li>
</ul>
<br />

<h3 id="12-해결-방법">1.2. 해결 방법</h3>
<ol>
<li>진실을 아는 사람들의 리스트를 큐에 넣는다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/79bb6e56-6d4c-4064-975b-8eb6a555d67c/image.JPG" alt=""></p>
<br />

<ol start="2">
<li>큐에서 앞에 요소를 꺼내서 그 사람이 참가한 파티들을 모두 파티 블랙리스트에 넣고 거기에 참가한 블랙리스트가 아니었던 사람들 모두 큐에 넣는다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/82ca5e85-1746-4d2a-8519-831e6e4ca06c/image.JPG" alt=""></p>
<br />

<ol start="3">
<li><p>위의 과정을 반복한다.</p>
</li>
<li><p>파티리스트에 남은 요소의 개수를 출력한다.</p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/good-jinu/post/8a9a3e5b-9e0b-4105-aa0d-75d32d9ad699/image.JPG" alt=""></p>
<h2 id="코드">코드</h2>
<pre><code class="language-python">import sys
from collections import deque
input = sys.stdin.readline

def f():
    N, M = map(int, input().split())
    tmp = list(map(int, input().split()))
    if tmp[0] == 0:
        return M

    que = deque()
    # 거짓말하면 안되는 사람 리스트 (블랙 리스트)
    blk_l = [False] * N
    for i in tmp[1:]:
        blk_l[i - 1] = True
        que.append(i - 1)
    # 각 파티에 참가하는 사람들 리스트
    people_l = []
    # 각 사람들이 참가하는 파티 리스트
    party_l = [[] for _ in range(N)]
    for i in range(M):
        tmp = list(map(lambda x: int(x) - 1, input().split()))[1:]
        for j in tmp:
            party_l[j].append(i)
        people_l.append(tmp)
    # 블랙리스트의 사람이 참석한 파티 리스트 (거짓말을 하면 안되는 파티)
    blk_party = []
    while que:
        person = que.popleft()
        for i in party_l[person]:
            if i not in blk_party:
                blk_party.append(i)
                for j in people_l[i]:
                    if not blk_l[j]:
                        blk_l[j] = True
                        que.append(j)
    return M - len(blk_party)

print(f())</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 1389. 케빈 베이컨의 6단계]]></title>
            <link>https://velog.io/@good-jinu/BOJ-%EC%BC%80%EB%B9%88-%EB%B2%A0%EC%9D%B4%EC%BB%A8%EC%9D%98-6%EB%8B%A8%EA%B3%84</link>
            <guid>https://velog.io/@good-jinu/BOJ-%EC%BC%80%EB%B9%88-%EB%B2%A0%EC%9D%B4%EC%BB%A8%EC%9D%98-6%EB%8B%A8%EA%B3%84</guid>
            <pubDate>Fri, 27 May 2022 02:30:00 GMT</pubDate>
            <description><![CDATA[<h1 id="boj-케빈-베이컨의-6단계">[BOJ] 케빈 베이컨의 6단계</h1>
<p><a href="https://www.acmicpc.net/problem/1389">https://www.acmicpc.net/problem/1389</a></p>
<br />
<br />

<h2 id="1-아이디어💡">1. 아이디어💡</h2>
<h3 id="11-문제분석">1.1. 문제분석</h3>
<ul>
<li>모든 사람들은 다른 모든 사람들에 대해 무조건 친구관계가 연결되게 입력이 주어진다.</li>
<li>케빈 베이컨 점수는 특정 사람이 다른 모든 사람들과 관계가 이어지기 위해 거쳐가야하는 관계수를 합한 값이다.</li>
</ul>
<br />

<h3 id="12-해결-방법">1.2. 해결 방법</h3>
<blockquote>
<p>플로이드 와샬 알고리즘을 사용한다.</p>
</blockquote>
<ol>
<li>각 사람들의 관계 거리에 대한 값을 저장한 테이블을 생성한다.</li>
</ol>
<table>
<thead>
<tr>
<th align="center"></th>
<th align="center"><strong>1</strong></th>
<th align="center"><strong>2</strong></th>
<th align="center"><strong>3</strong></th>
<th align="center"><strong>4</strong></th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>1</strong></td>
<td align="center">0</td>
<td align="center">INF</td>
<td align="center">INF</td>
<td align="center">INF</td>
</tr>
<tr>
<td align="center"><strong>2</strong></td>
<td align="center">INF</td>
<td align="center">0</td>
<td align="center">INF</td>
<td align="center">INF</td>
</tr>
<tr>
<td align="center"><strong>3</strong></td>
<td align="center">INF</td>
<td align="center">INF</td>
<td align="center">0</td>
<td align="center">INF</td>
</tr>
<tr>
<td align="center"><strong>4</strong></td>
<td align="center">INF</td>
<td align="center">INF</td>
<td align="center">INF</td>
<td align="center">0</td>
</tr>
</tbody></table>
<ol start="2">
<li>입력으로 받은 관계를 모두 1로 바꿔준다.</li>
</ol>
<table>
<thead>
<tr>
<th align="center"></th>
<th align="center"><strong>1</strong></th>
<th align="center"><strong>2</strong></th>
<th align="center"><strong>3</strong></th>
<th align="center"><strong>4</strong></th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>1</strong></td>
<td align="center">0</td>
<td align="center">1</td>
<td align="center">INF</td>
<td align="center">1</td>
</tr>
<tr>
<td align="center"><strong>2</strong></td>
<td align="center">1</td>
<td align="center">0</td>
<td align="center">1</td>
<td align="center">INF</td>
</tr>
<tr>
<td align="center"><strong>3</strong></td>
<td align="center">INF</td>
<td align="center">1</td>
<td align="center">0</td>
<td align="center">1</td>
</tr>
<tr>
<td align="center"><strong>4</strong></td>
<td align="center">1</td>
<td align="center">INF</td>
<td align="center">1</td>
<td align="center">0</td>
</tr>
</tbody></table>
<ol start="3">
<li><p>1행부터 차례대로 자기와 연결된 사람들을 큐에 넣고 큐에서 꺼낸 사람의 행을 탐색해서 연결된 사람을 큐가 꺼내진 행의 사람으로 부터의 거리를 계산해서 기존의 저장된 거리값보다 작다면 새롭게 갱신한다.</p>
<ul>
<li><p>예시: 1행에서 2, 4와 연결 되어 있으므로 큐에 삽입한다.</p>
</li>
<li><p>큐에서 꺼내면 2가 나오고 2행에서 연결된 3까지의 거리가 1이다. 따라서 1에서 3까지의 거리는 1+1=2가 된다.</p>
<table>
<thead>
<tr>
<th align="center"></th>
<th align="center"><strong>1</strong></th>
<th align="center"><strong>2</strong></th>
<th align="center"><strong>3</strong></th>
<th align="center"><strong>4</strong></th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>1</strong></td>
<td align="center">0</td>
<td align="center">1</td>
<td align="center">2</td>
<td align="center">1</td>
</tr>
<tr>
<td align="center"><strong>2</strong></td>
<td align="center">1</td>
<td align="center">0</td>
<td align="center">1</td>
<td align="center">INF</td>
</tr>
<tr>
<td align="center"><strong>3</strong></td>
<td align="center">INF</td>
<td align="center">1</td>
<td align="center">0</td>
<td align="center">1</td>
</tr>
<tr>
<td align="center"><strong>4</strong></td>
<td align="center">1</td>
<td align="center">INF</td>
<td align="center">1</td>
<td align="center">0</td>
</tr>
</tbody></table>
</li>
</ul>
</li>
<li><p>위의 과정을 반복한다.</p>
</li>
</ol>
<table>
<thead>
<tr>
<th align="center"></th>
<th align="center"><strong>1</strong></th>
<th align="center"><strong>2</strong></th>
<th align="center"><strong>3</strong></th>
<th align="center"><strong>4</strong></th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>1</strong></td>
<td align="center">0</td>
<td align="center">1</td>
<td align="center">2</td>
<td align="center">1</td>
</tr>
<tr>
<td align="center"><strong>2</strong></td>
<td align="center">1</td>
<td align="center">0</td>
<td align="center">1</td>
<td align="center">2</td>
</tr>
<tr>
<td align="center"><strong>3</strong></td>
<td align="center">2</td>
<td align="center">1</td>
<td align="center">0</td>
<td align="center">1</td>
</tr>
<tr>
<td align="center"><strong>4</strong></td>
<td align="center">1</td>
<td align="center">2</td>
<td align="center">1</td>
<td align="center">0</td>
</tr>
</tbody></table>
<p>위의 예시에선 모든 사람의 케빈 베이컨 점수가 3으로 똑같기 때문에 가장 작은 인덱스인 1을 출력한다.</p>
<h2 id="2-코드">2. 코드</h2>
<pre><code class="language-python">import sys
from collections import deque
input = sys.stdin.readline

INF = 128
N, M = map(int, input().split())
relationship = [[INF] * N for _ in range(N)]
for i in range(M):
    a, b = map(int, input().split())
    relationship[a - 1][b - 1] = 1
    relationship[b - 1][a - 1] = 1
for i in range(N):
    relationship[i][i] = 0
que = deque()
for i in range(N):
    for j in range(N):
        if i != j and relationship[i][j] &lt; INF:
            que.append((j, relationship[i][j]))
    while que:
        pos = que.popleft()
        for j in range(N):
            if relationship[pos[0]][j] + pos[1] &lt; relationship[i][j]:
                relationship[i][j] = relationship[pos[0]][j] + pos[1]
                que.append((j, relationship[i][j]))
total_score = list(map(lambda x: sum(x), relationship))
print(total_score.index(min(total_score)) + 1)</code></pre>
]]></description>
        </item>
    </channel>
</rss>