<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>GRIT_Munhyeok.log</title>
        <link>https://velog.io/</link>
        <description>흔들리지 말고 나만의 공부를 하자</description>
        <lastBuildDate>Wed, 03 Sep 2025 14:48:43 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>GRIT_Munhyeok.log</title>
            <url>https://images.velog.io/images/grit_munhyeok/profile/0c1c5aeb-61de-4f51-b927-ac498483945a/social.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. GRIT_Munhyeok.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/grit_munhyeok" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[React Native 공식 문서 기여하기]]></title>
            <link>https://velog.io/@grit_munhyeok/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C-%EA%B8%B0%EC%97%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@grit_munhyeok/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C-%EA%B8%B0%EC%97%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 03 Sep 2025 14:48:43 GMT</pubDate>
            <description><![CDATA[<h1 id="분명-해결했다고-생각했는데">분명 해결했다고 생각했는데...</h1>
<p>정신없이 살던 중에 이메일이 왔다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/879ebd8f-b36a-41da-a2a0-f9fee2ec7791/image.png" alt="">
<strong>잉?</strong></p>
</blockquote>
<p>16KB 페이지 크기 요구사항을 해결하려고 React Native 버전을 0.79.2로 올려놨기 때문에 해당 이슈는 해결되었다.. 생각하고 있었는데 뜬금없이 이런 이메일이 왔다.</p>
<p>이상하다... 내 기억상으로 0.77 버전에 이미 해당 이슈를 해결한 걸로 기억하고 있었다.
<a href="https://reactnative.dev/blog/2025/01/21/version-0.77#android-version-15-support--16kb-page-support">공식 블로그 글</a></p>
<hr>
<h1 id="원인-분석-하기">원인 분석 하기</h1>
<p>바로 원인 분석을 위해 구글 플레이 콘솔을 켜고 확인해 봤다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/8415a33b-cf8c-4453-b1b6-65de262a5782/image.png" alt="">webp... 이미지 컴포넌트에 무슨 문제가 있나..?</p>
</blockquote>
<p>고민을 해보니 과거에 Image 컴포넌트에서 webp 혹은 gif 같은 형식을 쓰려면 build.gradle에 어떤 라이브러리를 추가해야 한다고 해서 추가했던 기억이 생각났다.</p>
<p>그래서 <strong>&quot;아 버전이 뭔가 오래됐나 보다 생각하고 <a href="https://reactnative.dev/">공식 Docs</a>로 들어가서 버전 체크해 봐야지~&quot;</strong> 하고 들어가서 이미지 컴포넌트를 확인했다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/d7115347-775c-4be3-82dd-8fd5b4f683db/image.png" alt="">공식 문서를 보니 <a href="https://github.com/facebook/fresco">Fresco</a> 3.2.0 라이브러리를 추가해야 한다.</p>
</blockquote>
<p>그래서 내 버전을 확인해 봤는데...</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/7078bfaa-daa4-472c-a78f-69cf9e58bbee/image.png" alt="">야무지게 구버전을 사용하고 있었다.</p>
</blockquote>
<p>세상에 3.1.3 난 대체 얼마나 오래된 버전을 사용하고 있었는가... 하면서 부랴부랴 3.2.0으로 변경했다.</p>
<h2 id="fresco-정체가-뭐냐">Fresco... 정체가 뭐냐...</h2>
<p>근데 문득 이 Fresco라는 라이브러리가 어떤 걸 위해 만들어진 라이브러리인가 궁금해서 공식 Github에 들어가 보았다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/b75914db-079c-4166-9f8e-5b6a5dc841af/image.png" alt="">
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/152c92c7-b900-4199-bf81-e626da1512d9/image.png" alt="">Fresco는 안드로이드 앱에 이미지를 띄우기 위한 강력한 시스템이라고 한다.
요약하자면 Fresco를 쓰면 개발자가 OutOfMemoryError 겪는 일을 줄여줄 수 있다고 한다. <del>(작동 원리 적으면 길어질 것 같아서 생략..)</del></p>
</blockquote>
<p>사실 나는 첫 페이지 상단을 보자마자 당황했었다.
왜 당황했는지 이 글을 읽는 여러분도 찾았을 거라 믿는다.</p>
<p>왜냐하면...</p>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/1e55842d-d0e5-488b-b8c5-b93433915cfe/image.png" alt="">Release 버전이 3.6.0이었기 때문이다.</p>
<p>오...? 뭐지...?</p>
<p>당장 릴리즈 내용을 읽어보았다</p>
<p>AVIF 포맷을 지원한다...
디스크 캐시 읽기 시간 초과 지원...
.
.
. 
쭉 읽어보다가 3.4.0 버전에서 난 스크롤을 멈춰버렸다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/4bc0752c-119f-49f1-a06e-3d963a8a4ad0/image.png" alt="">3.4.0 버전부터 Android 15를 위한 16KB 페이지 크기가 지원된다는 것이다.</p>
</blockquote>
<p><strong>그렇다는 건 3.2.0 버전은 여전히 16KB 페이지 크기를 지원하지 않는다는 의미다.</strong></p>
<hr>
<h1 id="이게-왜-문제일까">이게 왜 문제일까?</h1>
<p>일단 공식 문서는 React Native로 개발하는 개발자도 보지만
<strong>React Native를 처음 시작하는 사람들이 가장 먼저 보는 홈페이지다</strong></p>
<p>공식 문서에 있는 컴포넌트들을 보면서 어떻게 사용할 수 있는지 공부하는데
저런 식으로 오래된 버전의 라이브러리 버전을 유지하면
<strong>RN으로 프로젝트를 만드는 사람들은 전부 잠재적으로 16KB 페이지 크기 이슈를 겪을 수 있다는 것이다.</strong> </p>
<p>그리고 꽤 충격적인 사실이 있는데...
<strong>25년 11월 1일부터 16KB 메모리 페이지 크기를 지원하지 않으면 업데이트를 출시할 수 없게 된다.</strong> 이건 치명적이라고 생각한다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/0040c23d-d784-49d9-ad71-78d562ea2a88/image.png" alt=""></p>
<blockquote>
<h3 id="아-expo-유저는-괜찮다">아 Expo 유저는 괜찮다.</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/ce02bda9-0e6f-4ff0-8e19-64775166b38f/image.png" alt="">아마 이건 RN-CLI를 쓰는 유저만 해당될 것 같다.</p>
</blockquote>
<h2 id="--나는-webp나-gif-안-쓸-거라서-fresco-필요-없는데요"><strong>??? : 나는 webp나 gif 안 쓸 거라서 Fresco 필요 없는데요?</strong></h2>
<p>Fresco를 안 쓴다면 당연히 해당 이슈에서 벗어날 수 있다.
하지만 요즘 WebP는 이미지 압축률도 좋고 애니메이션도 지원하기 때문에 여러 플랫폼에서 사용하고 있는 이미지 포맷이다.</p>
<p><strong>결국 나는 해당 문제를 직접 PR로 올려보기로 했다.</strong></p>
<hr>
<h1 id="문제-해결하기">문제 해결하기</h1>
<p>어떤 걸 할 땐 항상 이유가 있어야 한다.
<strong>왜 이렇게 했는지에 대해 설명</strong> 하기 위해 위에서 찾은 내용과 <a href="https://github.com/facebook/react-native">React Native</a>의 깃허브를 좀 찾아보았다.</p>
<p>Release 내역을 찾아보니 <a href="https://github.com/facebook/react-native/releases/tag/v0.78.0">0.78</a>부터 이미 Fresco의 버전을 3.6.0으로 대응하고 있었다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/3e095d1a-8152-43f5-b602-e850d72292d9/image.png" alt=""></p>
<p>엇 그러면
Fresco는 3.4.0, RN은 0.77 버전부터 16KB 페이지 사이즈를 대응했으니
0.77 버전은 3.4.0으로 해둬야겠다는 생각이 들었다.</p>
<p>왜냐하면 0.77에서 Fresco 3.5.0 혹은 3.6.0 버전으로 하면 혹시 모를 이유로 인해 프로젝트에서 각종 오류가 나오지 않을까 걱정했다..</p>
<p>좀 더 정보를 찾아봤는데
<a href="https://github.com/facebook/react-native/releases/tag/v0.77.0">0.77의 Release 내역</a>에서 커밋한 내용을 보니 0.77에서 3.4.0을 대응하는 내용을 찾을 수 있었다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/e54d2dcb-2540-4d3b-b36c-179495080189/image.png" alt=""></p>
<p>오케이! 테스트를 해보자</p>
<hr>
<h1 id="테스트하기">테스트하기</h1>
<p>이제 내 논리가 제대로 적용되는지 확인해 볼 시간이었다.
그렇게 대단한 건 아니고 내 프로젝트에 Fresco 버전을 바꾸는 것이었다.
현재 나는 0.79.2를 사용하고 있었기 때문에 가장 최신 버전인 3.6.0을 사용해 보았다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/7315a472-f034-4606-9f60-0139a57ad04c/image.png" alt=""></p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/9bd0f2d5-65cc-49db-b1ff-9a5e946303fa/image.png" alt="">
빌드는 잘되었고 난 이대로 PR을 작성하기로 했다.</p>
</blockquote>
<hr>
<h1 id="pr-작성하기">PR 작성하기</h1>
<p>사실 오픈소스에 PR을 작성하는 건 처음이 아니다.
예전에 <a href="https://github.com/ammarahm-ed/react-native-admob-native-ads">Deprecated 된 Repo</a>에 PR을 올렸지만, 당연히 관리를 안 하고 있어 요청이 받아들여지진 못했었다.</p>
<p>하지만 당시에 반응이 좋았다.. 그래서 그런가 더 아쉬웠다...</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/50d172b8-1058-4cf9-bf7e-55cf7a1c550c/image.png" alt=""><img src="https://velog.velcdn.com/images/grit_munhyeok/post/8e940800-f49a-4ce0-9871-04b727ed734f/image.png" alt=""><a href="https://github.com/ammarahm-ed/react-native-admob-native-ads/pull/394#issuecomment-2870057627">망한 PR 구경하러 가기</a></p>
</blockquote>
<p>이번에도 GPT의 도움을 받아 PR을 작성하였다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/6e96a821-d471-47ac-9d9d-c720f3caad51/image.png" alt=""><a href="https://github.com/facebook/react-native-website/pull/4760#event-19457378964">해당 PR 구경하러 가기</a></p>
</blockquote>
<p>아까 찾았던 Release 내용을 링크를 달아가며 왜 0.77에는 3.4.0을 해야 하는지 간략하게 적어두었다.</p>
<p>중간에 CLA sign을 해야 한다는 메타의 피드백을 받아 다시 PR을 작성했었다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/836abff3-7e3c-403a-a575-3614dd253d75/image.png" alt=""></p>
<hr>
<h1 id="결과는">결과는?</h1>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/0b6c4697-a6e8-4a1a-9ace-4fe8f128e86a/image.png" alt=""></p>
<p>고맙다는 인사와 함께 </p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/b5682e4c-8fbd-42a7-9cf6-3bbbe56adde1/image.png" alt=""></p>
<p>다행스럽게도 Merge가 되었다!!
<a href="https://reactnative.dev/docs/0.80/image">React Native Image Component</a>
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/7a534395-05e3-466c-8577-9adcca144b04/image.png" alt="">
3.6.0으로 버전이 바뀐 모습을 볼 수 있다..!
이런 간단한 거라도 오픈소스에 기여해서 기분이 참 뿌듯했다..😁</p>
<hr>
<h1 id="느낀-점">느낀 점</h1>
<p>처음으로 컨트리뷰터가 됐는데 나름 치명적인 부분을 기여한 것 같아서 기분이 좋았고
내가 겪는 문제에서 시작되어서 이렇게 오픈소스 기여까지 할 줄은 몰랐다.</p>
<h2 id="사실">사실...</h2>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/3ece6b2e-9129-4052-9bce-acbd1296aa21/image.png" alt="">
예제 코드 아래쪽에 보면 제때 업데이트되지 않을 수 있다고 명시되어있다.. 하핫 <del>(머쓱)</del>
그래도 처음 하는 사람들한테는 도움이 되지않았을까..?</p>
<p>다음에는 꼭 React Native CLI나 FastAPI 같은 내가 자주 쓰는 곳에 PR을 올려보고 싶다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[여러 개의 알림을 하나로 묶어서 보내기]]></title>
            <link>https://velog.io/@grit_munhyeok/%EC%97%AC%EB%9F%AC-%EA%B0%9C%EC%9D%98-%EC%95%8C%EB%A6%BC%EC%9D%84-%ED%95%98%EB%82%98%EB%A1%9C-%EB%AC%B6%EC%96%B4%EC%84%9C-%EB%B3%B4%EB%82%B4%EA%B8%B0</link>
            <guid>https://velog.io/@grit_munhyeok/%EC%97%AC%EB%9F%AC-%EA%B0%9C%EC%9D%98-%EC%95%8C%EB%A6%BC%EC%9D%84-%ED%95%98%EB%82%98%EB%A1%9C-%EB%AC%B6%EC%96%B4%EC%84%9C-%EB%B3%B4%EB%82%B4%EA%B8%B0</guid>
            <pubDate>Thu, 26 Dec 2024 18:39:24 GMT</pubDate>
            <description><![CDATA[<h4 id="github-actions-status"><strong>Github Actions Status</strong></h4>
<p><a href="https://github.com/munhyok/gamlendar_fcm/actions/workflows/scheduled_workflow.yml"><img src="https://github.com/munhyok/gamlendar_fcm/actions/workflows/scheduled_workflow.yml/badge.svg" alt="Gamlendar Game Notification Workflow"></a></p>
<hr>
<h1 id="시작하기-전에">시작하기 전에</h1>
<p>겜린더에는 사용자가 출시 예정인 게임을 달력에 등록하여 출시 당일 알림을 주는 기능이 있습니다.
저는 이 기능을 구현하기 위해 Firebase에서 <a href="https://firebase.google.com/docs/cloud-messaging/js/topic-messaging?hl=ko">Topic</a> 이란 기능을 활용했는데요</p>
<hr>
<h2 id="firebase-cloud-messaging---topic">Firebase Cloud Messaging - Topic</h2>
<p>Topic은 주제 별로 알림을 전송할 클라이언트를 분류하여
효율적으로 여러 기기에 알림을 전송할 수 있는 기능입니다.
마치 pub / sub 구조와 비슷하다고 생각이 드네요</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/d1cf543c-c84e-4e80-b5f1-d36ee09b292a/image.png" alt=""></p>
<hr>
<h1 id="문제">문제</h1>
<h2 id="초기-구조">초기 구조</h2>
<p>어떤 것을 Topic으로 잡아야 할지 고민을 한 결과 게임 정보마다 있는 ID로 주제를 잡으면 될 것이라고 판단하였습니다.</p>
<p>그래서 알림 구조를 만들어보았는데요
<strong>과정은 파란색 → 회색 → 빨간색 순서로 봐주시면 됩니다.</strong>
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/3bf72e5f-2934-40d9-b786-78fbac570757/image.png" alt=""></p>
<p>예상 결과물에 보면 알 수 있듯이 2024년 12월 8일에 출시하는 게임 4가지가 각각 알림이 나오는 걸 확인할 수 있습니다.</p>
<p>이 방식의 장단점이 있었는데요</p>
<h3 id="장점">장점</h3>
<ul>
<li>개발하기 편하다...</li>
<li>DB를 사용할 필요가 없다.</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>각각 알림이 오기 때문에 중복되는 알림으로 인한 알림 폭탄이 예상된다.</li>
</ul>
<hr>
<h1 id="알림-폭탄">알림 폭탄?</h1>
<p>예상 결과물을 보게 되면 겉보기엔 문제가 없어 보입니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/452da659-36e1-4e75-9d1b-27918f8c9bde/image.png" alt=""></p>
<p>하지만 만약 게임을 10개 이상 달력에 등록하게 되면</p>
<blockquote>
<p><strong>3~4개는 괜찮아도 10개가 한 번에 온다....? 이거 거의 알림 폭탄인데..?</strong></p>
</blockquote>
<p>라는 생각과 함께 개선해야한다는 결론에 도달하게 되었습니다.</p>
<hr>
<h1 id="해결-방안">해결 방안</h1>
<p>문제를 해결하기 위해 저는 사용자가 달력에 등록한 정보를 DB에서 불러와 하나의 알림으로 묶어서 보내야겠다는 판단을 하였습니다.</p>
<p>일단 Topic의 기준을 유저 ID로 변경하였고 이를 기반으로 알림 구조를 다시 만들어보았습니다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/c6403e38-c2c8-4ef4-9518-1ae54a7c911f/image.png" alt=""></p>
<p>플로우를 번호 순서대로 정리를 해봤는데요</p>
<h2 id="순서-정리">순서 정리</h2>
<h3 id="1-로그인">1. 로그인</h3>
<p>달력에 등록하기 위해선 사용자는 로그인을 해야 합니다.</p>
<h3 id="2-로그인-성공-시-access-token-발급">2. 로그인 성공 시 Access Token 발급</h3>
<p>로그인 성공을 하면 Access Token을 발급받는데요 그곳에 <strong>유저 id</strong>가 존재합니다.
<strong>예시로 &quot;id&quot;:&quot;00000000&quot;로 하겠습니다.</strong></p>
<h3 id="3-유저-id로-fcm-topic-구독">3. 유저 ID로 FCM Topic 구독</h3>
<p>로그인 성공과 동시에 id를 이용해 FCM Topic을 구독하게 됩니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/bb7fae06-84e4-43b7-a0fa-9bd4504deea2/image.png" alt=""></p>
<h3 id="4-게임을-자유롭게-탐색">4. 게임을 자유롭게 탐색</h3>
<p>말 그대로 게임을 자유롭게 탐색을 합니다.</p>
<h3 id="5-탐색-중-맘에-드는-게임을-달력에-등록-시-mongodb와-redis에-저장">5. 탐색 중 맘에 드는 게임을 달력에 등록 시 MongoDB와 Redis에 저장</h3>
<p>게임이 맘에 들었다면 Redis는 HASH 타입으로 저장, MongoDB에는 게임 id가 저장이 됩니다.</p>
<p>Redis에 저장되는 데이터를 활용하기 때문에 Redis를 중점적으로 보자면
날짜 - 유저 ID - 게임1, 게임2 이런식으로 저장이 되는데요</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/e694c2d5-2caf-4d56-bc23-c6def0ed0914/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/c1c65092-b90d-4609-8db9-2cbc227d2f6c/image.png" alt=""></p>
<p>이를 기반으로 실제 결과물을 보게 되면 아래와 같은 이미지가 나오게 됩니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/05d8220f-883f-40a5-b50f-063ea279ef39/image.png" alt="">
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/61684e51-766a-401d-b066-43daa4a610c1/image.png" alt=""></p>
<p>날짜와 유저 ID를 기반으로 HASH 타입으로 저장이 되어있고 유저 ID 속에는 유저가 저장한 게임들이 저장됩니다.</p>
<h3 id="6-bot이-실행되는-날짜-기준으로-게임을-예약한-유저-id-불러오기">6. BOT이 실행되는 날짜 기준으로 게임을 예약한 유저 ID 불러오기</h3>
<p>오늘 날짜에 맞춰 달력에 게임을 등록한 유저 ID를 불러옵니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/b2473dff-46c1-4d64-8488-0da561b0dc17/image.png" alt=""></p>
<h3 id="7-유저-id-별-게임-목록을-하나의-알림으로-그룹화">7. 유저 ID 별 게임 목록을 하나의 알림으로 그룹화</h3>
<p>게임 목록을 불러오고 message_template에 맞게 메시지를 그룹화 시킵니다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/bc0b1928-fcbb-4887-842f-b5653b8e840f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/8b0b170c-1dc4-485a-b2b8-a52e259106ae/image.png" alt=""></p>
<h3 id="8-유저-id-topic-별로-각각-전부-전송">8. 유저 ID Topic 별로 각각 전부 전송</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/2911f2cc-af53-43ee-90b1-7cd3b1124c5d/image.png" alt=""></p>
<p>여러 개의 출시 알림을 하나의 알림으로 모아서 전송할 수 있게 됩니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/443029ad-d57d-4014-889e-6d55fae18674/image.png" alt=""></p>
<h2 id="개선점">개선점</h2>
<ul>
<li><p>알림 폭탄을 줄일 수 있다.</p>
</li>
<li><p>유저 ID 기반으로 Topic을 구독하였기 때문에 다중 로그인에도 동일한 알림을 전송할 수 있게 되었습니다.</p>
<blockquote>
<p>이전처럼 게임 id를 기반으로 구독하게 되면
여러 기기에 같은 계정으로 로그인 하더라도 A 기기는 전부 알림이 오지만 추후 B 기기에 같은 계정으로 로그인을 하게 되었을 경우
알림이 오지 않아 동일한 사용자 경험을 제공할 수 없게 됩니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/0d4c6422-a9b8-4055-ba89-62728fb0ac8b/image.png" alt=""></p>
</blockquote>
</li>
<li><p>MongoDB와 Redis의 역할을 나눠 I/O를 분산하여 MongoDB의 부담을 줄일 수 있었습니다.</p>
<blockquote>
<p>사실 이런 작업은 Redis 없이도 가능합니다. MongoDB Query를 작성해 분류해서 불러온 후 똑같이 그룹화 시켜 알림을 전송하면 되는데요</p>
<p>하지만 그렇게 하면 쿼리 작업으로 인한 연산이 발생해 안 그래도 느린 자체 서버에 부담을 주기 싫었습니다.</p>
<p>구조적으로 복잡한 데이터가 아니라고 생각했기 때문에 key-value 스토리지인 Redis를 사용하는 것이 좋다고 판단하였습니다.</p>
</blockquote>
</li>
</ul>
<hr>
<h2 id="요즘-알림-그룹화-시켜주는데-사용하지-않은-이유">요즘 알림 그룹화 시켜주는데 사용하지 않은 이유</h2>
<p>요즘 iOS나 Android는 알림을 그룹화 시켜주는 기능이 존재합니다.
근데 이걸 사용하지 않은 이유는 관리하기가 더 힘들 것 같다는 생각이 들었습니다.</p>
<p>안드로이드의 경우 Channel_id로 여러 유형의 알림을 분류하고 그룹화 시켜주는데요. 저는 이미 Topic으로 그 역할을 하고 있다고 생각해 굳이 사용할 필요성을 느끼지 못했습니다.</p>
<p>그래서 channel_id를 하나만 사용하고 있습니다.</p>
<p>그리고 그룹화 시켜줘도 알림은 계속 울린 후에 그룹화가 되기 때문에
저는 알림 1번으로 오늘 어떤 게임이 출시하는지 유저에게 알려주고 싶었습니다.</p>
<hr>
<h2 id="나는-github-actions를-쓸-줄-몰라요-라고-한다면">나는 Github Actions를 쓸 줄 몰라요! 라고 한다면...</h2>
<p>라즈베리파이 같은 간단한 보드사셔서 우분투 설치하고 환경 구성하신 후 Crontab 같은 걸로 스케줄링하면 좋습니다.</p>
<p><del>사실 Github Actions로 하는 것보다 더 나을 수도...</del>
저도 사실 지금 운영하고 있는 서버 PC에서 crontab을 설정해 운영할 고민도 했지만 학습 목적으로 Github Actions를 이용했습니다.</p>
<hr>
<h1 id="마치며">마치며</h1>
<p>겜린더 프로젝트를 하면서 제한된 하드웨어 성능에 최대한 뽕을 뽑아보려고 노력하고 있는데요...
제 스스로 판단하기엔 나름 타당한 근거로 이번 알림 시스템을 잘 개선했다고 생각합니다.</p>
<p>알림 시스템은 여전히 잘 동작하고 있고 앞으로도 안정적으로 운영하려고 최대한 노력해 보려 합니다.</p>
<p>혹시 궁금하신 점이 있다면 언제든 댓글 남겨주세요 열심히 답글 올리겠습니다.</p>
<p>긴 글 읽어주셔서 감사합니다. 🙇‍</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[겜린더 출시 후기]]></title>
            <link>https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%EC%B6%9C%EC%8B%9C-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%EC%B6%9C%EC%8B%9C-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Fri, 20 Dec 2024 05:53:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>게임이 언제 출시하는지 사이트 들어가고 유튜브로 일일이 찾으셨나요?
겜린더로 한 번에 찾고 달력에 저장하세요!</p>
</blockquote>
<p>📋 스팀, 플레이스테이션, 엑스박스, 닌텐도 스위치의 출시 예정 게임을 한눈에 보세요!</p>
<blockquote>
</blockquote>
<p>📆 출시 예정 게임을 캘린더에 등록할 수 있어요!</p>
<blockquote>
</blockquote>
<p>🔔 매일 아침마다 오늘 어떤 게임이 출시되었는지 알려줘요!</p>
<blockquote>
</blockquote>
<p>👾 만약 내가 인디게임 개발자라면 겜린더에 무료로 게임을 등록할 수 있어요! (모바일 게임도 가능해요)</p>
<blockquote>
</blockquote>
<p><a href="https://play.google.com/store/apps/details?id=com.mhkang.gamlendar"><strong>구글 플레이</strong></a></p>
<blockquote>
<p><a href="https://apps.apple.com/kr/app/%EA%B2%9C%EB%A6%B0%EB%8D%94/id6738235706"><strong>앱 스토어</strong></a></p>
</blockquote>
<blockquote>
<p><strong>Github Repo 모음</strong>
겜린더 백엔드 : <a href="https://github.com/munhyok/Gamlendar_BE_Local">링크</a>
겜린더 게임 수집 봇 : <a href="https://github.com/munhyok/Gamlendar_Selenium">링크</a>
겜린더 게임 출시 알림 자동화 : <a href="https://github.com/munhyok/gamlendar_fcm">링크</a></p>
</blockquote>
<blockquote>
<p><strong>관련 글 (수시로 업데이트됩니다)</strong>
<a href="https://velog.io/@grit_munhyeok/%EC%97%AC%EB%9F%AC-%EA%B0%9C%EC%9D%98-%EC%95%8C%EB%A6%BC%EC%9D%84-%ED%95%98%EB%82%98%EB%A1%9C-%EB%AC%B6%EC%96%B4%EC%84%9C-%EB%B3%B4%EB%82%B4%EA%B8%B0">여러 개의 알림을 하나로 묶어서 보내기</a> - <strong>2024-12-27</strong> </p>
</blockquote>
<blockquote>
<p>개발하면서 어려웠던 부분이나 기술적인 부분은 분량이 많을 것 같아
이번 글에선 간략하게 요약했습니다.</p>
</blockquote>
<hr>
<h1 id="리뉴얼">리뉴얼</h1>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/ba44126c-b11e-4a01-b85e-6281fe8fd3b1/image.png" alt=""></p>
<p>겜린더가 드디어 다시 출시되었습니다.
<a href="https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-2.0.0-%EA%B0%9C%EB%B0%9C-%ED%9B%84%EA%B8%B0">겜린더 2.0.0 개발 후기</a>를 작성한 지 벌써 3년이 지났네요</p>
<p>3년 전의 나와 지금 나의 차이점을 한줄로 설명하자면:</p>
<blockquote>
<p>이제야 조금은 개발자다워진 것 같습니다.</p>
</blockquote>
<p>하지만 여전히 제 자신의 부족함을 다시 알 수 있었던 프로젝트였습니다.</p>
<hr>
<h1 id="포기하기엔-너무-멀리-와버린-프로젝트">포기하기엔 너무 멀리 와버린 프로젝트</h1>
<p>&quot;3년이나 걸릴 이유가 있나?&quot;라고 생각하셨다면, 맞는 말입니다.
이 프로젝트가 3년이나 걸릴 일은 죽어도 아니었죠.</p>
<p>작년에는 아예 집중을 하지 못했었습니다. 번아웃이 왔었거든요...
3년이지만 실질적인 개발 기간은 1년 되는 것 같네요...</p>
<p>사실, 겜린더는 출시 후에 그만두려던 프로젝트였습니다.
다른 사이드 프로젝트도 시도해보고 싶었지만, 겜린더는 계속 제 머릿속에서 떠나질 않았습니다.</p>
<p>그리고 솔직히, 저렇게 어정쩡한 앱을 출시하고 나니 스스로도 부끄러웠습니다.
어디 가서 내가 개발했다고 말하기조차 어려울 것 같았죠.</p>
<p>마지막으로, 누군가에게 정말 도움이 되는 제대로 된 서비스를 만들어보고 싶었습니다.</p>
<hr>
<h1 id="슬로건-변경">슬로건 변경</h1>
<p>겜린더의 초기 슬로건은
<strong>&quot;달력으로 알아보는 게임 정보&quot;</strong>였습니다.</p>
<p>즉, <strong>&quot;어떤 게임이 출시하는지 달력을 통해 알아보고, 출시 당일 알림을 받아보자!&quot;</strong>를 목표로 했습니다.</p>
<p>하지만 제가 구현하고 싶었던 본질적인 가치를 더 잘 담아내고 싶었고, 친구의 피드백도 큰 도움이 되었습니다.
그 결과, 슬로건을 <strong>&quot;나만의 게임 캘린더&quot;</strong>로 바꾸게 되었습니다.</p>
<p><a href="https://velog.io/@grit_munhyeok/%ED%9A%8C%EA%B3%A0%EB%A1%9D-%EA%B2%B8-%EC%9D%BC%EA%B8%B0#%EC%8A%AC%EB%A1%9C%EA%B1%B4-%EB%B3%80%EA%B2%BD">슬로건 변경했던 상세한 이야기</a></p>
<hr>
<h1 id="디자인은-정말-어렵다">디자인은 정말 어렵다</h1>
<p>겜린더는 정말 많은 디자인 변천사를 겪었습니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/ec2bc27a-098b-4a3c-b5da-39ca9d01efff/image.png" alt=""></p>
<p>자세한 내용은
<a href="https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A0%9C%EC%9E%91%EA%B8%B0-1">겜린더 프론트엔드 제작기 1</a>에서 확인하실 수 있습니다.</p>
<p>하지만 기존 디자인은 슬로건 변경과 함께 대폭 수정되었습니다.</p>
<p>항상 겜린더의 UI/UX에 피드백을 주던 친구가 이번에는 직접 UI를 리디자인해 주었죠.</p>
<p>덕분에 UX가 크게 개선되었고, 더 간결하고 깔끔한 UI를 완성할 수 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/5c1e51d2-4b82-4128-95c7-46adfde1202e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/f2d3dadd-5a12-4ded-ba66-459404b40247/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/767fac5a-18c3-4d14-990f-8ed9909f91fc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/0b08705e-d3c0-409d-a41f-c8ab25820e41/image.png" alt=""></p>
<p>사실 개발이 지연된 가장 큰 이유 중 하나가 UI/UX 디자인이었습니다.</p>
<p>하지만 친구가 도와준 이후로는 디자인 걱정을 덜고 개발에만 집중할 수 있었습니다.
그 결과, 개발이 훨씬 순탄하게 진행되었죠.</p>
<p>친구가 디자인을 완성해 준 덕분에, 저는 이를 기반으로 API 명세서를 작성하고 본격적으로 개발에만 몰두할 수 있었습니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/f88f4848-b06f-49ed-a699-2746cc4b11e4/image.png" alt=""></p>
<p><strong>이 글을 빌려 친구에게 다시 감사하다는 말 전합니다.</strong></p>
<hr>
<h1 id="느낀점">느낀점</h1>
<p>이번에는 정말 제대로 된 앱을 만들고 싶다는 마음으로, 설계부터 더 열심히 고민하고 작업했습니다.</p>
<p>아직 부족한 점이 많지만, 본질적인 부분을 안정적으로 구현해낼 수 있어서 정말 기뻤습니다.</p>
<p>삽질했던 부분들은 하나씩 정리하며, 따로 글로 남길 예정입니다.</p>
<p>겜린더는 여기서 끝이 아니라 이제 시작입니다.
본질적인 기능을 구현한 지금부터가 진짜라고 생각하며, 게이머가 애용하는 앱으로 거듭나기 위해 최선을 다해볼 생각입니다.</p>
<p>지금까지 받은 수많은 피드백을 바탕으로 새로운 기능을 정리했고,
그 기능들을 단순히 추가하는 것에 그치지 않고, UI/UX에 어떻게 자연스럽게 녹여낼지 고민해야 할 것 같습니다.</p>
<hr>
<h1 id="서비스-리뷰">서비스 리뷰</h1>
<h2 id="개요">개요</h2>
<p>개발 과정에서 중점적으로 고려한 주요 포인트는 다음과 같습니다:</p>
<ol>
<li><p>출시 예정 게임의 수 부족 문제 해결
→ 이용자가 더 다양한 게임 정보를 확인할 수 있도록 개선</p>
</li>
<li><p>회원가입 및 로그인 기능 구현
→ 사용자 편의성과 보안을 모두 고려</p>
</li>
<li><p>푸시 알림(FCM)의 재설계
→ 알림 폭탄을 방지하기 위한 체계적인 설계</p>
</li>
<li><p>페이징(Pagination) 구현
→ 자유로운 게임 탐색 경험 제공과 함께 서버 부하 최소화</p>
</li>
<li><p>VPN을 활용한 보안 강화
→ 자체 서버 운영 환경에서 데이터를 안전하게 보호</p>
</li>
</ol>
<h2 id="사용한-스택">사용한 스택</h2>
<h3 id="기술-스택">기술 스택</h3>
<p><strong>Front-End</strong></p>
<ul>
<li>React Native</li>
<li>Axios</li>
<li>React Query</li>
</ul>
<p><strong>Back-End</strong></p>
<ul>
<li>Docker</li>
<li>Nginx</li>
<li>FastAPI</li>
<li>MongoDB</li>
<li>Redis</li>
<li>Firebase Cloud Message(FCM)</li>
</ul>
<p><strong>게임 수집 봇</strong></p>
<ul>
<li>Selenium</li>
<li>MariaDB</li>
<li>Pandas</li>
</ul>
<p><strong>자동화</strong></p>
<ul>
<li>Github Action</li>
</ul>
<hr>
<h3 id="프로젝트-관리">프로젝트 관리</h3>
<ul>
<li>Notion</li>
<li>Figma</li>
</ul>
<hr>
<h1 id="실제-서비스-화면">실제 서비스 화면</h1>
<h2 id="front-end-주요-기능">Front-End 주요 기능</h2>
<h3 id="1-자유로운-게임-탐색-및-제스쳐를-이용한-빠른-달력-등록">1. 자유로운 게임 탐색 및 제스쳐를 이용한 빠른 달력 등록</h3>
<p>페이징을 통해 데이터 로딩을 최적화했습니다.
React Native에서는 FlatList보다 성능이 뛰어난 <a href="https://github.com/Shopify/flash-list">FlashList</a> 라이브러리를 사용했습니다.
스크롤 시 성능 차이가 명확히 드러나 만족스러웠습니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/283493d0-2759-4b94-a940-cec72693d2d4/image.webp" alt=""></p>
<hr>
<h3 id="2-로그인회원가입">2. 로그인/회원가입</h3>
<p>회원 가입 절차를 직관적으로 만들기 위해 UX에 집중했습니다.
상단의 진행 바를 통해 단계별로 진행 상황을 쉽게 확인할 수 있도록 설계했습니다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/cf450396-23ca-4eef-aa52-5b59a83b9c2a/image.png" alt="">
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/46db16f0-cbff-4c89-b507-6892fccde7ee/image.png" alt=""></p>
<p>또한, 가입 단계별로 필요한 UI를 컴포넌트화하여 효율적으로 재사용할 수 있게 구성했습니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/9463de64-8ed6-4a5e-b6fe-14b593f4cb25/image.png" alt=""></p>
<hr>
<h3 id="3-설정페이지">3. 설정페이지</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/903aa35b-7bda-4fa4-b4d0-d38e8740ac76/image.png" alt=""></p>
<hr>
<h3 id="4-푸시-알림-받기">4. 푸시 알림 받기</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/4740106b-86eb-4f75-8440-bd1a97d4ca7c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/3e1c9476-0b19-4c81-87f7-9bfb0c6aa703/image.png" alt=""></p>
<hr>
<h2 id="back-end">Back-End</h2>
<p>백엔드 개발 과정에서 여러 문제를 만나며 많은 것을 배울 수 있었습니다.
각 기능별로 어떤 문제를 겪었고, 어떻게 해결했는지를 정리한 글을 차근차근 작성해 나갈 예정입니다.</p>
<p>이 글에선 간략하게 설명만 하고 마치겠습니다.</p>
<p>아래 이미지는 전체적인 구상도입니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/901ea40d-f614-4884-bde8-e1ae2a110811/image.png" alt=""></p>
<hr>
<h3 id="jwt를-이용한-로그인">JWT를 이용한 로그인</h3>
<p>Redis를 활용한 효율적인 JWT 로그인 구현</p>
<h4 id="1-로그인-프로세스">1. 로그인 프로세스</h4>
<ul>
<li>사용자가 로그인 시 Access Token과 Refresh Token이 발급됩니다.
Refresh Token을 이용해 Access Token을 재발급받을 경우, Refresh Token도 새로 생성하여 보안을 강화합니다.</li>
</ul>
<h4 id="2-멀티-로그인-관리">2. 멀티 로그인 관리</h4>
<ul>
<li>한 계정으로 여러 기기에서 로그인할 수 있도록, 각 기기에 UUID를 생성하여 구분.
Redis에 계정ID:UUID 형태로 Refresh Token을 저장하여 기기별로 독립적으로 관리합니다.</li>
</ul>
<h4 id="3-redis-사용-이유">3. Redis 사용 이유</h4>
<ul>
<li>Expire 기능: Refresh Token의 만료 시간을 Redis에서 자동으로 관리하여 유지보수 편리.
기존 겜린더에서 Redis를 사용하고 있었기에 자연스럽게 통합.
빠른 읽기/쓰기 성능으로 다중 기기 로그인 시 효율적인 관리 가능.</li>
</ul>
<h4 id="4-구현-결과">4. 구현 결과</h4>
<ul>
<li>보안성을 강화하면서도 다중 기기 지원이 가능해졌으며, Redis를 통해 Token 관리의 복잡성을 줄이고 성능을 최적화할 수 있었습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/c6d0e790-4e45-4866-85d8-eaf3be748cc4/image.png" alt=""></p>
<hr>
<h3 id="푸시-알림-fcm">푸시 알림 (FCM)</h3>
<p>Firebase Cloud Messaging(FCM)을 이용해 <strong>게임 출시 알림</strong>을 제공합니다.
게임 출시 알림은 Github Actions로 매일 오전 9시에 자동으로 실행되는데요
Github-Hosted runners를 이용하기 때문에 좀 느리긴 하지만 오류 없이 정상적으로 작동 중이라 만족하고 있습니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/f65fa1b5-605e-4262-9773-70e20af37de8/image.png" alt=""></p>
<p>특히 알림 폭탄 문제를 해결하기 위해 설계를 최적화하고 사용자 경험을 개선했습니다.</p>
<p>아래 이미지는 당시 어떻게 구현할지 구상했던 이미지입니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/5d80a4dc-61ec-4da0-805e-710cca731024/image.png" alt=""></p>
<blockquote>
<p><strong>2024-12-27 업데이트</strong>
푸시 알림 관련해서 개선한 글을 작성하였습니다.
자세한 내용은 하단 글을 통해 확인해주시면 감사하겠습니다.
<a href="https://velog.io/@grit_munhyeok/%EC%97%AC%EB%9F%AC-%EA%B0%9C%EC%9D%98-%EC%95%8C%EB%A6%BC%EC%9D%84-%ED%95%98%EB%82%98%EB%A1%9C-%EB%AC%B6%EC%96%B4%EC%84%9C-%EB%B3%B4%EB%82%B4%EA%B8%B0">여러 개의 알림을 하나로 묶어서 보내기</a></p>
</blockquote>
<h3 id="배포-자동화">배포 자동화</h3>
<p>배포 자동화는 그냥 큰 리소스를 쓰고 싶지 않아 간단하게 CI/CD 파이프라인을 만들어 보았습니다.
Github Actions를 사용하여 코드가 푸시될 때마다 자동으로 빌드하고 배포하는 방식입니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/7e35d6a0-8f39-42bd-aae1-8fd882f2021b/image.png" alt=""></p>
<hr>
<h3 id="vpn을-이용한-보안-강화">VPN을 이용한 보안 강화</h3>
<h4 id="개인-서버-어떻게-안전하게-운영할까">개인 서버, 어떻게 안전하게 운영할까?</h4>
<p>겜린더는 현재 개인 서버에서 운영되고 있습니다. 개인 서버는 외부 공격에 취약하기 때문에, 이를 보완하기 위해 VPN을 적극적으로 활용했습니다.</p>
<h4 id="데이터베이스-외부에서-막아야-안전하다">데이터베이스, 외부에서 막아야 안전하다</h4>
<p>MongoDB와 Redis 같은 중요한 데이터베이스는 외부에서의 직접 연결을 완전히 차단했습니다.
만약 데이터베이스에 직접 접근해야 할 일이 있다면, VPN을 통해서만 접속할 수 있도록 설정했습니다.</p>
<h4 id="swagger-문서도-아무나-못-보게">Swagger 문서도 아무나 못 보게</h4>
<p>FastAPI의 Swagger 문서 페이지는 기본적으로 개발 편의를 위해 제공되지만, 이 역시 VPN에 연결된 사용자만 접근 가능하도록 제한했습니다.
추가로, IP Whitelist를 만들어 특정 IP만 접속할 수 있도록 보안을 한층 더 강화했습니다.</p>
<h4 id="보안은-선택이-아닌-필수-안-그럼-큰일-난다">보안은 선택이 아닌 필수... 안 그럼 큰일 난다...</h4>
<p>VPN과 IP Whitelist 덕분에 서버 접근 경로를 최소화할 수 있었고, 외부 위협으로부터 서버를 한층 더 안전하게 보호할 수 있었습니다.
작은 서버라도 보안에 신경 쓴다면, 예상치 못한 문제를 미리 방지할 수 있습니다!</p>
<hr>
<h2 id="게임-수집-봇">게임 수집 봇</h2>
<p>게임 데이터 수집과 관련해 이전에 작성한 두 가지 글이 있습니다:</p>
<ul>
<li><a href="https://velog.io/@grit_munhyeok/%EC%95%BD-2300%EA%B0%9C%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%88%98%EC%A7%91%ED%95%B4-DB%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0">약 2300개의 게임을 수집해 DB에 저장하기</a></li>
<li><a href="https://velog.io/@grit_munhyeok/%EA%B2%8C%EC%9E%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%88%98%EC%A7%91-%EB%B4%87-%EB%AC%B8%EC%A0%9C%EC%A0%90-%EB%B0%8F-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0">게임 데이터 수집 봇 파이프라인 개선하기</a></li>
</ul>
<hr>
<h1 id="마치며">마치며</h1>
<h2 id="아쉬웠던-점">아쉬웠던 점</h2>
<ul>
<li>아직 코드를 완벽하게 작성하지 못한 점이 아쉬웠습니다.</li>
<li>백엔드에서 응답을 통일한 뒤, 프론트엔드에서 코드 재사용성을 높이려 했으나, 급하게 개발하다 보니 이를 구현하지 못한 점이 아쉬웠습니다.</li>
<li>올해 안에 출시하고자 급하게 진행하다 보니, 출시 초기에는 버그가 많았고, 넣지 못한 기능도 있었습니다.</li>
<li>생각보다 구현한 기능에 비해 보이는 부분이 적어 아쉬움이 남습니다. 😂</li>
<li>기초 지식은 여전히 부족하고 더 공부해야겠다는 마음가짐을 가질 수 있게 되었습니다.</li>
</ul>
<h2 id="만족했던-점">만족했던 점</h2>
<p>혼자서 서비스를 만들어본 경험이 정말 값졌습니다.
스스로 실력에 대한 의심이 많았지만, 이번 경험을 통해 많은 의문이 해결되었고, 단순히 코드를 작성하는 사람이 아니라 문제를 파악하고 해결할 수 있는 능력을 갖춘 개발자로 성장했다는 점에서 뿌듯함을 느낍니다.</p>
<h2 id="앞으로-계획">앞으로 계획</h2>
<p>이제 취업 준비를 본격적으로 시작할 생각입니다. CS, 자료구조, 코딩 테스트, SQL 등 기초부터 다시 복습하며 실력을 쌓고자 합니다. 이 기회가 마지막으로 기초를 제대로 다질 수 있는 시간이 아닐까 싶습니다. 겜린더는 주말마다 업데이트를 위해 꾸준히 개발할 예정입니다.</p>
<blockquote>
<p><strong>Github Repo 모음</strong>
겜린더 백엔드 : <a href="https://github.com/munhyok/Gamlendar_BE_Local">링크</a>
겜린더 게임 수집 봇 : <a href="https://github.com/munhyok/Gamlendar_Selenium">링크</a>
겜린더 게임 출시 알림 자동화 : <a href="https://github.com/munhyok/gamlendar_fcm">링크</a></p>
</blockquote>
<p>읽어주셔서 감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[게임 데이터 수집 봇 파이프라인 개선하기]]></title>
            <link>https://velog.io/@grit_munhyeok/%EA%B2%8C%EC%9E%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%88%98%EC%A7%91-%EB%B4%87-%EB%AC%B8%EC%A0%9C%EC%A0%90-%EB%B0%8F-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@grit_munhyeok/%EA%B2%8C%EC%9E%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%88%98%EC%A7%91-%EB%B4%87-%EB%AC%B8%EC%A0%9C%EC%A0%90-%EB%B0%8F-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 15 Jul 2024 10:23:15 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@grit_munhyeok/%EC%95%BD-2300%EA%B0%9C%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%88%98%EC%A7%91%ED%95%B4-DB%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0">전에 만들었던 데이터 수집 봇</a>을 테스트 하고있었는데
생각보다 많은 오류와 불편한 점이 발견 되어 이를 개선시키기 위해 다시 좀 들여다보았다.</p>
<hr>
<h1 id="문제">문제</h1>
<p>기존 파이프라인을 먼저 보자</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/8de215ef-5497-4244-8c82-ac49c4780fe2/image.png" alt=""></p>
</blockquote>
<p>이런 과정인데
여러 번 테스트를 거치면서 여러 문제점이 있었다.</p>
<h2 id="1-데이터-파편화-발생">1. 데이터 파편화 발생</h2>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/461b64e8-5651-4e5e-94cd-0287c67ca7c0/image.png" alt=""></p>
<ol>
<li>플랫폼마다 수집</li>
<li>CSV로 백업 파일 생성</li>
<li>RDB(MariaDB)로 insert하고 다음 플랫폼 수집을 진행하였다.</li>
</ol>
<p>위와 같은 프로세스가 진행되어 다음 플랫폼으로 넘어갔다고 가정해보자</p>
<p>Playstation에서 데이터를 수집하는데 오류가 발생되어 만약 프로그램이 종료된다면..?
이전에 수집했던 데이터는 더미데이터로 남게되어버린다.</p>
<p>덕분에 데이터를 관리하는데 어려운 측면이 있었다.
실패하면 그 더미데이터를 수동으로 지워야했기 때문이다.</p>
<p>생각보다 별 거 아니겠거니 했는데 데이터가 쌓이다보니 관리가 너무 힘들었다.</p>
<h2 id="2백업-데이터-저장-방식의-문제">2.백업 데이터 저장 방식의 문제</h2>
<p>기존 백업 데이터를 저장할 때
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/3f9f6446-1888-4573-8384-7e7020f3e59e/image.png" alt=""></p>
<p>생성 날짜와 시간을 함께 사용해서 저장했는데
이게... 음... 나쁜건 아닌거같은데 좀 가독성이나 보기 불편한 기분이었다.
그래서 타임스탬프를 써서 저장하는게 낫다고 판단하였다.</p>
<p>생성 날짜와 시간을 전역 변수로 생성을 했었는데
이번엔 Database 클래스에 인스턴스 생성 시 같이 생성할 예정이다.</p>
<hr>
<h1 id="개선">개선</h1>
<p>위의 문제점을 인지 후 개선한 버전이다.</p>
<h2 id="개선된-v2-버전">개선된 V2 버전</h2>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/8259a273-a0be-4e9b-81ba-b3cb000befdb/image.png" alt=""></p>
<h3 id="비교해보자">비교해보자</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/be77c5c8-c160-43db-9af4-03b4a4d2259d/image.png" alt=""></p>
<h3 id="1-영역을-명확하게-분류">1. 영역을 명확하게 분류</h3>
<p>Init, Collection, Upload 영역으로 명확하게 프로세스를 분류하였다.</p>
<h3 id="2-수집-후-일괄적-업로드">2. 수집 후 일괄적 업로드</h3>
<p>기존에는 한 플랫폼 수집하자마자 DB로 Insert 과정을 거쳤지만
이번엔 Steam, Playstation, Xbox, Switch 이 4개의 데이터를 전부 수집하지 못하면 Upload 프로세스로 넘어가지 못하게 하였다.</p>
<p>덕분에 일관성 있는 데이터와 더미데이터를 줄일 수 있을 것으로 기대한다.</p>
<h3 id="3-병렬-프로그래밍을-고려한-설계">3. 병렬 프로그래밍을 고려한 설계</h3>
<p>사실 Collection 영역을 만든 건 추후 병렬 프로그래밍을 통한 수집 속도 개선을 하기 위한 설계다.</p>
<p>기존 V1에서 만약 병렬 프로그래밍을 한다고 해보자
Steam, Playstation, Xbox, Switch 데이터를 동시에 수집하고
MariaDB에 insert를 하는 과정이 발생한다고 하면</p>
<p>만약 Switch 데이터가 먼저 수집이 되어 insert를 하고있는 중간에 Xbox가 데이터를 수집해서 insert를 동시에 한다고 가정해보자 그렇게 되면
<strong>성능 저하</strong>도 우려되고 <strong>동시성 문제</strong>나 <strong>데드락</strong>이 걸릴 수도 있다.</p>
<p>이를 방지하기 위해서 Collection을 영역을 나눠
Collection은 병렬로 데이터 수집을 진행한 다음 순차적으로 DB에 insert를 하는 것이
구조적으로 보나 확장성면에서 더 유리할 것으로 판단하였다.</p>
<hr>
<h1 id="xbox-번들-탐지-개선">Xbox 번들 탐지 개선</h1>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/99bef7a1-c458-4cc2-b3bb-fff7d3933c55/image.png" alt="">
이번에 Xbox 번들 탐지 알고리즘도 개선하였다.
사실 상 새로 만들었다고 하는게 맞다.</p>
<h2 id="패턴-분석">패턴 분석</h2>
<p>개선하기 위해서
일단 상품이 어떤식으로 나오는지 패턴을 분석했다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/80b92781-95c2-44bf-b3d6-75fffe4b7c6d/image.png" alt="">
Xbox는 이렇게 Edition 같은 패키지 상품도 출시 예정 리스트에 올리는데</p>
<p>상세 페이지는 &quot;포함됨&quot; 혹은 &quot;이 번들&quot; 영역이 존재한다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/19449155-1609-4769-a2db-36a965f388bd/image.png" alt="">
하지만 인식해야할 건 &quot;이 번들&quot; 영역이다.</p>
<blockquote>
</blockquote>
<p>공통점 -&gt; 모든 오리지널 게임들은 다 첫번째에 배치되어있다.(라고 생각했으나 아니였다.)
“이 번들” -&gt; 인식 대상
“포함됨&quot; -&gt; 인식 대상 X (대부분 DLC)</p>
<p>&quot;이 번들&quot;에 있는 오리지널 게임으로 Redirect 하는 것이 목표이다.</p>
<p>이 번들 영역은 일단 분석해 본 결과 4가지 패턴이 존재하였다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/6d6dcb1e-2ae0-403d-8acf-90d7267bd268/image.png" alt="">
이렇게 4가지 패턴으로 존재하는데
오리지널 게임은 보통 게임보기, 가격 형태로 표시가 된다는 것이었다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/e265e99f-222a-4de8-836a-1e1a4254497b/image.png" alt=""></p>
<p>그리고 만약 가격+게임보기처럼 동시에 존재하는 형태인 경우
가격이 붙은 상품이 오리지널 게임이었다.</p>
<p>이런 패턴을 분석하여 솔루션을 정리하였다.</p>
<h2 id="솔루션">솔루션</h2>
<ol>
<li><p>게임보기 혹은 가격이 있으면 그건 원본 게임일 확률이 높다.</p>
</li>
<li><p>N번째 게임을 순차적으로 수집해 패턴과 일치하는지 확인 후 그 곳으로 페이지를 넘긴다.</p>
</li>
<li><p>만약 첫번째에서 패턴이 일치하지 않으면 두번째 세번째 넘어가면서 순차적으로 패턴 분석</p>
</li>
</ol>
<blockquote>
<p><strong>알고리즘</strong> <img src="https://velog.velcdn.com/images/grit_munhyeok/post/e4e385ee-d137-4a6d-a7d2-69377c568cee/image.png" alt=""></p>
</blockquote>
<blockquote>
<p><strong>코드</strong> <img src="https://velog.velcdn.com/images/grit_munhyeok/post/29c2dca1-549d-45bb-b527-6e5fd89728f5/image.png" alt=""></p>
</blockquote>
<blockquote>
<p><strong>패턴 탐지 if문</strong><img src="https://velog.velcdn.com/images/grit_munhyeok/post/1120cf1b-9b25-435e-8b19-0facc78515ca/image.png" alt=""></p>
</blockquote>
<p><a href="https://velog.io/@grit_munhyeok/%EC%95%BD-2300%EA%B0%9C%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%88%98%EC%A7%91%ED%95%B4-DB%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0#%EB%B2%88%EB%93%A4-%ED%83%90%EC%A7%80-%EA%B8%B0%EB%8A%A5">이전 번들 탐지 알고리즘</a></p>
<p>이전 코드와 비교해보자
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/3cedc59f-0e6f-48f3-bab7-68dac56b21d3/image.png" alt=""></p>
<p>이전에 비하면 엄청 단순화 되었고 정확도가 올라갔다!</p>
<p>이전 알고리즘은 제대로 탐지를 못해 이상한 DLC페이지로 넘어갔었다.
이 부분 때문에 이상한 데이터를 수집하거나 프로그램이 터졌었다.</p>
<hr>
<h1 id="마무리">마무리</h1>
<p>다음 V3는 아무래도 병렬 프로그래밍을 통한 수집 속도를 개선할 예정이다.
아마 이 파트는 겜린더가 정식 출시했을 때 할 수 있지 않을까 싶다.</p>
<p>지금은 앱 개발이 더 급하기 때문이다.
개발 중인 부분을 잠깐 보여주자면
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/de21a89f-e30e-4e73-9887-45208ba3c0d0/image.webp" alt="">
빠르게 탐색하고 나만의 겜린더에 빠르게 추가하는 인터랙션을 넣어보고 있다.
얼른 정식 출시해서 사용자 피드백을 많이 들어보고 싶다. 😁</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠키와 세션 그리고 JWT]]></title>
            <link>https://velog.io/@grit_munhyeok/%EC%BF%A0%ED%82%A4%EC%99%80-%EC%84%B8%EC%85%98-%EA%B7%B8%EB%A6%AC%EA%B3%A0-JWT</link>
            <guid>https://velog.io/@grit_munhyeok/%EC%BF%A0%ED%82%A4%EC%99%80-%EC%84%B8%EC%85%98-%EA%B7%B8%EB%A6%AC%EA%B3%A0-JWT</guid>
            <pubDate>Mon, 08 Jul 2024 06:25:21 GMT</pubDate>
            <description><![CDATA[<h1 id="목적">목적</h1>
<p>세션과 쿠키 그리고 JWT에 대해 공부하고 겜린더에는 왜 JWT를 적용했는지 정리하기 위한 글이다.</p>
<hr>
<h1 id="http">HTTP</h1>
<p>세션을 알아보기 전에 HTTP에 대한 정리를 해보면 좋을 것 같다.</p>
<p>HTTP는 HyperText Transfer Protocol의 약자로 www(World Wide Web) 상에서 정보를 주고 받을 수 있는 프로토콜이라고 한다.
주로 HTML 문서를 주고받는 데 쓰이지만 일반적인 텍스트, 이미지, 영상, 음성, 파일 등도 전송할 수 있다.</p>
<h2 id="특징">특징</h2>
<h3 id="클라이언트-서버-구조">클라이언트 서버 구조</h3>
<p>클라이언트와 서버 사이에 이루어지는 요청/응답(Request/Response) 프로토콜인데
예를 들어 클라이언트가 HTML을 요청하면 해당 필요한 정보를 해당 클라이언트에게 전달한다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/63e5e3e1-cae1-4af4-a240-07f73bd4cfd8/image.png" alt=""></p>
<h3 id="비연결성connectionless">비연결성(Connectionless)</h3>
<p>이건 사실 1.0의 대표적인 특징이고 1.1부턴 지속 연결을 사용한다... 특징이라고 하긴 좀 애매하지만 그래도 가져와 봤다.</p>
<h3 id="stateless-하다">Stateless 하다</h3>
<p>HTTP에서는 서버가 클라이언트의 상태를 보존하지 않는다.</p>
<p>응답과 요청이 독립적인데 이를 통한 장단점이 존재한다.</p>
<ul>
<li><p>장점 : 서버 확장성이 높다
응답 서버를 쉽게 바꿀 수 있어 서버 증설하기 좋은 편이다.</p>
</li>
<li><p>단점 : 클라이언트가 추가 데이터를 전송해야 한다.</p>
</li>
</ul>
<blockquote>
<p>그럼, 로그인 같은 유저의 상태(정보)를 유지하는 서비스는 어떻게 하는 거지?</p>
<p>이럴 때
브라우저 쿠키, 세션, 토큰 등을 이용해 상태를 유지해 주는 것이다.</p>
</blockquote>
<hr>
<h1 id="cookie">Cookie</h1>
<p>HTTP Cookie란 웹 서버에 의해 사용자의 컴퓨터에 저장되는 &quot;이름을 가진 작은 크기의 데이터&quot;(4KB) 이다.
사용자가 어떠한 웹사이트를 방문할 때 사용자의 웹 브라우저를 통해 인터넷 사용자의 컴퓨터나 다른 기기에 설치되는 작은 기록 정보 파일을 일컫는다.</p>
<p>쿠키는 스파이웨어를 통해 유저의 브라우징 행동을 추적하는 데 사용될 수 있고 누군가의 쿠키를 훔쳐 해당 사용자의 웹 계정 접근권한을 획득할 수도 있다.</p>
<p>쿠키는 보통 세 가지 목적을 위해 사용된다.</p>
<ol>
<li>세션 관리</li>
</ol>
<ul>
<li>서버에 저장해야 할 로그인, 장바구니, 게임 스코어, 접속 시간 등의 개인 정보 관리</li>
</ul>
<ol start="2">
<li>개인화</li>
</ol>
<ul>
<li>각 사용자에게 적절한 페이지를 보여줌 (사용자 선호, 테마 등의 세팅)</li>
</ul>
<ol start="3">
<li>트래킹</li>
</ol>
<ul>
<li>사용자의 행동과 패턴을 분석하고 기록하는 용도</li>
</ul>
<p>요즘은 <a href="https://developer.mozilla.org/ko/docs/Web/API/Web_Storage_API">Web Storage</a>를 사용해 정보를 저장하는 것을 권장한다고 한다.</p>
<p>요청마다 쿠키가 함께 저장되기 때문에 특히 Mobile Data Connections인 경우 성능이 급격히 떨어질 수 있다.
정보를 클라이언트 측에 요청하려면 Web Storage의 종류인 Local storage, Session storage 아니면 Indexed DB를 사용하면 된다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/e90ae881-6043-40d3-be89-e48a057f2887/image.png" alt="">
Velog도 야무지게 사용한다.
근데 Local storage에 저장하면 XSS공격에 취약하지 않을까...?
차라리 HttpOnly, Secure, SameSite 플래그가 설정된 쿠키에 저장하는 게 나을지도..</p>
</blockquote>
<p><a href="https://www.rdegges.com/2018/please-stop-using-local-storage/">Please Stop Using Local Storage</a> 좋은 글이 있어 공유해본다.</p>
<hr>
<h1 id="http-session">HTTP Session</h1>
<p>HTTP는 상태가 유지되지 않는 Stateless 한 프로토콜이라고 했다. 그럼, 서버는 클라이언트의 상태를 어떻게 기억할까?</p>
<p>클라이언트의 상태를 기억하기 위해 대표적으로 Session 개념을 사용하였다.</p>
<h2 id="작동-방식">작동 방식</h2>
<ol>
<li><p>서버는 고유한 세션 ID(JSESSIONID)를 웹 브라우저 쿠키로 전달한다.</p>
</li>
<li><p>이를 받은 클라이언트는 서버에 어떠한 요청을 할 때 쿠키(JSESSIONID)를 함께 전달한다. </p>
</li>
<li><p>서버는 전달받은 세션 ID를 서버에 이미 저장되어 있는 정보와 비교해 클라이언트의 상태를 지속적으로 유지하게 된다.</p>
</li>
</ol>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/5d87ef07-ef1c-4768-9468-a7f82f3ce050/image.png" alt="">
Session ID가 저장된 쿠키를 이용한 HTTP Request 과정</p>
</blockquote>
<h2 id="구조">구조</h2>
<ul>
<li><p>서버에 세션 저장소가 필요하다 (메모리, 데이터베이스 등)</p>
</li>
<li><p>클라이언트에는 세션 ID만 저장하는데 보통 쿠키에 저장한다.</p>
</li>
</ul>
<h2 id="세션의-특징">세션의 특징</h2>
<ul>
<li><p>Session ID는 브라우저 단위로 저장되어 브라우저 종료 시 소멸한다.</p>
</li>
<li><p>사용자의 로그인 상태, 닉네임 등 사용자가 요청할 때마다 필요한 정보들을 세션에 담아두면 사용자 DB에 접근할 필요가 없어 효율적이다.</p>
</li>
<li><p>Stateful 하고 서버 측에서 세션 관리 및 제어가 용이하다.</p>
</li>
</ul>
<hr>
<h1 id="jwt">JWT</h1>
<p>JWT(JSON Web Token)은 JSON 객체로 당사자 간에 정보를 안전하게 전송하기 위한 컴팩트하고 독립적인 방식을 정의하는 개방형 표준이다. <a href="https://datatracker.ietf.org/doc/html/rfc7519">(RFC 7519)</a></p>
<p>라고 하면 너무 복잡해 간단하게 정리해 보자</p>
<h2 id="작동-방식-1">작동 방식</h2>
<p>서버가 사용자 인증 후 JWT를 생성하여 클라이언트에 전송
클라이언트는 이후 요청마다 JWT를 함께 전송
서버는 JWT의 서명을 확인하여 유효성 검증</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/dd1cf37a-b3a4-4c39-9065-e86d89c4c965/image.png" alt="">
서버는 JWT를 디코딩해 유효성 검증을 진행한다.</p>
</blockquote>
<h2 id="구조-1">구조</h2>
<ul>
<li>Header: 토큰 타입과 해시 알고리즘 정보</li>
<li>Payload: 클레임(사용자 ID, 만료 시간 등)</li>
<li>Signature: 토큰의 유효성을 검증하는 서명</li>
</ul>
<h2 id="jwt-특징">JWT 특징</h2>
<ul>
<li>Stateless: 서버에 별도의 저장소가 필요 없음</li>
<li>확장성이 좋음: 서버 간 공유가 쉬움</li>
<li>클라이언트 측에서 저장 및 관리</li>
</ul>
<hr>
<h1 id="난-왜-jwt를-사용했을까">난 왜 JWT를 사용했을까?</h1>
<ol>
<li>확장성과 Open API</li>
</ol>
<ul>
<li><p>추후 겜린더 서비스가 만약 커진다면 개발자들에게 겜린더의 데이터를 Open API로 배포하고 싶은 목표가 있었다. (누가 쓸지는 모르겠다만...)</p>
</li>
<li><p>이럴 땐 서비스 확장에 유리한 JWT가 더 적합하다고 판단하였다.</p>
</li>
</ul>
<ol start="2">
<li>리소스의 효율성</li>
</ol>
<ul>
<li><p>라즈베리파이로 서버를 일단 구성해야하기 때문에 제한된 리소스 환경에선 세션보단 JWT가 서버 메모리 사용을 줄일 수 있다고 생각한다.</p>
</li>
<li><p>세션 저장 및 조회할 때 서버에 대한 부하가 증가할 것 같아 JWT가 더 좋다고 생각했다.</p>
</li>
</ul>
<ol start="3">
<li>개발 난이도</li>
</ol>
<ul>
<li>세션도 분명 확장성을 보완하기 위해 세션 클러스터링 같은 방법이 있다고 배웠다. 하지만 JWT보단 난이도가 높을 것으로 판단하였다.</li>
</ul>
<h1 id="물론-jwt의-한계도-매우-잘-알고-있다">물론 JWT의 한계도 매우 잘 알고 있다.</h1>
<p>보안 이슈가 생기면 정말 골때리기 때문에 신중하게 구현해야 한다. 그래서 내가 할 수 있는 방법들을 생각해 보았다.</p>
<ol>
<li>Access Token &amp; Refresh Token 탈취</li>
</ol>
<ul>
<li><p>이를 위해 Access Token의 만료시간을 짧게 할 예정이다.</p>
</li>
<li><p>Refresh Token은 Token Rotation 기능을 구현해 Refresh할 때마다 새로운 Token을 발급받을 수 있도록 하였다.
(Redis를 적극 활용하였다)</p>
</li>
<li><p>애초에 토큰에 민감한 정보를 많이 넣지 않을 예정이다.
자칫하면 DB 조회가 좀 더 늘어날 수 있지만 그래도 Token 자체에 예민한 개인정보를 넣는 것보단 나을 것 같다.</p>
</li>
</ul>
<ol start="2">
<li><p>HTTPS 구현</p>
</li>
<li><p>앱 보안</p>
</li>
</ol>
<ul>
<li><p>로그아웃 시 안전하게 토큰 삭제</p>
</li>
<li><p>토큰 저장할 때 Local Storage 쓰는 게 아닌 좀 더 안전한 저장소 사용 (iOS의 Keychain, Android의 EncryptedSharedPreferences 같은 것들...)</p>
</li>
</ul>
<hr>
<h1 id="마무리">마무리</h1>
<p>겜린더는 확장성이 중요하다고 생각해 JWT를 구현하였지만,</p>
<p>언제나 기술의 장단점을 알고 명확하게 왜 써야 하는지 그리고 단점을 최대한 보완하는 것이 중요하다고 생각했다.</p>
<p>사실 예전에 Spring Boot로 Session 로그인을 구현한 적이 있었다. 근데 사실 구현하기가 너무 힘들었다.</p>
<p>레퍼런스가 많이 없었기 때문이었다.(스프링인데..?)
다들 JWT로 구현하는것이 유행인 것 처럼 로그인 기능을 구현하는 강의나 그런걸 찾아볼 때 전부 JWT로 구현하는 것을 보았었다.</p>
<p>기술에도 트랜드가 있다고 하지만 왜 써야 하는지 명확하게 모르면 안된다고 생각해 이번 글을 정리해보았다.</p>
<p>다음 글은 JWT를 어떻게 구현하였는가에 대해 적어볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[회고록 겸 일기]]></title>
            <link>https://velog.io/@grit_munhyeok/%ED%9A%8C%EA%B3%A0%EB%A1%9D-%EA%B2%B8-%EC%9D%BC%EA%B8%B0</link>
            <guid>https://velog.io/@grit_munhyeok/%ED%9A%8C%EA%B3%A0%EB%A1%9D-%EA%B2%B8-%EC%9D%BC%EA%B8%B0</guid>
            <pubDate>Fri, 05 Jul 2024 09:49:21 GMT</pubDate>
            <description><![CDATA[<h1 id="시작하기-전에">시작하기 전에</h1>
<p>최근 겜린더를 개발하면서 생겼던 일들을 서사별로 정리하고 싶어 일기 느낌으로 정리해 보았습니다.</p>
<hr>
<h1 id="슬로건-변경">슬로건 변경</h1>
<p>겜린더의 슬로건을 바꾸었다.</p>
<blockquote>
<p><strong>기존 슬로건</strong>
달력으로 알아보는 게임 정보</p>
</blockquote>
<hr>
<p> <strong>새로운 슬로건</strong>
나만의 게임 캘린더</p>
<p>최근 겜린더의 UI를 개선한 부분을 UI/UX 공부하는 친구에게 자랑 아닌 자랑을 했었다.
<a href="https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A0%9C%EC%9E%91%EA%B8%B0-1">겜린더 프론트엔드 제작기 1</a>
근데 친구가 UI 관련 피드백을 해주다가 팩폭을 했는데</p>
<p>한마디로 요약하자면
친구가 현재의 겜린더는 딱히 써야 할 이유를 모르겠다고 했었다.</p>
<blockquote>
<p>*<em>친구 : *</em>
겜린더라길래 자신이 원하는 게임을 달력에 추가하는 건 줄 알았는데 아니네?
그럴 거면 왜 써 오히려 불편해</p>
</blockquote>
<p>친구가 말하는 게 사실 맞다. 지금의 겜린더는 특출난 무언가가 없다.
그냥 달력 형태로 게임 정보 보여주는 건데 말 그대로 CRUD인데 무슨 매력이 있겠는가
심지어 보기에도 더 불편한 달력 형태로 데이터를 제공한다. <del>(최악 그 잡채)</del></p>
<p>굳이 핑계를 대보자면 내가 개발 능력이 없다고 생각했고,
빨리 만들어야겠단 생각을 했기 때문에 그냥 최대한 단순화하고 싶었다.
사실 이렇게 생각하면 안 되는 건데 <strong>어느 순간 현재 상황에 안주하고 있었다</strong></p>
<blockquote>
<p><strong>이렇게 만들면 내가 만족할까?</strong></p>
</blockquote>
<p>또 새로 만든다고 코드를 다시 작성하고 있을 나 자신이 보였고
후회할 짓 하지 말고 내가 하고 싶은 것 다 구현해 보자 싶었다.</p>
<p>그래서 <strong>&quot;나만의 게임 캘린더&quot;</strong>로 슬로건을 바꾸었다.</p>
<h1 id="그럼-회원-관리를-해야-하나">그럼, 회원 관리를 해야 하나?</h1>
<p>슬로건이 나만의 게임 캘린더로 바뀌면서 단순히 달력 형태로
사용자에게 게임 정보를 제공하는 걸 벗어나야겠다고 판단했다.</p>
<p>친구의 말을 토대로</p>
<p>자신이 원하는 게임을 저장해 달력에 볼 수 있게 하고, 덩달아 출시 알림까지 받아낼 수 있게 하는 과정이 사용자가 쓰기에도 더 유용할 것 같았다.</p>
<blockquote>
<p><strong>단순한 개념도</strong>
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/74fa897e-be0c-47ef-9012-62bce9d6e60b/image.png" alt=""></p>
</blockquote>
<p>회원 관리는 혼자 서비스를 운영하는 데 있어 상당히 리스크가 있다.</p>
<p>당장 생각나는 것만 해도
보안 문제, 인프라 문제, 비용적인 문제, 구글플레이나 앱스토어의 정책문제 등등... 여러 문제가 생길 것이 분명하다는 느낌이 팍팍 들었고
간단한 Key Value 스토리지나 SQLite 같은 거 사용해 로컬에 데이터를 저장하는 게 어떠려나? 싶었다.</p>
<blockquote>
<p>나 : 폰을 바꾸거나 앱을 재설치하면 데이터가 전부 날아가!
대신 관리는 내가 할 필요가 없고! 운영하는 데 큰 비용이 들지 않아서 이득!</p>
</blockquote>
<p>라고 친구한테 설명했으나...
이걸 같이 듣고 있던 친구의 반응은</p>
<blockquote>
<p>친구 : 쓰읍 그건 좀.... 살짝 아쉬운데? <del>(에반데?)</del></p>
</blockquote>
<p>당연한 반응이다.
요즘 누가 저렇게 사용자의 데이터를 저장도 안 하고 날려 먹게 하는가...
다 계정 연동되서 저장되는 세상이다.</p>
<h2 id="알고-보니-매너리즘에-빠진-건-나-자신이었다">알고 보니 매너리즘에 빠진 건 나 자신이었다.</h2>
<p>난 아직도 운영에 대한 리스크를 줄이려고 최대한 발악했던 것이다.</p>
<p>과거에는 항상 사용자의 입장에서 생각해보려고 노력했던 내 생각은 어느 순간 스스로의 현실에 타협하고, 편안함을 찾으려는 나 자신을 보고 충격받았다.</p>
<p>도전해도 모자를 판에 이런 생각을 하는 나 자신이 너무 싫었고,
일단 기능을 다 만들고 운영은 나중에 생각하자는 마음으로 결국 회원 기능을 만들기로 하였다.</p>
<blockquote>
<p>새벽 4시에 머릿속에 생각나는 것들만 열심히 휘갈겨 쓴 흔적들 <img src="https://velog.velcdn.com/images/grit_munhyeok/post/1aa9988d-e08b-4a78-b8ea-a636a4adf120/image.png" alt=""> <img src="https://velog.velcdn.com/images/grit_munhyeok/post/094d87a2-65c0-4ac2-9c98-0cffb6d807e6/image.png" alt=""></p>
</blockquote>
<hr>
<h1 id="jwt-기능-구현">JWT 기능 구현</h1>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/cbda61d0-8d6b-4705-af56-cbe1cf5c0d00/image.png" alt=""></p>
<p>회원 기능을 만들자! 라고 생각하고 공부하면서 개발을 하니 어느 순간 완성 되어있었다.</p>
<p>2주 정도 예상했었는데 생각보다 빨리 만들어냈다.</p>
<p>다만 아직 비밀번호 재설정 관련 기능을 만들지 못했고 
보안을 위해 Refresh Token Rotation 기능을 구현하였다.</p>
<p>Refresh Token Rotation을 만들면서 약간 문제 아닌 문제가 있었는데
이 내용은 추후 JWT 구현 과정 글을 작성할 때 적을 예정이다.</p>
<h1 id="친구의-디자인-피드백과-도움">친구의 디자인 피드백과 도움</h1>
<p>그리고 이전에 피드백 받은 친구한테 UI 관련해 좀 더 자세한 내용을 들어보고 싶어 질문을 했는데 친구가 직접 Figma로 본인이 생각했던 UI 디자인을 만들어주었다.</p>
<p>생각 이상으로 너무 잘 나와서 이대로 친구가 만들어준 UI 디자인을 토대로 다시 프론트 작업을 할 생각이다.</p>
<p>내가 여태껏 구현하지 못했던 것들을 구현해야 해서 나름의 도전이라고 생각하고 또 해보려고 한다.</p>
<blockquote>
<p>1시간 30분 정도 걸렸다는데 내가 한 것보다 훨씬 낫다
역시 배운 사람은 다르다... <img src="https://velog.velcdn.com/images/grit_munhyeok/post/6dd1ee5d-81ba-40db-88b8-fc427d344a76/image.png" alt=""></p>
</blockquote>
<hr>
<h1 id="마무리">마무리</h1>
<p>나는 항상 어떤 일을 하든 최악의 상황을 가정하고 여러 가지 경우의 수를 생각한다. 그러다 보니 생각이 점점 많아지게 되는데</p>
<p>덕분에 항상 어떤 아이디어를 내놔도 생각 많은 성격 탓에 스스로를 검열해 아이디어로만 남겨놓는 경우가 대부분이다.</p>
<p>사실 욕심이 많아서 아이디어를 많이 내보았지만
실제로 실행하기가 무섭기도 하다.</p>
<p>이러다가 취업은 할 수 있을까...
사회에서 인정도 못 받을 것 같다는 생각 때문에 그런 것 같다.</p>
<p>이런 복잡한 생각들을 요즘은 최대한 단순하게 생각하려고 한다.
그냥 해야지!, 하지 말아야지! 생각하는 게 아니다.</p>
<p>지금은 퇴사했지만, 과거 Apple 최고 디자이너 Jony Ive가 iOS 7 소개했던 영상이 있는데 영상 초반 그가 했던 말은 정말 인상 깊었다.</p>
<blockquote>
<p>&quot;단순함은 복잡함에 질서를 부여하는 것이다.&quot;<img src="https://velog.velcdn.com/images/grit_munhyeok/post/f252344f-c1c6-477f-bcd2-03c0b077fc66/image.png" alt=""></p>
</blockquote>
<p>어렸을 적 봤을 땐 디자인에만 적용되는 문장 같았지만, 
인생을 살아가는 데도 매우 중요한 문장이라고 생각한다.</p>
<p>어렸을 때부터 감명받아 항상 인생에 적용하려 노력했었는데 어느 순간 잊고 살아온 것 같아 이렇게 다시 한번 복기해본다.</p>
<p><strong>열심히 허슬하자</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개념으로만 잡혀있던 싱글톤 패턴 직접 적용하기]]></title>
            <link>https://velog.io/@grit_munhyeok/%EC%8B%B1%EA%B8%80%ED%86%A4%ED%8C%A8%ED%84%B4-%EC%A7%81%EC%A0%91-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@grit_munhyeok/%EC%8B%B1%EA%B8%80%ED%86%A4%ED%8C%A8%ED%84%B4-%EC%A7%81%EC%A0%91-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 31 May 2024 10:02:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Github
<a href="https://github.com/munhyok/Gamlendar_Selenium">Gamlendar_Selenium</a></p>
</blockquote>
<blockquote>
<p>겜린더 게임 수집 파이프라인을 안정화 작업을 하고 있을 때 발생했던 일이었다.
<a href="https://velog.io/@grit_munhyeok/%EC%95%BD-2300%EA%B0%9C%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%88%98%EC%A7%91%ED%95%B4-DB%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0">약 2300개의 게임을 수집해 DB에 저장하기</a></p>
</blockquote>
<h1 id="문제">문제</h1>
<p>안정화 하고 테스트를 해보는데
분명 수집할 때는 1400개 이상의 게임이 수집되었지만
DB에는 24개의 게임 밖에 저장이 되지 않았다.</p>
<p>왜 그럴까..?</p>
<h1 id="원인">원인</h1>
<p>일단 기존 데이터 수집 방식 구조를 알아야 할 필요가 있다.</p>
<p>수집 프로그램은 최초 실행 때 db 인스턴스를 생성한다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/50206e19-5337-43e1-83a8-44a1070982b0/image.png" alt="">
main.py 최초 실행 때 db 인스턴스를 생성하는 걸 볼 수 있다.</p>
</blockquote>
<p>이번에 작업하면서
안정화하면서 기능을 하나 추가했었는데</p>
<p>지난번 글에서 언급한 닌텐도 스위치 페이지 난제를 해결하기 위해
스위치에서 수집한 게임이 기존 DB에 수집되어 있는가를 확인하는 검색 작업을 추가하였다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/334c73f7-876c-4551-840a-bfb719fe9e5a/image.png" alt=""> 저번에 언급한 난제</p>
</blockquote>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/138dbf46-fa04-4ba7-a1d0-65fdb5123c16/image.png" alt=""> <img src="https://velog.velcdn.com/images/grit_munhyeok/post/6bf1d0dc-0c5d-42e3-8124-c821d0062957/image.png" alt="">
switch/detailScrap.py
db = Database()로 인스턴스를 생성하고 findGame 함수로 검색할 수  있게 만들어뒀었다.</p>
</blockquote>
<p>여기서 이제 눈치 빠르신 분들은 어디가 문제인지 눈치채셨을 것 같다.</p>
<blockquote>
<p><strong>그렇다 똑같은 Database 인스턴스가 여러 개 생성되었다.</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/926b9a67-6f6b-4808-b055-f27216d819de/image.png" alt=""></p>
<p>이를 해결하기 위해선 예전에 공부했던 싱글톤 패턴을 이용하면 될 것 같단 생각이 들었다.</p>
<h1 id="해결">해결</h1>
<h2 id="싱글톤-패턴">싱글톤 패턴??</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/90abc782-d5ed-469a-ac78-7b771f39f898/image.png" alt="">출처: 위키피디아</p>
</blockquote>
<p>짧게 요약하면
싱글톤 패턴은 특정 클래스의 인스턴스를 1개만 생성되는 것을 보장하는 디자인 패턴이다.</p>
<p>장점으로</p>
<ol>
<li><p>메모리 낭비를 줄일 수 있다.
한 개의 인스턴스만을 고정 메모리 영역에 생성하기 때문에 메모리 낭비를 방지할 수 있다.</p>
</li>
<li><p>속도 측면의 이점
생성된 인스턴스를 사용할 때는 이미 생성된 인스턴스를 활용하여 오버헤드를 줄일 수 있다.</p>
</li>
<li><p>데이터 공유가 가능하다.
여러 클래스에서 데이터를 공유하며 사용할 수 있다. 하지만 동시성 문제가 발생할 수 있어 설계할 때 유의해야 한다.</p>
</li>
</ol>
<h2 id="왜-나는-싱글톤-패턴을-사용했는가">왜 나는 싱글톤 패턴을 사용했는가?</h2>
<h3 id="1-데이터-공유">1. 데이터 공유</h3>
<p>일단 싱글톤 패턴을 사용해야겠다고 생각한 이유는
데이터의 공유가 제일 큰 목적이었다.</p>
<p>물론 그렇게 되면 의존성이 높아질 수 있다는 단점도 있지만</p>
<p>애초에 데이터 공유가 안되면 아예 내가 원하는 대로 작동이 불가능했기 때문에 싱글톤 패턴을 사용하기로 했다.</p>
<h3 id="2-싱글-스레드로-동작">2. 싱글 스레드로 동작</h3>
<p>내가 만든 수집 봇은 싱글 스레드로 동작한다.
싱글톤의 대표적인 문제점 중 하나가 멀티 스레드 환경에서의 동시성 문제인데</p>
<p>내 수집 봇은 애초에 싱글 스레드로 동작했기 때문에 크게 문제가 없을 것 같단 판단이 들었다.</p>
<h2 id="해결한-코드">해결한 코드</h2>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/5fb47d5f-97dc-4519-919a-61097f1fb7c6/image.png" alt=""></p>
<p>이런 식으로 중복된 인스턴스를 만들지 못하게 하였다.</p>
<p>하지만 이렇게 했는데도 여전히 1400개에서 24개밖에 올리지 못하는 모습을 보여주었다.</p>
<p>코드와 메모리 주소를 보니깐 인스턴스는 분명 하나인데
인스턴스를 할당할 때마다 init을 하는 것 같았다.</p>
<p>그래서 최초로 초기화하면 그 이후에는 초기화하지 못하게 만들었다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/25574601-f174-47af-8512-a1557f4e9582/image.png" alt=""></p>
<h1 id="마무리">마무리</h1>
<p>싱글톤 패턴은 자바를 공부하면서 대표적인 디자인 패턴으로 공부를 하게 되었는데 이걸 파이썬에 적용해 볼 줄은 몰랐다.</p>
<p>그래도 덕분에 개념을 더 확실하게 잡아갈 수 있었고</p>
<p>이런 상황에서 싱글톤 패턴을 이용해 문제를 해결해야 한다는 생각까지 도달한 모습을 보고 그냥 막 배운 건 아니구나 싶었다.</p>
<p>싱글톤 패턴을 찾아보면서 안티패턴이라고 하던데, 객체지향적인 측면에서 보면 맞는 말이다.</p>
<p>하지만 무작정 안티패턴이라 해서 배척하기보다는, 내 상황과 조건을 확인해보고 필요하면 사용하는 것이 맞다고 보았다.</p>
<p>애초에 멀티 스레드 환경이 아니라 싱글톤 패턴을 사용하기 좋은 조건이 아니었을까 생각한다.</p>
<p>잠깐 언급한 닌텐도 스위치 게임과의 데이터 통합도 잘 해결된 것 같고, 이렇게 수집 봇도 나름의 안정화를 잘 거쳐가고 있는 중이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[겜린더 프론트엔드 제작기 - 1]]></title>
            <link>https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A0%9C%EC%9E%91%EA%B8%B0-1</link>
            <guid>https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%A0%9C%EC%9E%91%EA%B8%B0-1</guid>
            <pubDate>Thu, 23 May 2024 18:24:53 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>피드백은 언제나 환영합니다. 궁금하신 점이 있다면 댓글 달아주세요!</p>
</blockquote>
<p>최근 게임 수집 데이터 파이프라인을 안정화 작업을 해 많은 부분을 개선했습니다.
<a href="https://velog.io/@grit_munhyeok/%EC%95%BD-2300%EA%B0%9C%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%88%98%EC%A7%91%ED%95%B4-DB%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0">약 2300개의 게임을 수집해 DB에 저장하기</a></p>
<p><em><strong>백엔드를 튼튼하게 만들었으니 멋지게 화면을 뽑아내 줄 프론트엔드도 필요하겠죠?</strong></em></p>
<p>드디어 백엔드 부분은 좀 빛이 보이는 기분이라 이제 막막한 프론트엔드 작업을 이어서 해보려고 합니다.</p>
<p>만들 때 어떤 고민을 했었는지 생각을 정리해 구현 과정을 적어두었고
물론 지금 만들고 있는 건 아직 작업 중이기 때문에 달라질 수 있습니다.</p>
<hr>
<h1 id="겜린더메인-화면">겜린더(메인 화면)</h1>
<p>사실 여러 번 디자인을 갈아엎기도 하고
겜린더의 본질은 무엇인지 다시 한번 고민하게 되었던 시간이었습니다.</p>
<p>앱의 메인 화면은 정말 중요하죠</p>
<p><strong>&quot;어떻게 하면 달력을 좀 더 친숙하게 표현할 수 있을까?&quot;</strong>
이 부분을 많이 고민했습니다.</p>
<p>아예 달력 형태를 쓰지말까?라는 의견도 나왔지만
이름이 겜린더(게임 캘린더)인 만큼 달력의 형태를 더욱 유지하고 싶었습니다.</p>
<p>그래서 기존 판매하는 달력들의 이미지를 많이 검색하기도 하고
집안에 있는 탁상 달력이나 벽걸이 달력 등을 많이 봤는데요</p>
<blockquote>
<p>Reference Image <img src="https://velog.velcdn.com/images/grit_munhyeok/post/07277000-873f-4d79-ae37-0aabe6ea8f98/image.png" alt="">출처 : 달카닷컴</p>
</blockquote>
<p>이런 식으로 대부분 달력들은 상단에 여러 이미지나 정보들이 담겨있고
하단에 달력이 있는 형태가 많았습니다.</p>
<p>이 아이디어를 착안하여 겜린더의 페이지를 개선해 보았습니다.</p>
<h2 id="개선된-화면">개선된 화면</h2>
<p>저 게임 등록 배너 이미지는 최종 버전에서 <strong>당연히</strong> 없어질 예정입니다.
<del>(마리오를 실제 앱에 사용했다간... 큰일 날지도..)</del></p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/e7c9835d-6c87-4411-814e-132836f353f3/image.png" alt=""> </p>
</blockquote>
<p>어떠신가요?
기존 앱과는 많이 달라 보인다면 다행이네요</p>
<p>개선된 화면의 장점이라면
달력이 하단으로 자연스럽게 내려오면서 한 손으로 달력 조작을 쉽게 할 수 있게 되었다는 점이었습니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/6ed5a758-7bc2-4dbf-abe1-e16cbb789b38/image.png" alt="">Thumb Zone</p>
</blockquote>
<p>사용자는 한 손으로 달력을 조작할 수 있고
자연스럽게 상단 배너로 새로운 정보를 습득할 수 있게 되었습니다.</p>
<p>상단에 있는 배너는 광고 배너로 활용할 예정인데
겜린더에 직접 게임 등록을 신청해 주신 게임을 홍보할 수 있는 배너로 활용할 예정입니다.</p>
<hr>
<h1 id="게임-목록">게임 목록</h1>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/bb9154e0-032e-49a4-973d-3827b0f8364e/image.png" alt=""></p>
<p>게임 목록은 별도의 페이지를 만들어 분류하였습니다.</p>
<table>
<thead>
<tr>
<th>기존 버전</th>
<th>개선 버전</th>
</tr>
</thead>
<tbody><tr>
<td>카드 형식으로 게임 이름과 제작사 정보가 담겨 있음</td>
<td>간략하게 정보를 보여주고 게임 출시 D-Day와 지원하는 플랫폼 표시</td>
</tr>
</tbody></table>
<h2 id="기존-카드-디자인">기존 카드 디자인</h2>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/9177d4c7-400a-4a4d-9b63-39d39ef32352/image.png" alt="">
예전에는 게임 썸네일의 이미지가 풀 사이즈로 노출되는게
이쁘고 멋지다고 생각했었는데요</p>
<p>이런 식으로 디자인을 하면 공간 효율이 많이 좋지 못하다 생각하였고,
유저에게 더 많은 정보를 전달하는 게 중요하다 생각하였습니다.</p>
<h2 id="개선-버전-디자인">개선 버전 디자인</h2>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/c2ade603-3c5f-4187-b552-f08e8110ff60/image.png" alt="">
썸네일 이미지를 일부 포기하더라도 위의 이미지와 같이 정리해두는 게 훨씬 좋아 보여 이 디자인을 채택하게 되었습니다.</p>
<p>덕분에 한 화면에 더 많은 게임들의 정보가 보이게 되어 화면 공간 효율이 좋아졌다고 생각합니다.</p>
<p>D-Day 기능을 넣어 언제 출시하는지 직관적으로 볼 수 있어 개인적으로 더 좋네요!
고민을 많이 했었는데 좋은 결과물이 나온 것 같아 다행입니다.</p>
<hr>
<h1 id="게임-상세">게임 상세</h1>
<p>게임의 상세 정보를 보여주기 위한 페이지를 정말 많이 개선했습니다.
메인화면 다음으로 공을 들여봤는데요</p>
<h2 id="기존-버전">기존 버전</h2>
<p>일단 기존 버전의 디자인부터 보시죠</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/e36ee9b7-094a-48ae-806f-42106f6ebd4a/image.png" alt="">
진짜 정보 전달에만 집중되어 있는 느낌...
사실 이때는 실력이 없어 이거에 만족하자~ 이런 느낌이었습니다.</p>
</blockquote>
<h3 id="디자인-의도">디자인 의도</h3>
<p>개선 버전의 디자인 의도부터 말씀드리자면
유저가 게임 설명 페이지를 볼 때 몰입이 되었으면 좋겠다고 생각했습니다.</p>
<p>어떻게 하면 지루한 상세 정보 페이지를 몰입감 있게 보여줄 수 있을까?
라는 고민을 열심히 해 나온 결과물입니다.</p>
<h2 id="개선-버전">개선 버전</h2>
<h3 id="애니메이션-미리보기">애니메이션 미리보기</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/3879fd5e-58ab-4b1e-a6d1-a0508a1aa64e/image.webp" alt=""></p>
</blockquote>
<p>너무 빨랐나요? 다시 천천히 봅시다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/2982c2f2-5258-498e-a1ba-baba4c37cc9f/image.png" alt=""> 첫 화면</p>
</blockquote>
<p>디자인 의도에서 말했던 것처럼
첫 화면에서 유저가 게임에 몰입되는 것을 원했기 때문에 과감하게 화면 전체를 채워보았습니다.</p>
<p>하단에는 정보들이 간략하게 나오게 됩니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/3df0972c-1351-462d-a831-237db1ffca63/image.png" alt=""> 좌측 제스처로 화면을 넘겼을 때 상세 설명이 나오게 됩니다.</p>
</blockquote>
<p>설명은 역시 검은 화면에 흰 글자가 더 보기 좋을 것 같아서 그렇게 작업해 보았고</p>
<p>기존 버전은 게임 설명에서 문단도 제대로 구분이 되어있지 않는 경우도 있었는데요
그 이유는 제가 일일이 하나씩 게임을 수집하느라 문단 구분하기가 힘들어서 제대로 수집이 되지 않은 경우가 많았습니다.</p>
<hr>
<h2 id="ux-경험-개선을-위한-애니메이션-추가">UX 경험 개선을 위한 애니메이션 추가</h2>
<p>몰입감 있게 디자인 한 건 좋은데
<strong>좌측 제스처로 화면을 넘기는 사실을 유저가 모른다면...?</strong> 라는 생각이 들어
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/696ec7a8-5e80-4297-a132-eda85de29a1a/image.png" alt=""></p>
<p>게임 정보와 그에 맞게 제스처로 넘기라는 화살표를 만들어뒀었는데요
<strong>이걸로 과연 충분할까?</strong> 라는 생각이 들었습니다.</p>
<p>과한 생각이긴 하지만
게임 정보의 위치는 글 상단에 언급했던 Thumb Zone에 포함되어 있어
자칫하면 엄지에 가려져 보이지 않을 수도 있겠다는 생각도 했습니다.</p>
<blockquote>
<p>결국 애니메이션을 넣어 눈에 사용자 눈에 바로 즉각적으로 보이게 해주는 것이 좋다고 판단하여 애니메이션을 추가하게 되었습니다.</p>
</blockquote>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/a2d9867a-294d-4937-9705-58cf7820fa44/image.webp" alt="">적절하게 Fade In과 Out을 활용해 반짝거리면서 눈에 띄게 해보았습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/d3676a61-2689-4212-a814-9a7be13246c8/image.png" alt="">
간략한 구현 코드입니다.</p>
<hr>
<h1 id="글-마무리">글 마무리</h1>
<p>아직 구현할 화면과 기능들은 많습니다.</p>
<ol>
<li>검색 페이지</li>
<li>게임 페이지</li>
<li>FCM</li>
</ol>
<p>그 외..</p>
<ul>
<li>게임 페이지 컨텐츠 구상 등...</li>
</ul>
<p>예전에는 급급하게 화면에 글 띄우는 것만 해도 정말 재밌었는데
이제는 이런 UI와 UX 같은 사용성까지 고려해가며 제가 생각했던 디자인을 구현할 수 있게 되어 매우 기쁘네요</p>
<p>빠른 시일내로 제작해 얼른 재출시를 해보고 싶어집니다.</p>
<p>그리고 프론트 결과물을 보고나니 더욱 더 백엔드를 다시 처음부터 설계하길 잘했단 생각이 다시한 번 들었습니다.</p>
<p>마지막으로 전체적인 플로우를 보며 글을 마무리 하겠습니다.
UI나 UX 관련 피드백은 언제나 환영합니다.</p>
<p>읽어주셔서 감사합니다.</p>
<hr>
<h1 id="전체적인-플로우">전체적인 플로우</h1>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/51c2d473-a478-46de-9ae3-a77647027203/image.webp" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[약 2300개의 게임을 수집해 DB에 저장하기 (데이터 파이프라인)]]></title>
            <link>https://velog.io/@grit_munhyeok/%EC%95%BD-2300%EA%B0%9C%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%88%98%EC%A7%91%ED%95%B4-DB%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@grit_munhyeok/%EC%95%BD-2300%EA%B0%9C%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%88%98%EC%A7%91%ED%95%B4-DB%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 12 Mar 2024 15:27:01 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>목차
<a href="https://velog.io/@grit_munhyeok/%EC%95%BD-2300%EA%B0%9C%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%88%98%EC%A7%91%ED%95%B4-DB%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0#%EA%B6%81%EA%B7%B9%EC%A0%81%EC%9D%B8-%EB%AA%A9%ED%91%9C">1. 궁극적인 목표</a>
<a href="https://velog.io/@grit_munhyeok/%EC%95%BD-2300%EA%B0%9C%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%88%98%EC%A7%91%ED%95%B4-DB%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0#%EB%82%B4%EA%B0%80-%EA%B2%AA%EC%9D%80-%EB%AC%B8%EC%A0%9C%EC%A0%90">2. 내가 겪은 문제점</a>
<a href="https://velog.io/@grit_munhyeok/%EC%95%BD-2300%EA%B0%9C%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%88%98%EC%A7%91%ED%95%B4-DB%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0#%EC%86%94%EB%A3%A8%EC%85%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8%EC%9D%84-%EB%A7%8C%EB%93%A4%EC%9E%90">3. 솔루션 (데이터 파이프라인을 만들자!)</a>
<a href="https://velog.io/@grit_munhyeok/%EC%95%BD-2300%EA%B0%9C%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%88%98%EC%A7%91%ED%95%B4-DB%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0#%EC%9D%98%EB%AC%B8%EC%A0%90">4. 의문점</a>
<a href="https://velog.io/@grit_munhyeok/%EC%95%BD-2300%EA%B0%9C%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%88%98%EC%A7%91%ED%95%B4-DB%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0#%ED%9B%84%EA%B8%B0">5. 후기</a></p>
</blockquote>
<blockquote>
<p>피드백은 언제나 환영합니다.
언제든 훈수해 주셔도 괜찮습니다.</p>
</blockquote>
<blockquote>
<p>지난 글에 이어서 작성합니다.
<a href="https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%EC%8A%A4%ED%81%AC%EB%9E%98%ED%95%91-%EB%B4%87-%EC%A0%9C%EC%9E%91%EA%B8%B0-Steam">겜린더 스크래핑 봇 제작기 Steam편</a></p>
</blockquote>
<blockquote>
<p>Github
<a href="https://github.com/munhyok/Gamlendar_Selenium">Gamlendar_Selenium</a></p>
</blockquote>
<h1 id="3줄-요약">3줄 요약</h1>
<blockquote>
</blockquote>
<ol>
<li>게임 정보를 자동으로 수집하고 저장하는 데이터 파이프라인 구축</li>
<li>데이터 전처리(Data Cleansing)을 통해 일관성 있는 데이터 정제 및 RDB에 저장</li>
<li>Steam, Xbox, Playstation, Nintendo Switch 합쳐 약 2300개의 데이터를 수집할 수 있었음</li>
</ol>
<h1 id="궁극적인-목표">궁극적인 목표</h1>
<p>리마인드 하기 위해 다시 목표부터 작성해 보자면..</p>
<ol>
<li><p>게임 정보를 하나하나 손으로 수집하기 힘들기 때문에 자동화하기</p>
</li>
<li><p>중복 게임은 하나로 합쳐주기</p>
<p>게임 출시하는 플랫폼은 여러 곳이 존재하는데
만약 A라는 게임이 Playstation, Xbox에 출시한다면
각 플랫폼 페이지에 있는 A 게임의 정보를 하나로 합쳐 정리해 주는 것이 목표였습니다.</p>
</li>
</ol>
<h2 id="예시를-들어보자">예시를 들어보자</h2>
<table>
<thead>
<tr>
<th align="center">게임이름</th>
<th align="center">게임설명</th>
<th align="center">제작사</th>
<th align="center">플랫폼</th>
</tr>
</thead>
<tbody><tr>
<td align="center">엘든링</td>
<td align="center">그냥 겁나어려운 게임</td>
<td align="center">FromSoftware</td>
<td align="center">Playstation</td>
</tr>
<tr>
<td align="center">엘든링</td>
<td align="center">그냥 겁나어려운 게임</td>
<td align="center">FromSoftware</td>
<td align="center">Xbox</td>
</tr>
<tr>
<td align="center">엘든링</td>
<td align="center">그냥 겁나어려운 게임</td>
<td align="center">FromSoftware</td>
<td align="center">Steam</td>
</tr>
</tbody></table>
<p>이런 식으로 정리가 되면 중복되는 데이터가 너무 많아지는데..
예제라서 별것 아니네~ 할 수 있겠지만 세상에는 게임이 정말 많았습니다..</p>
<p>이런 게임이 수백 개가 쌓이면 저걸 어떻게 관리할 방법이 있을지...
저는 생각나지 않았습니다.</p>
<table>
<thead>
<tr>
<th align="center">게임이름</th>
<th align="center">게임설명</th>
<th align="center">제작사</th>
<th align="center">플랫폼</th>
</tr>
</thead>
<tbody><tr>
<td align="center">엘든링</td>
<td align="center">그냥 겁나어려운 게임</td>
<td align="center">FromSoftware</td>
<td align="center">Playstation, Xbox, Steam</td>
</tr>
</tbody></table>
<p>이렇게 합쳐 추후에 JSON 형식으로 정리할 때도</p>
<pre><code>{
    &quot;게임이름&quot;:&quot;엘든링&quot;,
    &quot;게임설명&quot;:&quot;그냥 겁나어려운 게임&quot;,
    &quot;제작사&quot;:&quot;FromSoftware&quot;,
    &quot;플랫폼&quot;:[Playstation, Xbox, Steam&quot;]
}


</code></pre><p>이런 식으로 손쉽게 정리했으면 좋을 것 같은데
이렇게 정리하면 결과물은 어떻게 나올까요??</p>
<h2 id="예상-결과물">예상 결과물</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/f837a5d8-6ba2-454f-9013-c3c45eb3764a/image.png" alt="">
현재 리뉴얼을 진행 중인 겜린더 상세정보 페이지
이미지 우측 하단에 지원 플랫폼 아이콘을 볼 수 있다.</p>
</blockquote>
<p>요런 결과물이 나오게 됩니다</p>
<h1 id="내가-겪은-문제점">내가 겪은 문제점</h1>
<h2 id="1-데이터-수집-전략">1. 데이터 수집 전략</h2>
<ul>
<li>각 플랫폼마다 형태가 전부 다른데 일일이 하나하나 웹페이지를 분석하여 페이지에 맞게 수집을 해야 하는 것
(이건 당연한 소리지만... 생각보다 애를 많이 먹었네요...😂)</li>
</ul>
<h2 id="2-수집한-데이터의-게임-이름을-일치시키기">2. 수집한 데이터의 게임 이름을 일치시키기</h2>
<p>같은 게임이더라도 각 플랫폼에 등록되어 있는 게임의 이름이 다른 경우가 있었습니다.</p>
<table>
<thead>
<tr>
<th align="center">Xbox</th>
<th align="center">Playstation</th>
</tr>
</thead>
<tbody><tr>
<td align="center">유니콘 오버로드</td>
<td align="center">Unicorn Overlord</td>
</tr>
</tbody></table>
<p>이런 식으로 어떤 곳에선 한국어로 되어있고 어떤 곳은 영어로 되어있었던 건 기본이고...</p>
<table>
<thead>
<tr>
<th align="center">Xbox</th>
<th align="center">Playstation</th>
</tr>
</thead>
<tbody><tr>
<td align="center">Taxi Life: A City Driving Simulator</td>
<td align="center">Taxi Life: Supporter Edition</td>
</tr>
</tbody></table>
<p>이런 식으로 DLC 번들팩을 같이 포함해서 노출시키는 경우도 많았습니다.
(사실 이 부분은 완벽하게 해결하진 못했습니다.)</p>
<h2 id="3-게임-이외의-dlc도-함께-노출">3. 게임 이외의 DLC도 함께 노출</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/b214181c-d757-4701-a907-c1f9514aa830/image.png" alt="">
위 사진은 Xbox의 출시 예정 리스트 중 일부</p>
</blockquote>
<p>MLB The Show 24만 현재 5개나 겹쳐있는 이런 경우도 많았습니다.
저런 데이터까지 전부 수집하면 중복되는 데이터가 많아져</p>
<p>겜린더를 이용하는 유저 입장에선 필요 없는 데이터까지 노출되어 정작 해당 게임에 대한 핵심 정보는 보지 못할 수 있겠다 생각하였습니다.</p>
<h2 id="4-닌텐도는-왜-한국-페이지랑-북미-페이지랑-완전히-다를까요">4. 닌텐도는 왜... 한국 페이지랑 북미 페이지랑 완전히 다를까요...?</h2>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/8669b8d8-8d8d-4701-bf33-91675fc82bde/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/d9df06f2-16d5-4e29-b207-9c23cbe21dca/image.png" alt=""></p>
<p>북미 페이지와 한국 페이지의 레이아웃 디자인은 완전히 달랐습니다.
심지어 SKU(Stock Keeping Unit)도 형태 자체가 달라 이건 뭐.. 어떻게 수집해야 할지 고민을 많이 했습니다.</p>
<h2 id="5-성인-게임-필터링">5. 성인 게임 필터링</h2>
<p><a href="https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%EC%8A%A4%ED%81%AC%EB%9E%98%ED%95%91-%EB%B4%87-%EC%A0%9C%EC%9E%91%EA%B8%B0-Steam">겜린더 스크래핑 봇 제작기 Steam편</a>
여기서 보면 Steam에 있는 성인 게임을 어떻게 필터링했는지 나와있기 때문에 솔루션 부분에서 간단하게 언급만 하겠습니다.</p>
<p>이렇게 5가지를 어떻게 해결했고
어떤 구조로 만들었는지 구체적으로 적어보겠습니다.</p>
<hr>
<h1 id="솔루션-데이터-파이프라인을-만들자">솔루션 (데이터 파이프라인을 만들자!)</h1>
<h2 id="겜린더-백엔드-구조도">겜린더 백엔드 구조도</h2>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/a60d506a-833a-44e9-aa20-15fc64249b59/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/d33d2f3d-e0f3-4db1-8fbd-e1286059ad3f/image.png" alt=""> 흐름도..</p>
<p>유저에게 최종적으로 어떻게 전달되는지 보여주는 간략한 구조도입니다.
오늘은 MongoDB - MariaDB - Selenium 이렇게 3가지 영역을 좀 더 설명해 볼까 합니다.</p>
<h2 id="데이터-파이프라인-구조">데이터 파이프라인 구조</h2>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/a76d23e6-d2dc-4ffa-a1c9-a171aaf886d3/image.png" alt="">
게임 수집과 DB에 저장하기 위한 데이터 파이프라인을 구축해 보았는데요
여기서 영역 별로 쭉 훑어보도록 하겠습니다.</p>
<hr>
<h2 id="data-수집">Data 수집</h2>
<p>데이터 수집을 시작할 때 처음부터 Selenium 브라우저를 2개를 실행하기로 했습니다.
<a href="https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%EC%8A%A4%ED%81%AC%EB%9E%98%ED%95%91-%EB%B4%87-%EC%A0%9C%EC%9E%91%EA%B8%B0-Steam">겜린더 스크래핑 봇 제작기 Steam편</a> 이유는 여기에 나오지만 간략한 설명을 하자면</p>
<p>게임 검색에 필요한 게임 이름이 한국어뿐만 아니라 영어도 필요했기에
2개를 실행해 각각 수집하기 위해서였죠</p>
<p>여기선 겪은 문제점 중 <strong>3번(게임 이외의 DLC 노출)</strong> 을 어떻게 해결했는지 적어보려고 합니다.</p>
<h3 id="번들-탐지-기능">번들 탐지 기능</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/b214181c-d757-4701-a907-c1f9514aa830/image.png" alt=""></p>
<p>여기 중 가장 좌측 상단의 첫 번째 MVP 에디션을 눌러보면
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/4263c7ec-e6ed-4584-bb95-fb311c382437/image.png" alt="">
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/3dbfb1fd-9c04-450d-a2fa-b26baf37bdf8/image.png" alt=""></p>
<p>게임의 정보와 함께 하단을 내렸을 때 <strong>&quot;이 번들&quot;</strong>이라는 정보가 나오게 됩니다.
제가 원하는 정보는 DLC가 아닌 순수한 게임의 정보였기 때문에</p>
<p><strong>&quot;이 번들&quot;</strong> 정보를 수집해
<strong>&quot;MLB The Show 24 Xbox One&quot;</strong> 혹은 <strong>&quot;MLB The Show 24 Xbox X|S&quot;</strong> 둘 중 하나의 페이지로 다시 <strong>리다이렉트</strong> 시키는 것이 목표였습니다.</p>
<p>반대로 저런 번들이 없다는 건 순수한 게임 페이지로 판단할 수 있어 바로 게임 정보를 수집할 수 있게 만들었습니다.</p>
<h3 id="코드">코드</h3>
<p>주의⚠️ 스파게티 코드라 눈뽕이 있을 수 있으니 참고 바랍니다. 죄송합니다...🙇
(물론 Refactoring 할 예정입니다...)
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/45684a71-88a1-41bb-bda1-6545285f20ee/image.png" alt=""></p>
<p>물론 다른 것들도 여러 가지 번들이 많기 때문에
처음 게임 리스트에서부터 사전에 정해둔 단어가 포함되어 있으면 수집 대상에서 제외하는 1차 필터링 기능을 넣었습니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/f01c8fe6-6029-46b2-8619-1d5ff3c2091e/image.png" alt=""></p>
<p>하지만 1차 필터링으론 한계가 꽤 많았습니다.</p>
<p>필터링 단어에 &quot;Edition&quot;이란 단어를 넣으면
게임 이름에 Edition이 들어간 게임이 의도치 않게 제외될 수도 있어 번들 탐지 기능을 추가하였습니다.</p>
<p>Xbox 페이지를 수집하면서 꽤나 중복된 게임 정보가 많았기 때문에 여러 수집 전략을 생각해 볼 수 있었습니다.</p>
<h3 id="성인-게임-필터링">성인 게임 필터링</h3>
<p>겪었던 문제점 중 5번 성인 게임 필터링이 있었죠?
이 부분은
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/5df9e35d-adf4-4170-9496-1bef9bcec98c/image.png" alt="">
그냥 필터링 걸어서 해결했습니다.</p>
<p>저 태그가 들어간 친구들은 도저히 겜린더에 들어가기 힘들겠단... 게임들도 많았습니다.
그래서 그냥 자체적으로 걸러냈습니다.</p>
<p>아직 성인 인증 기능이 겜린더에는 안 들어갔거든요...🥲</p>
<hr>
<h2 id="data-cleansing">Data Cleansing</h2>
<p>사실상 여기가 제일 중요한 지점인데요 이 부분을 잘해야
나중에 DB에 저장할 때 <strong>데이터 무결성</strong>과 중복 데이터를 잘 처리할 수 있었기 때문이었습니다.</p>
<p>그래서 수집한 사이트마다 미묘하게 다른 게임 데이터들을 일관성 있게 정제해야 했습니다.</p>
<p>정제 기법은 3가지가 존재하는데요 3가지의 소개와 겜린더에는 어떻게 적용했는지 같이 설명하겠습니다.</p>
<h3 id="1-변환">1. 변환</h3>
<p>다양한 형태로 표현된 값을 일관된 형태로 변환하는 작업</p>
<ul>
<li>수집된 게임 이름 중 불필요한 데이터를 없앴습니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/a47bcac0-d77c-4c42-958f-4450363f893b/image.png" alt="">
게임 이름 뒤에 있는 PS4 &amp; PS5는 필요 없기 때문에 replace로 간단하게 지워주었습니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/6d94d202-95a7-4a7f-8532-91db2cd32df2/image.png" alt="">
이렇게 괄호가 정말 많이 들어간 게임도 존재했는데요
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/4c4e875b-a302-4515-b6a4-12dd77280311/image.png" alt="">
코드로 보여드리는 게 빠를 것 같네요
정규식을 이용했습니다.</li>
</ul>
<p>굳이 두 번 넣은 이유는 괄호 안에 괄호가 있는 경우가 많아 2번 넣어 해결했습니다.</p>
<h3 id="2-파싱">2. 파싱</h3>
<p>데이터를 정제 규칙을 적용하기 위한 유의미한 최소 단위로 분할하는 과정</p>
<ul>
<li>수집한 데이터 중에서 날짜를 예시로 들면 좋겠네요
Steam은 연, 월, 일로 날짜를 표시하고 (2024년 3월 12일)
Xbox는 &#39;-&#39;로 표시 (2024-03-12)
Playstation은 &#39;/&#39; (2024/3/12)
Nintendo Switch는 &#39;.&#39; (2024.03.12)</li>
</ul>
<p>각각 달랐기 때문에 연, 월, 일의 최소 단위로 분할부터 하였습니다.
2024, 3, 12 이런 식으로 추출하였습니다.</p>
<h3 id="3-보강">3. 보강</h3>
<p>변환, 파싱, 수정, 표준화 등을 통한 추가 정보 반영</p>
<ul>
<li>겜린더에 적용하기 위해선 YYYY-DD-MM 형식으로 만들어야 했습니다.
그래서 보강이 좀 필요했는데요
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/bd22458f-1812-42d4-b40d-d541a6ecf636/image.png" alt="">
Playstation을 예시로 들면 &#39;/&#39;을 기준으로 split 후
월, 일에 zfill(2)를 통해 3을 03으로 변환하는 작업을 거쳐
겜린더 형식에 맞게 &#39;-&#39;.join을 넣어 2024-03-12 가 최종적으로 출력되도록 하였습니다.</li>
</ul>
<h2 id="닌텐도-페이지-수집">닌텐도 페이지 수집</h2>
<p>사실 이게 제일 난제였습니다.
다른 페이지들은 페이지 주소만 바꿔서 영문 페이지나 한국 페이지로 변경할 수 있었는데
얘네는 각각 독립적으로 사이트를 운영하고 있었습니다...</p>
<p>어.. 그러면 영문 게임 이름 수집을 어떻게 해야하지...?라고 고민을 많이 했습니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/0763487f-f7b3-43ef-8568-aae4e028a766/image.png" alt=""> 고민한 흔적들..</p>
</blockquote>
<p>하지만 고민하면서 사이트를 둘러보고 있을 때
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/9b0f9650-1dfb-46e4-9468-46c3b3394b74/image.png" alt=""> 게임 이름 중에 이렇게 괄호로 영문 이름을 사용한 게임이 있더군요!
그 반대의 경우도 있었습니다. 영어(한국어) 형태로
이거라도 추출해야겠다 싶었습니다. 이게 제 최선이 아닐까 싶었습니다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/cf87ebfb-74f3-4c1a-a162-130c2b279274/image.png" alt=""></p>
<p>이런식으로 추출하는 코드를 만들었는데</p>
<ol>
<li>raw_title에 괄호가 존재하는지 re.search로 확인</li>
<li>match가 True인 경우 추출한 문자열이 영어인지 ASCII 코드로 확인</li>
<li>True면 괄호 안의 문자열은 영어이므로 eng_title에 저장 kor_title은 raw_title에서 괄호를 제거해 저장하였습니다.</li>
<li>match가 False인 경우는 그 반대로 했습니다.</li>
<li>아예 위 조건에 맞지 않는 경우는 그냥 raw_title를 eng, kor 둘 다 저장했습니다.</li>
</ol>
<h2 id="데이터-무결성">데이터 무결성</h2>
<p>데이터 무결성을 위해 여러 작업도 거쳤는데요
한국어로 된 게임 이름은 추후 DB에 넣을 때 무결성이 좋지 못하다고 판단했고
그래서 영문 이름을 기준으로 넣어야겠다는 생각이 들었습니다.</p>
<p>영문 이름을 수집한 이유도 그중 하나였는데요
데이터를 CSV로 전환하는 작업 중 title column 데이터를 autokwd에 있는 영문 이름으로 덮어씌우는 작업까지 해 데이터 무결성을 지킬 수 있었습니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/de5f2b8a-552e-48f4-9a33-0f1b533ae117/image.png" alt="">for 문만 보시면 됩니다.</p>
</blockquote>
<p>왜 concat을 하는지는 <a href="https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%EC%8A%A4%ED%81%AC%EB%9E%98%ED%95%91-%EB%B4%87-%EC%A0%9C%EC%9E%91%EA%B8%B0-Steam">겜린더 스크래핑 봇 제작기 Steam편</a> 참고해주시면 감사하겠습니다... 🙇</p>
<p>최종적으로 CSV 파일로(백업용) 저장되는데 이 파일은 DB로 저장됩니다..</p>
<hr>
<h2 id="save-to-mariadb">Save to MariaDB</h2>
<p>여러 가지 과정을 거쳐 결국 MariaDB에 저장하게 되는데
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/2f4ed36a-f5a0-4f12-8c6f-e6ade70d052f/image.png" alt="">
전체적인 ERD입니다. (
Platform, autokwd, screenshot이 중복되는 데이터가 많기 때문에 2정규화를 거쳤습니다.</p>
<p>Dragon&#39;s Dogma 2를 기준으로 찾아보면</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/53c73a0e-e28f-4263-a2dc-d47685161e9f/image.png" alt=""><img src="https://velog.velcdn.com/images/grit_munhyeok/post/eeaf82f1-0612-4d08-a938-6ad15c3a30f6/image.png" alt=""><img src="https://velog.velcdn.com/images/grit_munhyeok/post/1020fbe3-982e-49b3-8811-d8e3b8436f91/image.png" alt="">platform 테이블</p>
</blockquote>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/fde6800b-9c53-4f79-ac1a-b212ab722895/image.png" alt="">screenshot 테이블</p>
</blockquote>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/cb4dfae7-fc47-495d-815f-74393b55b3f8/image.png" alt="">autokwd 테이블
Dragon&#39;s Dogma 2는 한국어 이름이 없어서 다른 게임으로 대체하였습니다.</p>
</blockquote>
<hr>
<h2 id="transform-data-rdb---json">Transform Data (RDB -&gt; JSON)</h2>
<p>자.. 이제 모은 정보들을 다시 하나로 합쳐 JSON 형태로 변환해 API로 전송해야 합니다.</p>
<p>MongoDB로 보내는 작업을 해야 하는데...</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/12f2a04c-6f20-42f5-b81d-f4213968c961/image.png" alt=""></p>
<p>View를 따로 만들어서 조회하고</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/d03179ae-b2e0-4bef-a5a1-05cbda00d0eb/image.png" alt=""></p>
<p>일부 필요한 데이터를 또 추가로 넣어둔 다음... </p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/89a95e60-34ad-407f-ae79-c3f0df4249e1/image.png" alt="">
Request로 간단하게 넣어주면 MongoDB로 옮기는 작업이 끝나게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/0ba31be6-a857-49bb-9dcc-d2794f5275e4/image.png" alt=""></p>
<p>MongoDB에 이쁘게 옮겨진 모습입니다.
위에 보니 2399개가 저장되었네요 😊</p>
<hr>
<h1 id="의문점">의문점</h1>
<p>실제로 지인들에게 들었던 말인데 </p>
<blockquote>
</blockquote>
<p>&quot;이럴 거면 그냥 CSV에서 데이터 통합해서 JSON으로 보내는 게 더 낫지 않냐! 굳이 왜 RDB같은 거 왜 씀?&quot;
(Pandas 뒀다 뭐 함?)</p>
<p>어.. 물론 맞는 말 같아서 할 말 없었는데 굳이 이유를 설명하자면
RDB에 저장해 두는 게 추후 데이터 관리에 매우 용이할 것 같아서 저렇게 번거로워도 작업했던 것이 가장 큰 이유였고 언제든 데이터 복구를 위해 사용한 것도 있습니다.
(그냥 RDB를 제대로 써보고 싶었던 욕심도 있었습니다.)</p>
<p>겜린더는 라즈베리파이 환경에서 돌릴 예정이기 때문에 여러가지 요인으로 서버가 다운될 수도 있습니다. (정전, SD카드 수명, 라즈베리파이 수명 등..)</p>
<p>그래서 데이터를 지켜야 하는데 관리를 하려면 아무래도 2중으로 DB를 나누는 것이 좋다고 생각했고</p>
<p>MongoDB는 유저가 볼 수 있는 &quot;Contents DB&quot;라고 생각하고
MariaDB는 서버 단에서 데이터를 관리할 수 있는 DB로 생각하고 있었습니다.</p>
<p>물론 이렇게 되면 데이터 불일치(Data Inconsistency) 현상이 발생할 수 있지만</p>
<p>MariaDB는 데이터를 용이하게 관리하고 백업의 목적성이 크기 때문에 일단 이렇게 유지해 볼까 합니다.</p>
<hr>
<h1 id="후기">후기</h1>
<h2 id="되찾은-본질">되찾은 본질</h2>
<p>처음 겜린더를 만들 때도 겜린더의 본질은 무엇일까 생각했었는데
&quot;출시 예정의 게임을 누구나 빠르고 쉽게 볼 수 있었으면 좋겠다&quot;였습니다.
왜냐하면 친구가 게임 스트리머를 준비하는 데 게임을 일일이 스팀에서 찾기 귀찮아했었기 때문이었죠</p>
<p>하지만 그때는 너무 방대한 스팀 게임들에 압도당해 다른 기능을 만든다던가 본질을 좀 회피했었는데
이제서야 본질에 제대로 맞게 플랫폼이 만들어질 것 같습니다.</p>
<h2 id="생각보다-어렵고-복잡했던-데이터-파이프라인-작업">생각보다 어렵고 복잡했던 데이터 파이프라인 작업</h2>
<p>데이터 전처리 작업이 엄청 힘든 작업일 거라곤 생각하지 못했습니다.
여태 간단하게 <a href="https://velog.io/@grit_munhyeok/%EC%98%A4%ED%94%88%EB%A7%88%EC%BC%93-%EC%B5%9C%EC%A0%80%EA%B0%80-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9B%84%EA%B8%B0">수집만하고 이쁘게 표시해주는 크롤링 작업</a>만 해봤었는데요
이렇게 데이터를 하나하나 정제하고 일관성 있게 맞추는 작업까지 하려니 정말 힘든 작업이었지 않나 싶습니다.
예상하지 못하는 변수들이 정말 많은 것 같아요...😂</p>
<p>아직 부족한 점은 많습니다.
사실 DB도 제가 제대로 구성해서 효율적인 구조인지 정확히 잘 몰라서 피드백은 언제나 환영입니다. <strong>댓글로 훈수 좀 해주세요...</strong></p>
<p>작업하다 보니 더러워진 스파게티 코드들은 당연히 정리해야 하고, 제대로 정제되지 않는 데이터도 있어서 그런 부분들을 좀 더 고민해 보고 보강도 해야 합니다.</p>
<p>하지만 여태 테스트를 해봤을 땐 100개 중에 3개(3%) 정도의 데이터가 제대로 정제되지 못해 DB에 중복되어 저장되는 경우도 있었습니다.
(이건 좀 더 수집해보고 확인해야할 것 같습니다.)</p>
<p>이런 부분들은 제가 수 작업을 좀 거쳐 MongoDB로 보내는 작업을 거칠 예정입니다.</p>
<p>그래도 일일이 60개 이상의 게임을 수집하는데 8시간 쓰는것보다 정말 많이 좋다고 생각합니다.</p>
<p>일이 줄었네요 😁</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[출시 예정 게임의 정보를 수집해보자 - Steam편]]></title>
            <link>https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%EC%8A%A4%ED%81%AC%EB%9E%98%ED%95%91-%EB%B4%87-%EC%A0%9C%EC%9E%91%EA%B8%B0-Steam</link>
            <guid>https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%EC%8A%A4%ED%81%AC%EB%9E%98%ED%95%91-%EB%B4%87-%EC%A0%9C%EC%9E%91%EA%B8%B0-Steam</guid>
            <pubDate>Fri, 15 Dec 2023 11:20:33 GMT</pubDate>
            <description><![CDATA[<p>드디어 <a href="https://store.steampowered.com//search/?supportedlang=koreana%2Cenglish&amp;category1=998&amp;filter=popularwishlist&amp;ndl=1">Steam Upcoming</a> 페이지를 스크래핑 하는 시간이 찾아왔습니다.
어떤 전략으로 했고 코드는 어떻게 작성했는지 천천히 리마인드하기 위해 글을 작성합니다.</p>
<blockquote>
<p>핵심만 추려서 정리했으니 자세한 부분은 <a href="https://github.com/munhyok/Gamlendar_Selenium">Github</a> 참고해 주세요!</p>
</blockquote>
<hr>
<h1 id="폴더-구성">폴더 구성</h1>
<h2 id="upcoming">upcoming</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/550a7ae7-8d54-4b9c-a249-b489d7054c7b/image.png" alt="">
플랫폼 별로 폴더를 만들고 플랫폼 별로 Log와 Backup 폴더를 만들어 문제가 발생할 때 언제든 플랫폼 별로 빠르게 확인할 수 있게 만들었습니다.</p>
</blockquote>
<h2 id="core">Core</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/c70a2c96-e852-4e03-be6b-60537f74bc98/image.png" alt="">
자주 사용하는 기능은 core에 따로 분류하였습니다.</p>
</blockquote>
<hr>
<h1 id="어떤-데이터가-필요할까">어떤 데이터가 필요할까?</h1>
<p>겜린더에서 필요한 데이터를 알아보기 위해선 현재 개발 중인 겜린더 앱 상태를 보면...</p>
<p>연보라색의 둥근 사각형이 필요한 데이터입니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/18e77e32-14d0-45cc-8bfd-6645bab605b0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/6b8beee8-ac89-47c1-841f-deeb761edb62/image.png" alt=""></p>
<p>최대한 단순하게 보자면 총 8가지가 나오게 되는데..</p>
<ol>
<li>게임 이름 + 영문 이름</li>
<li>제작사</li>
<li>썸네일</li>
<li>플랫폼 지원</li>
<li>스크린샷</li>
<li>게임의 자세한 정보</li>
<li>유튜브 트레일러 링크 (이건 나중에 🥲)</li>
<li>출시 날짜</li>
</ol>
<p>게임의 영문 이름까지 수집하는 이유
사용자가 한국어로만 검색하지 않기 때문에 영문 이름도 수집합니다.
<a href="https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%EA%B2%80%EC%83%89-%EC%9E%90%EB%8F%99%EC%99%84%EC%84%B1-%EC%84%B1%EB%8A%A5%EC%9D%84-%EA%B0%9C%EC%84%A0%ED%95%B4-%EB%B3%B4%EA%B8%B0">겜린더 검색 자동완성 성능을 개선해보기</a></p>
<hr>
<h1 id="전략">전략</h1>
<ol>
<li><p>목록 데이터 수집</p>
<ul>
<li>게임 이름, 출시 날짜, 상세 페이지 링크</li>
</ul>
</li>
<li><p>각 게임의 상세 페이지 데이터 수집</p>
<ul>
<li>제작사, 썸네일, 스크린샷, 게임의 상세한 정보</li>
<li>성인 게임은 수집 금지</li>
</ul>
</li>
<li><p>데이터 통합</p>
</li>
</ol>
<h1 id="1-목록-데이터-수집">1. 목록 데이터 수집</h1>
<h2 id="분석">분석</h2>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/64ab4b21-d6fd-4544-8c85-05b9288564b7/image.png" alt="">일단 가볍게 페이지를 들어가면 목록들이 나옵니다.</p>
<p>검색에 일치하는 결과가 2,160개가 있다고 나오는데
<strong>&quot;오! 그럼 바로 태그 지정해서 수집하면 그만 아닌가!?&quot;</strong> 라고 생각하면 큰일 납니다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/d3a496f5-37e6-44dc-acaa-647aa2a5eb86/image.png" alt=""> 2,160개의 정보를 한 번에 띄우는 것이 아닌 스크롤을 내리면 데이터를 받아오는 구조이기 때문에</p>
<blockquote>
<p><strong>위의 사항을 고려해 전략을 세우면</strong></p>
</blockquote>
<ol>
<li><strong>스크롤 전의</strong> 페이지 전체 높이 값을 알아낸다. (currnetPageHeight)</li>
<li>페이지 스크롤을 한다.</li>
<li><strong>스크롤 후의</strong> 페이지 전체 높이 값을 알아낸다. (newPageHeight)</li>
<li>스크롤 전과 스크롤 후의 페이지 높이가 같은지 비교한다.</li>
<li>같지 않다면 아직 불러올 컨텐츠가 있다는 의미이기 때문에 스크롤을 계속한다.</li>
<li>만약 값이 같다면 스크롤을 다 했다는 의미로 해석해 스크롤을 중지한다.</li>
</ol>
<p>이런 부분을 인지하고 설계한 플로우차트를 보면
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/15bc9632-f164-4c33-91c1-69ef3018de43/image.png" alt=""></p>
<blockquote>
<p>currentPageHeight : 현재 페이지 높이
newPageHeight : 스크롤 후 페이지 높이</p>
</blockquote>
<blockquote>
<p>두 변수의 높이 값이 다르다면 게임 이름, 출시 날짜 수집
두 변수의 높이 값이 같다면 스크롤 중지</p>
</blockquote>
<p>위의 설계를 코드로 구현해 봅시다!</p>
<h2 id="코드-구현">코드 구현</h2>
<ul>
<li>페이지 언어 한국어로 바꿔주기
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/2511b828-cd32-4f60-a3ae-a962bb5ec429/image.png" alt=""> 가장 먼저 수집하기 전에 driver가 실행되고 페이지를 불러오면 영문으로 표시되는데 페이지를 한국어로 바꿔줘야 합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/2faaf31e-9fe5-40da-b33d-45d91299f1f2/image.png" alt=""></p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/0dbbe117-28a1-4d03-9eae-88440f91a0e3/image.png" alt="">my_game에 게임 이름, 출시 날짜, 상세 페이지 링크까지 수집</p>
</blockquote>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/493e13a2-bb87-4198-9093-6e46e9e073ab/image.png" alt="">실제 결과 값</p>
</blockquote>
<p>목록 데이터 수집은 이렇게 마무리하고 이제 상세 페이지 링크를 활용해
상세 페이지를 수집해 봅시다!</p>
<hr>
<h1 id="2-각-게임의-상세-페이지-데이터-수집">2. 각 게임의 상세 페이지 데이터 수집</h1>
<p>이제 상세 페이지 수집을 위해 어떻게 해야할지 고민해 봅시다.</p>
<p>일단 저는 대전제 1가지는 무조건 걸었습니다.</p>
<blockquote>
<p><strong>&quot;노출이 정말로 심한 성인 게임은 최대한 수집을 하지 않는다.&quot;</strong></p>
</blockquote>
<p>이 부분을 어떻게 해결했는지 천천히 과정을 설명드리면서 진행하겠습니다.</p>
<h2 id="상세-페이지-분석">상세 페이지 분석</h2>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/b4dd4b97-19db-4f5e-b987-11c8075a9274/image.png" alt="">잠깐 숨은 그림 찾기를 해볼까 합니다.
여기서 찾을 수 있는 요소는 총 4가지가 존재합니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/a6b79170-0c2c-4e63-8d7a-209e9a86f9fd/image.png" alt="">
 게임 이름(영문 이름), 개발자, 태그, 스크린샷</p>
</blockquote>
<p>또 하단으로 내리면 게임의 상세 정보가 나옵니다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/d001bbf5-626d-4db2-9aba-b95c5c67fdbd/image.png" alt=""></p>
<p>그럼 총 5가지를 수집할 수 있네요!</p>
<p>수집한 상세 페이지 데이터와 목록 페이지 데이터를 하나로 통합해 CSV 파일로 통합하는 과정까지 해봅시다!</p>
<h2 id="코드-구현-1">코드 구현</h2>
<h3 id="성인-게임-인증">성인 게임 인증</h3>
<p>스팀 성인 게임 인증은  특징이 있습니다.
한 번만 인증하면 모든 성인 게임 열람이 가능합니다.</p>
<p>그래서 저는 GTA 5의 페이지로 먼저 성인 게임 인증을 하고 상세 페이지를 수집하기 시작했습니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/4f8cb3a0-3794-4ea9-9f15-0caf6429af2a/image.png" alt=""> 성인 게임 인증</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/3f312fa7-dcfd-4bd3-9f97-6f1f85f21fca/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/b51140ea-8c45-43a9-a3ee-6967b3536cd1/image.png" alt=""></p>
<h3 id="영문-이름-수집-방법">영문 이름 수집 방법</h3>
<p>영문 이름을 수집하기 위해서 webdriver를 2개를 실행해 진행하기로 했습니다.</p>
<p>driver 1개로 하면 계속 언어를 바꿔가면서 수집해야 하는데 언어 전환할 때 소요시간이 약 3초 정도가 소요됩니다.</p>
<p>단순하게 3초 * 2000개의 게임 = 6000초의 시간이 그냥 소비가 되기 때문에
2개로 실행하는 것이 더 합리적이라 판단했습니다.</p>
<blockquote>
<p>driver : 한국어
driver_eng : 영어</p>
</blockquote>
<p>driver은 이미 목록 데이터 수집할 때 한국어로 변경하였고
driver_eng는 새로운 프로세스로 실행이 되기 때문에 페이지를 불러올 때 영문 페이지가 나옵니다.</p>
<h3 id="성인-게임-수집-시-문제점과-해결-방안">성인 게임 수집 시 문제점과 해결 방안</h3>
<blockquote>
<p><strong>&quot;노출이 정말로 심한 성인 게임은 최대한 수집을 하지 않는다.&quot;</strong></p>
</blockquote>
<p>성인 게임은 최대한 보수적으로 수집한다고 했지만
우리가 하는 게임 중에 성인 인증이 필요한 게임은 생각보다 많았습니다.
(GTA, 용과 같이 시리즈 등..)</p>
<p>그리고 분명 노출이 엄청 심한 성인 게임인데도 오히려 성인 인증을 거치지 않는 게임들이 많았기 때문에 이걸 필터링을 어찌해야 할까... 고민을 정말 많이 했습니다.</p>
<p>그래서 생각한 방안 중 하나가 태그를 이용한 필터링을 생각해냈습니다.</p>
<p>정말 노출이 심한 성인 게임을 찾아보면서 공통적으로 들어가는 태그가 있었는데
대표적으로 <strong>&#39;헨타이&#39;,&#39;후방주의&#39;,&#39;선정적인 내용&#39;</strong> 이런 태그가 무조건 상위 태그에 존재하는 사실을 알았습니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/ec213c7f-536d-4972-a558-4efb19649e15/image.png" alt=""> 실제로 필터링 되었던 게임의 태그...</p>
</blockquote>
<p>그래서 저런 게임들을 필터링하기 위해 태그를 수집했고 위의 3개 중 1개라도 존재하면 수집하지 않는 방향으로 구성하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/f6401d4e-257b-4581-b4ce-8943adfa6bda/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/0959f692-3d99-4e51-8bd4-0fad31cd263a/image.png" alt=""></p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/5807028a-6a2f-49ed-964f-923194abdca1/image.png" alt=""> 수집 완료된 데이터
이제 하나로 통합해 봅시다</p>
</blockquote>
<h3 id="목록--상세-페이지-데이터-하나로-통합시키기">목록 + 상세 페이지 데이터 하나로 통합시키기</h3>
<p>데이터를 통합시키는 건 Pandas의 concat 함수를 이용해 하나로 통합시켰습니다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/9e5d1770-d5b9-44b6-bd29-c6d82786b322/image.png" alt=""></p>
<p>백업을 위한 csv까지 생성할 수 있게 하였고
이 함수는 자주 사용하기 때문에 Core에 따로 분리해 언제든 불러와 사용할 수 있게 만들었습니다.</p>
<hr>
<h1 id="3-결과">3. 결과</h1>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/73ca7f05-1116-43a5-a757-f695c67d0b38/image.png" alt="">
csv 파일은 깃허브에서 확인해 주시면 감사하겠습니다 :)</p>
</blockquote>
<p>깔끔하게 csv로 통합해서 결과물이 나오는데
이 데이터를 추후에는 MariaDB에 정리할 예정입니다.</p>
<p>아무래도 이런 데이터는 MongoDB보단 RDB인 MariaDB로 해야 가독성이나 추후 join을 이용한 데이터 조회에도 도움이 될 것 같다고 판단했습니다.</p>
<hr>
<h1 id="4-후기">4. 후기</h1>
<p>이제 겨우 스팀의 데이터를 수집했지만 할 일이 정말 많습니다.</p>
<p>겜린더라는 프로젝트를 하면서 많이 느낀 부분은
원하는 기능을 구현하기 위해 여러 가지 조사를 하면서</p>
<p><strong>내가 이 기술을 왜 써야 하는가에 대해 항상 스스로 질문하고 합리적인 판단을 하는 과정이</strong></p>
<p>정말 저에겐 공부할 목적을 만들 수 있어 정말 좋은 것 같습니다.</p>
<p>단, 조심해야 할 건
단순히 구현하는 것에 목적을 두는 게 아닌 제대로 원리를 배워가면서 추후 협업을 위한 가독성이 좋은 코드를 작성하는 것을 목표로 프로젝트를 진행해야겠네요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[겜린더 스크래핑 봇 제작기 [시작하기 전에]]]></title>
            <link>https://velog.io/@grit_munhyeok/%EC%8A%A4%ED%81%AC%EB%9E%98%ED%95%91-%EB%B4%87-%EC%A0%9C%EC%9E%91%EA%B8%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-%EC%A0%84%EC%97%90</link>
            <guid>https://velog.io/@grit_munhyeok/%EC%8A%A4%ED%81%AC%EB%9E%98%ED%95%91-%EB%B4%87-%EC%A0%9C%EC%9E%91%EA%B8%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-%EC%A0%84%EC%97%90</guid>
            <pubDate>Sat, 09 Dec 2023 08:01:11 GMT</pubDate>
            <description><![CDATA[<h1 id="스크래핑-봇-제작-이유">스크래핑 봇 제작 이유</h1>
<p>드디어 겜린더의 핵심이라고 할 수 있는 게임 데이터를 수집하려고 한다.</p>
<p>게임 데이터를 하나하나 수기로 작성했던 입장에서 너무 힘들었던 기억이 있기 때문에 꼭 데이터 수집을 자동화시키고 출시하고 싶었던 목표가 있었다.</p>
<p>하지만 <strong>부랴부랴 출시만 했던 서비스</strong>라 백엔드에 아주 많은 문제점이 있었고, 결국 처음부터 다시 제작했었기 때문에 시간이 많이 걸렸다. <del>(생각보다 많은 시행착오를 겪은 것 같다.)</del></p>
<blockquote>
<h2 id="시행착오-흔적들"><strong>시행착오 흔적들</strong></h2>
<p><a href="https://velog.io/@grit_munhyeok/Redis-%EC%8A%A4%ED%84%B0%EB%94%94-%EB%B0%8F-%EA%B2%9C%EB%A6%B0%EB%8D%94-%EC%84%9C%EB%B2%84-%EC%84%A4%EA%B3%84">Redis 스터디 및 겜린더 서버 설계</a>
<a href="https://velog.io/@grit_munhyeok/Redis-JSON-FastAPI%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%B4-CRUD-%EB%A7%8C%EB%93%A4%EA%B8%B0">Redis와 FastAPI를 활용해 CRUD 만들기</a>
<a href="https://velog.io/@grit_munhyeok/23.01.28-%EA%B2%9C%EB%A6%B0%EB%8D%94-%EA%B0%9C%EB%B0%9C-%EC%9D%BC%EC%A7%80">겜린더의 DB의 문제점과 고쳐야할 것들</a>
<a href="https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%EB%B0%B1%EC%97%94%EB%93%9C-%EB%AC%B8%EC%A0%9C-%EC%9D%B8%EC%8B%9D%EA%B3%BC-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%A1%B0%EC%82%AC">MongoDB는 겜린더에 적합한 DB일까?</a></p>
</blockquote>
<p>그리고 우연히 용산에서 알바를 하다가 사장님도 데이터 수집에 관심이 있으셔서
Selenium을 활용해 여러 데이터 스크래핑 프로그램을 만들 기회가 있었는데, 만드는 과정 중 겪었던 경험을 기반으로 이번 스크래핑 봇을 잘 만들어볼까 한다.</p>
<blockquote>
</blockquote>
<p><strong>과거에 스크래핑 봇을 제작할 때 내 코드를 보면서 아쉬웠던 점을 몇 개 뽑자면...</strong></p>
<ol>
<li>기능 별로 나누지 못해 코드 가독성 최악</li>
<li>무식한(?) 예외 처리</li>
<li>어차피 내가 개발하고 유지 보수하는 거라 협업 따위 생각하지도 않은 변수명과 구조...</li>
</ol>
<p>하지만 겜린더는 추후에 같이 해볼 사람이 있을 것을 고려하여
내가 할 수 있는 최선을 다해 코드를 잘 작성해 보려 한다.</p>
<hr>
<h1 id="그래서-어떤-사이트를-수집할-것인가">그래서 어떤 사이트를 수집할 것인가?</h1>
<p><a href="https://store.steampowered.com//search/?filter=popularwishlist&amp;os=win">Steam Upcoming</a></p>
<p><a href="https://store.playstation.com/ko-kr/category/82ced94c-ed3f-4d81-9b50-4d4cf1da170b/1?next_thirty_days=conceptReleaseDate">Playstation Upcoming</a></p>
<p><a href="https://www.xbox.com/ko-kr/games/all-games?cat=upcoming">Xbox Upcoming</a></p>
<p><a href="https://www.nintendo.co.kr/schedule">Nintendo Release Schedule</a></p>
<hr>
<h1 id="개인적인-목표">개인적인 목표</h1>
<p>항상 글을 작성할 때마다 아쉬웠던 점은 글도 글대로 못 쓰지만 시각화를 잘 못해서 아쉬움이 많았다.
나중에 내가 봐도 <strong>&quot;아니 이걸 보고 누가 이해를 할 수 있으려나?&quot;</strong> 라는 스스로의 의문도 많이 생겼었다.</p>
<p>그래서 이번엔 문서화도 잘해야겠지만 정확히 로직이 어떻게 돌아가는지 쉽게 알아볼 수 있는 시각화 자료를 만들어보고 싶다는 목표가 생겼다.</p>
<hr>
<h1 id="여담">여담</h1>
<h2 id="테스트-코드의-중요성">테스트 코드의 중요성</h2>
<p>사실 좀 더 빠르게 프로젝트를 진행할 수 있었는데 하도 설계를 잘해보겠다고 노션에 정리도 해보고 고심도 많이 했지만 결국 테스트 코드 몇 줄 작성해 그에 맞게 설계도 바꿔보고 하면서 진행하는 것이 뭔가 노션에 뜬구름 잡는 것보다 나았다.</p>
<hr>
<h1 id="github">Github</h1>
<h2 id="gamlendar_selenium"><a href="https://github.com/munhyok/Gamlendar_Selenium">Gamlendar_Selenium</a></h2>
<p>상세한 전략과 정리는 깃허브에...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis를 이용해 검색 자동완성 성능을 개선해 보기 [겜린더]]]></title>
            <link>https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%EA%B2%80%EC%83%89-%EC%9E%90%EB%8F%99%EC%99%84%EC%84%B1-%EC%84%B1%EB%8A%A5%EC%9D%84-%EA%B0%9C%EC%84%A0%ED%95%B4-%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%EA%B2%80%EC%83%89-%EC%9E%90%EB%8F%99%EC%99%84%EC%84%B1-%EC%84%B1%EB%8A%A5%EC%9D%84-%EA%B0%9C%EC%84%A0%ED%95%B4-%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 11 Sep 2023 10:35:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%EB%B0%B1%EC%97%94%EB%93%9C-%EB%AC%B8%EC%A0%9C-%EC%9D%B8%EC%8B%9D%EA%B3%BC-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%A1%B0%EC%82%AC">겜린더 백엔드 문제 인식과 문제 해결을 위한 조사</a>에서 이어지는 글 입니다.</p>
</blockquote>
<h1 id="요약">요약</h1>
<ol>
<li><p>Redis JSON에서 해야 했던 <strong>Indexing(색인)</strong> 작업이 사라져 성능을 개선</p>
</li>
<li><p>JSON 데이터가 아닌 <strong>Sorted Set 형태로 저장해 용량 문제 개선</strong></p>
</li>
<li><p>캐싱 작업을 통해 MongoDB의 부하를 대폭 감소</p>
</li>
<li><p>Redis-Stack에 대한 의존성 낮추기...</p>
</li>
</ol>
<blockquote>
<p><strong>피드백은 언제나 환영입니다. :)</strong></p>
</blockquote>
<hr>
<h1 id="개요">개요</h1>
<p>겜린더의 메인 DB를 Redis에서 MongoDB로 전환하면서 한 가지 문제점이 생겼습니다.
<strong>검색 기능을 어떻게 구현해야 할까?</strong></p>
<p>Redis에선 편하게 <a href="https://redis.io/docs/data-types/json/indexing_json/">ReJSON</a>을 통해 index와 search 기능을 제공합니다.
덕분에 과거 작성한 글 중에 실제로 적용해 구현까지 완료했었죠</p>
<blockquote>
<p><a href="https://velog.io/@grit_munhyeok/Redis-JSON-FastAPI%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%B4-DB%EB%A7%8C%EB%93%A4%EA%B8%B0">Redis와 FastAPI를 활용해 DB 만들기</a></p>
</blockquote>
<blockquote>
<p><strong>새로 제작 중인 겜린더 앱 일부</strong>
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/44a58958-96f9-411d-a0ab-6b1cd04d2740/image.gif" alt=""></p>
</blockquote>
<hr>
<h1 id="문제점--개발-방향-정하기">문제점 &amp; 개발 방향 정하기</h1>
<p>하지만 이번 MongoDB로 전환하는 과정에서 단순하게 쿼리 검색으로 자동완성 기능을 구현하면 큰일 날 것 같은 기분이 들었습니다.</p>
<p>자동완성 기능은 텍스트를 입력할 때마다 사용자에게 완성된 키워드를 줘야 했는데 <strong>In-Memory 특징 덕분에 읽기 속도가 빨랐던 Redis의 성능만 믿고 ReJSON의 기능을 이용했지만,</strong></p>
<p>MongoDB는 <strong>Disk에 저장하는 방식</strong>이기 때문에 분명 성능 상 큰 문제가 발생할 것이라고 판단했습니다.</p>
<p>그래서 방안을 고민하던 과정을 짧게 적어볼까 합니다.</p>
<h2 id="cache-warming">Cache Warming</h2>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/ef23a802-33df-43d6-8695-94e18fa42cae/image.png" alt=""></p>
<p>Cache Warming을 통해 예상 검색어를 미리 Redis에 저장해두는 방식을 고민했는데</p>
<p>과거에 ReJSON을 사용하는 <strong>Redis-stack이 Ubuntu 버전(jammy)를 지원하지 않는다</strong>는 대참사를 겪어본 적이 있어서 차마 적용하기가 좀 두려웠습니다.</p>
<blockquote>
<p>하지만 검색해 보니 이젠 지원하는 것 같네요.. 😂 
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/a08bac8e-7fa9-4ec6-ba54-9ca4e51aabaf/image.png" alt=""></p>
</blockquote>
<blockquote>
<p>예전엔 지원하지 않았는데... 🥺
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/8f5ab8a0-84ba-4aac-9fa7-837c070136b0/image.png" alt=""></p>
</blockquote>
<h2 id="굳이-redis-stack을-써야-할까">굳이 Redis-Stack을 써야 할까..?</h2>
<p>Cache Warming은 결국 여러 문제점으로 인해 아이디어를 폐기하고..</p>
<p>Redis-Stack을 사용하지 않고
<strong>Redis-Cli로 충분히 기능을 구현할 수 있지 않을까?</strong> 라는 생각을 했으나 String 형식으로는 검색 기능을 사용할 수 없었습니다.</p>
<p>그러다가 문득 전에 Redis를 공부하면서 Redis의 Sorted Set이라는 데이터 타입이 생각났습니다.</p>
<blockquote>
<p><a href="https://velog.io/@grit_munhyeok/Redis-%EA%B3%B5%EB%B6%80-2">Redis 공부 - 2</a>
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/23b1e750-6f16-4245-a355-c82d8a7a1c13/image.png" alt=""></p>
</blockquote>
<p><strong>자동완성 시스템 구축?</strong> 어떻게 가능하지? 하면서 다시 복습을 해봤는데</p>
<p><strong>ZRANGE</strong>로 구현할 수 있다는 사실을 알게 되었습니다.</p>
<p><strong>ZRANGE</strong>를 사용하면 부하도 줄이고 매우 적절하게 기능을 구현할 수 있지만 기존 로직과는 약간 다르게 설계를 해야 하는 리스크가 있었지만 이건 꼭 개선해 보고 싶은 생각에 다시 설계해 보았습니다.</p>
<hr>
<h1 id="기존-로직과-새로운-로직의-차이점">기존 로직과 새로운 로직의 차이점</h1>
<h2 id="기존-설계">기존 설계</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/75837cc2-576f-4a86-a0ec-d3c2cf270cfe/image.png" alt="">
FT.SEARCH를 이용해 DB에 autokwd에 있는 값과 <strong>유사한 JSON 데이터 전부를 가져오는 방식</strong></p>
</blockquote>
<h3 id="문제점">문제점</h3>
<p>유사한 데이터를 가져오는데 데이터 일부만 가져오는 것이 아닌 모든 데이터를 가져옵니다.</p>
<p>덕분에 기존 검색 기능에서 이미지와 각종 데이터를 바로 불러올 수 있어 UI&amp;UX 면에서 직관성은 매우 좋다고 생각했으나...</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/41969e70-42e0-4fdf-8dec-71ee5e5ee434/image.gif" alt=""></p>
<p>이렇게 하면 비용적인 측면에서 매우 좋지 못하는 것을 알게 되었죠...</p>
<p>가져오는 <strong>JSON 데이터가 커지면 그만큼 대역폭과 데이터 송수신 시간이 늘어나기 때문에 추후 서버 부하 문제</strong>로 고생할 것 같은 미래가 떠올랐습니다.
<strong>(라즈베리파이로 돌릴 예정이라 더욱... 성능 이슈가...)</strong></p>
<hr>
<h2 id="새로운-설계">새로운 설계</h2>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/d936f1c7-f582-48c8-9dee-f2c0c2966850/image.png" alt="">
<del>더 복잡해 보이는 건 기분 탓...이 아니다.</del></p>
<p>부분적으로 코드와 함께 설명을 해보자면...</p>
<hr>
<h3 id="mongodb와-redis에-데이터-저장">MongoDB와 Redis에 데이터 저장</h3>
<blockquote>
<p><strong>&quot;example&quot;이란 이름의 게임이 있다고 가정해 보겠습니다.</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/b8adf5ce-5d9d-4e22-bce4-5375b90b5575/image.png" alt="">Redis에는 <strong>autocomplete</strong>라는 Sorted Set에 &quot;example&quot;를 추가합니다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/ffa0ef16-ea50-4eda-a0d2-6a3d10279988/image.png" alt="">(예시로 example_1, example_2를 추가했습니다.)</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/58721782-5efb-4b11-ae3d-e4ad1e85fd57/image.png" alt=""> MongoDB에는 수집한 게임 데이터를 저장합니다.</p>
<h4 id="코드-구현">코드 구현</h4>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/6aef183f-13f7-42d1-a8d8-d14298630936/image.png" alt=""></p>
<hr>
<h3 id="자동완성-키워드-불러오기">자동완성 키워드 불러오기</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/b18e06ca-5bf0-4dca-96df-941e92c70396/image.png" alt=""></p>
<p>사용자는 겜린더에 <strong>&quot;exam&quot;이라는 텍스트만 입력했을 때를 가정</strong>하고
<strong>ZRANGE를 통해 &quot;exam&quot;이 포함된 텍스트를 찾게 됩니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/41d11787-ce2e-4986-b0d5-bcbca5a7f79e/image.png" alt=""></p>
<p>이미지 하단에 Response body을 보면 <strong>전에 추가한 example, example_1, example_2까지 전부 나오게 됩니다.</strong></p>
<h4 id="코드-구현-1">코드 구현</h4>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/860e25c1-f6b6-40b5-aec7-7f37cc7fba9a/image.png" alt=""></p>
<p><strong>유의할 점</strong>
Redis는 항상 결과물이 <strong>utf-8</strong>형식으로 콘솔에 출력이 됩니다.
영어로 검색할 때는 크게 문제가 되지 않지만 한국어는 제대로 검색되지 않는 문제가 발생하기 때문에 <strong>text를 utf-8로 인코딩을 먼저 거쳐야 합니다.</strong></p>
<hr>
<h3 id="불러온-키워드로-검색하고-캐싱-하기">불러온 키워드로 검색하고 캐싱 하기</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/f80395e2-8257-48eb-ba49-4c8ad6486878/image.png" alt=""></p>
<p><strong>example를 사용자가 선택해 검색을 시도하면 바로 MongoDB를 통해 &quot;example&quot;을 검색합니다.</strong></p>
<p>검색 결과를 <strong>Redis에 5분 동안 캐싱 하는 작업</strong>을 거쳐 최종적으로 사용자에게 결과를 보여주게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/ab7e5ea5-0820-4283-9108-17ad32756f79/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/cd1aa028-1840-41ac-886b-7139a4c84148/image.png" alt=""></p>
<p><strong>이렇게 캐싱을 하게 되면 순간적으로 검색어가 몰려 검색 시도가 발생할 때 MongoDB에 부하를 줄일 수 있다고 생각했습니다.</strong></p>
<h4 id="코드-구현-2">코드 구현</h4>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/d10dfedc-7a1a-4118-88da-58b0e63e004c/image.png" alt=""></p>
<hr>
<h1 id="성능-개선-정리">성능 개선 정리</h1>
<ol>
<li><p>기존 Redis JSON은 DB와 자동완성 기능 둘 다 수행했기 때문에 큰 트래픽이 발생하면 두 기능 전부 작동이 안됄 수 있었던 부분 개선</p>
</li>
<li><p>JSON 데이터가 아닌 <strong>Sorted Set 형태로 저장해 용량 문제도 개선</strong></p>
</li>
<li><p>만약 위에서 제시한 Cache Warming 작업으로 시도했다면 똑같이 JSON을 Indexing 해서 결과물을 찾아야 했기 때문에 성능이나 비용적으로 비효율적</p>
</li>
<li><p>캐싱 작업을 통해 MongoDB의 부하를 대폭 감소</p>
</li>
<li><p>Redis-Stack을 사용하지 않아도 된다! <strong>Redis-Stack에 대한 의존성 낮추기...</strong></p>
</li>
</ol>
<hr>
<h1 id="후기">후기</h1>
<p>뭔가 별거 없어 보이지만, <strong>Redis를 개인적으로 공부하고
공부한 내용을 잘 활용해서 문제를 해결했다는 점</strong>이 정말 뿌듯했습니다.</p>
<p>물론 로직이 바뀌면서 프론트도 다시 작업을 해야 하지만
<strong>무조건 개선해야 할 부분이었고 적절하게 트레이드오프 했다고 생각합니다.</strong></p>
<p>다음 글은 최종적으로 <strong>백엔드를 어떻게 설계했고,</strong>
<strong>기존 백엔드와 성능 비교 테스트도 해보면서 정리하는 글을 작성할 예정입니다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MongoDB는 겜린더에 적합한 DB일까?]]></title>
            <link>https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%EB%B0%B1%EC%97%94%EB%93%9C-%EB%AC%B8%EC%A0%9C-%EC%9D%B8%EC%8B%9D%EA%B3%BC-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%A1%B0%EC%82%AC</link>
            <guid>https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-%EB%B0%B1%EC%97%94%EB%93%9C-%EB%AC%B8%EC%A0%9C-%EC%9D%B8%EC%8B%9D%EA%B3%BC-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%A1%B0%EC%82%AC</guid>
            <pubDate>Sat, 26 Aug 2023 12:47:13 GMT</pubDate>
            <description><![CDATA[<hr>
<blockquote>
<p>이 글에는 코드가 없습니다.
현재 문제점과 어떻게 해결해야 할지 고민한 글입니다.</p>
<p>다음 글에 전체적인 백엔드 구조와 함께 정리해서 올릴 예정입니다.
아마 다음 글이 최종 백엔드 설계가 아닐까 싶네요 :)</p>
</blockquote>
<hr>
<h1 id="문제인식">문제인식</h1>
<p>현재 겜린더의 DB는 Redis로 개발하고 있다.</p>
<p>Redis는 In-Memory 방식이기 때문에 I/O 성능이 정말 빠르지만 반면에 외부요인으로 인해 데이터 손실이 발생할 수 있는 리스크가 존재한다.</p>
<p>그리고 개발과 함께 Redis를 공부하면서 문득 들었던 생각은</p>
<blockquote>
<p><strong>&quot;많은 게임 관련 정보를 담아야 하는데 Redis의 메모리 용량으로 감당이 가능할 수 있을까?&quot;</strong> 와</p>
</blockquote>
<blockquote>
<p><strong>&quot;라즈베리파이의 RAM 용량은 8GB인데 이 8GB를 전부다 사용하면 Swap이 발생할 테고 이러면 어차피 느려지지 않을까?&quot;</strong> 라는 의문이 생겼다.</p>
</blockquote>
<p>물론 위의 문제를 해결할 수 있는데, 데이터 손실 문제는 RDB, AOF 방식의 백업을 이용해 추후 외부적인 요인으로 데이터 손실이 발생했을 때 복구할 수 있다.</p>
<p>아니면 <strong>Redis Cloud</strong>를 사용하면 위의 의문들과 데이터 손실 걱정이 모두 해결된다. 하지만 매달 낼 돈이 없기도 하고...🥲
데이터와 트래픽이 늘어날수록 비용이 엄청나다는 글을 읽어버렸다.</p>
<p><a href="https://engineering.linecorp.com/ko/blog/LINE-integrated-notification-center-from-redis-to-mongodb">LINE 알림 센터의 메인 스토리지를 Redis에서 MongoDB로 전환하기</a></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/80011540-81b4-4884-9ea4-92d698272e78/image.png" alt=""></p>
<blockquote>
<p>기존에 LIN은 Redis를 메인 스토리지로 사용하면서 알림 관련 주요 데이터를 Redis에 저장해 서빙하고 있었습니다. 하지만 늘어가는 알림 종류와 스펙, 새 계정 유입 등의 영향으로 Redis 사용률이 지속적으로 증가하면서 결국 가용량의 80% 이상을 사용하는 상황까지 도달했습니다. 이에 따라 스왑 인/아웃(swap in/out)이 발생하면서 Redis 페일오버(failover) 또한 발생하는 상황이었고요.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/22b2c4d8-8ca5-4eaa-894d-e5d0c613af44/image.png" alt=""></p>
<p>내가 우려했던 상황이 그대로 LINE에서도 겪었던 문제였다.</p>
<p>그래서 Redis는 캐시 서버, MySQL 같은 디스크에 저장하는 DB를 사용하는 것이 적절하다고 판단했다.</p>
<p>*<em>그럼 다시 만들어야겠네...? 거의 다 개발했는데... *</em> 😂</p>
<hr>
<h1 id="문제-해결을-위한-조사">문제 해결을 위한 조사</h1>
<h2 id="sql를-제대로-다루지-못하는-나">SQL를 제대로 다루지 못하는 나..</h2>
<p>사실 난 SQL을 제대로 다루지 못한다.
그래서 갑작스럽게 RDB를 만들면 너무 조잡하고 효율적이지 못한 DB를 구축할 것 같았다.</p>
<p>실제로 MariaDB를 설치해 보고 대충 테이블을 만들었으나… 내가 제대로 만들고 있는지도 감이 잡히지 않았다.</p>
<p>그래서 정석으로 배우자!라는 기분으로 책을 하나 구매해서 개념 정리를 하고 있었다.</p>
<h2 id="rdb-vs-document">RDB vs Document?</h2>
<p>겜린더는 JSON 형태로 불러오는데 지인이 차라리 RDB 말고 Document 형식의 MongoDB를 사용하는 것이 어떻겠냐고 조언을 해주었다.</p>
<p>사실 아주 예전에 겜린더를 처음 개발할 때 MongoDB를 사용해보고 싶었지만, 그 당시의 나에겐 너무 복잡하고 당장 앱 만들기도 힘들어서 보류했었는데</p>
<p>MongoDB의 자세한 스펙과 겜린더에 적절한지 판단하기 위해 이 기회에 제대로 조사해 보았다.</p>
<h2 id="겜린더의-데이터-저장-방식">겜린더의 데이터 저장 방식</h2>
<p>겜린더는 기본적으로 JSON 형식으로 저장을 한다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/0dbac22d-aaa4-4963-8413-977d68263cf7/image.png" alt=""></p>
<p>일단 JSON으로 저장하는 가장 큰 이유가
<strong>서비스 확장 혹은 원하는 기능을 넣을 때 데이터 구조를 자유롭게 변경했어야 했기 때문에 JSON으로 저장했었다.</strong></p>
<h3 id="mongodb의-bson-binary-json">MongoDB의 BSON (Binary JSON)</h3>
<p>MongoDB의 장점 중 BSON(Binary JSON)이 있었는데 JSON Document를 Binary로 인코딩한 포맷이다.</p>
<p>왜 Binary로 인코딩을 했는지 알아보았는데 기존 JSON으로 저장하면 텍스트 기반이라 구문 분석이 느리고 공간 효율성이 떨어졌기 때문에 컴퓨터가 읽기 좋게 Binary 포맷으로 저장해 구문 분석과 공간 효율 단점을 해결하였다.</p>
<p>공식 스펙 사이트가 있다.
<a href="https://bsonspec.org/">BSON Spec</a></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/a469babe-673e-4b5e-8689-6b0a43b526ef/image.png" alt=""></p>
<p>이 3가지가 대표적인 특징이라는데</p>
<ol>
<li><p>경량화</p>
<ul>
<li><p>경량화는 모든 데이터 표현 형식에서 매우 중요한 요소</p>
</li>
<li><p>특히 네트워크를 통해 사용되는 경우는 더욱 중요하다.</p>
</li>
<li><p>그만큼 트래픽을 줄이고 공간을 최대한 활용할 수 있다는 의미이기 때문이다.</p>
</li>
</ul>
</li>
<li><p>횡단 가능</p>
<ul>
<li><p>BSON은 쉽게 탐색할 수 있도록 설계되어 있다.</p>
</li>
<li><p>기본 데이터 표현이 필요한 MongoDB에 있어서 필수적인 속성이다.</p>
</li>
</ul>
</li>
<li><p>효율적</p>
<ul>
<li>BSON으로 인코딩하고 BSON에서 디코딩 하는 것이 C 데이터 유형을 사용하기 때문에 대부분의 언어에서 매우 빠르게 수행된다.</li>
</ul>
</li>
</ol>
<ol start="4">
<li><p>이 밖에도</p>
<ul>
<li>JSON 보다 더 많은 데이터 타입을 사용할 수 있다.</li>
</ul>
</li>
</ol>
<p>이런 특징을 보고 RDB보단 Document 방식에 자유롭게 데이터 구조를 변경할 수 있는 MongoDB를 사용하는 것이 더 좋다고 판단하였다.</p>
<hr>
<h2 id="데이터-모델링">데이터 모델링</h2>
<p>Ref.
<a href="https://meetup.nhncloud.com/posts/276">(NHN Cloud) mongoDB Story 3: mongoDB 데이터 모델링</a></p>
<p><a href="https://fastcampus.co.kr/story_article_yhs">LINE+가 사내에 MongoDB를 도입한 이유</a></p>
<p>일단 데이터 모델링의 정확한 정의를 짚고 넘어가자</p>
<p>데이터 모델링이란 데이터를 정확하고 효율적으로 데이터베이스에 저장하기 위해 데이터 구조를 설계하는 과정을 의미한다.</p>
<p>RDB의 데이터 모델링은 Table 설계 후 Column을 설계하는 순서이지만, MongoDB는 Document를 설계 후 Collection을 설계하는 순서로 진행된다.</p>
<p>물론 그 이전에 요구사항과 필요한 데이터가 무엇인지 분석해야 하지만 이미 Redis로 설계하면서 필요한 데이터와 요구사항을 정립해뒀기 때문에 넘어간다.</p>
<h3 id="document-구조">Document 구조</h3>
<p>MongoDB에는 Document 구조가 2가지 존재한다.</p>
<h4 id="1-embedding">1. Embedding</h4>
<p>관계를 갖는 데이터 집합을 단일 Document에 포함하여 저장하는 방식</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/23d1e049-111c-453c-a73c-065c536c7f66/image.png" alt=""></p>
<p><strong>장점</strong></p>
<ul>
<li>데이터 관리가 직관적이고 쿼리가 단순</li>
<li>조회 성능이 좋고 도큐먼트에 포함된 관련 데이터를 모두 업데이트할 수 있다.</li>
<li>구조가 단순해 반정규화 모델이다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li><p>구조가 단순하고 조회 성능이 좋아지지만 데이터 불일치가 발생할 수 있다.</p>
</li>
<li><p>Document에 포함하는 데이터가 증가할수록 크기도 증가하여 디스크 I/O 시 성능 저하 및 최대 크기를 초과하면 저장이 불가능하다.</p>
</li>
</ul>
<h4 id="2-referencing">2. Referencing</h4>
<p>Document에 관계를 갖는 다른 Document의 식별자를 참조키로 저장하여 필요시 애플리케이션에서 조인하는 방식</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/1ccc1981-7bd8-42bd-a5c4-19a122998a50/image.png" alt=""></p>
<p><strong>장점</strong></p>
<ul>
<li><p>데이터가 중복되지 않도록 업무 성격별로 컬렉션을 분리 후 참조하므로 데이터 불일치가 발생하지 않는 정규화 모델</p>
</li>
<li><p>적절한 업무 단위의 Collection으로 데이터가 분리되어 Embedded 방식 대비 Document의 크기 증가가 작다.</p>
</li>
<li><p>업무 요건 추가 및 변경으로 인한 도큐먼트 구조에 미치는 영향이 적다.</p>
</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li><p>참조가 많거나 대규모 Document를 조회하는 경우 애플리케이션에서 2차 쿼리로 인한 처리량 증가로 조회 성능이 저하될 수 있다.</p>
</li>
<li><p>데이터 중복으로 인한 일관성 문제는 해소되나 참조 정보를 정확하게 관리하지 않는 경우 참조 정보 소실에 의한 데이터 정합성 문제가 발생할 수 있다.</p>
</li>
</ul>
<h3 id="collection">Collection</h3>
<p>관계형 데이터베이스의 Table과 같은 개념으로 용도가 같거나 유사한 도큐먼트들의 그룹을 묶는 단위를 의미한다. 간단하게 특징만 정리해 보자</p>
<h4 id="1-normal-collection">1. Normal Collection</h4>
<ul>
<li>가장 일반적으로 사용되는 컬렉션이다.</li>
</ul>
<h4 id="2-capped-collection">2. Capped Collection</h4>
<ul>
<li>고정된 크기를 갖는 Collection으로 높은 성능의 Logging 기능을 위해 설계되었다.</li>
</ul>
<h4 id="3-ttl-collection">3. TTL Collection</h4>
<ul>
<li>TTL Collection은 특정 시간이 경과한 도큐먼트를 자동으로 삭제하는 컬렉션으로 TTL 인덱스에 의해 지원되는 기능이다.</li>
</ul>
<h4 id="4-system-collection">4. System Collection</h4>
<ul>
<li>mongoDB 내부에서 사용되는 컬렉션으로 사용자가 지정하여 사용할 수 없다.</li>
</ul>
<hr>
<h2 id="겜린더에는-어떤-document-방식이-적합할까">겜린더에는 어떤 Document 방식이 적합할까?</h2>
<p>겜린더는 DB에 있는 <strong>게임 정보를 간단하고 빠르게 조회할 수 있는 기능이 핵심</strong>이다.</p>
<p>그렇다는 의미는 <strong>조회 성능이 중요</strong>하고 <strong>업데이트가 자주 발생하지 않는다.</strong>
어차피 데이터는 크롤러가 수집 후 DB에 계속 집어넣기 때문이다.</p>
<p>그래서 도달한 결론은 겜린더에는 사실 <strong>Embedded</strong> 방식이 매우 적합하다고 생각한다.</p>
<hr>
<h1 id="조사하면서-느낀-점">조사하면서 느낀 점</h1>
<p>최근 Redis, SQL을 공부한 덕분에 MongoDB의 용어가 이해하기 쉽고 빠르게 넘어갈 수 있었다고 생각한다.</p>
<p>사실 MongoDB를 사용해야 할지 1달 가까이 Docs를 읽어가면서 고민을 했는데, 이유는 간단했다.
기존 개발한 것들을 싹 다 갈아엎어야 하기 때문이었다.</p>
<p>하지만 장기적으로 봤을 땐 무조건 적용해야 한다는 생각과 함께 MongoDB와 RDB을 같이 공부할 수 있었다.</p>
<p>조사와 설계는 다했으니 이제 직접 만들어보면서 겪은 문제점과 어떻게 해결했는지 메모하면서 개발을 이어가야겠다.</p>
<p>프론트도 UI를 엄청 고민하면서 제작 중이다.
변천사들이 좀 많아서 개발하는 과정에서 UI&amp;UX가 어떻게 변화했는지 적어볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis 공부 - 2]]></title>
            <link>https://velog.io/@grit_munhyeok/Redis-%EA%B3%B5%EB%B6%80-2</link>
            <guid>https://velog.io/@grit_munhyeok/Redis-%EA%B3%B5%EB%B6%80-2</guid>
            <pubDate>Fri, 11 Aug 2023 19:08:20 GMT</pubDate>
            <description><![CDATA[<h1 id="chapter-2">Chapter 2</h1>
<h2 id="advanced-data-types">Advanced Data Types</h2>
<hr>
<h1 id="index">Index</h1>
<ol>
<li>Sets</li>
<li>Sorted Set</li>
<li>Bitmap</li>
<li>HyperLogLog</li>
</ol>
<hr>
<h1 id="sets">Sets</h1>
<p>집합, 비정렬, 중복된 Member가 있을 수 없는 Data Type</p>
<p>모든 Member가 Integer인 경우 Set 메모리 공간을 줄일 수 있다.</p>
<p>Element의 총 개수는 set-max-intset-entries의 값 만큼 높을 수 있다. </p>
<p>Data filtering, Data grouping, Membership checking 등으로 활용할 수 있다.</p>
<h3 id="commands">Commands</h3>
<p><strong>SADD</strong> : 세트에 Member 추가, 이미 존재하는 Member는 추가할 수 없다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/dc2f7a54-6e8c-4337-916e-195dd6685e12/image.png" alt=""></p>
<p><strong>SINTER</strong> : Set끼리 비교해서 겹치는 Member를 반환한다. (교집합)</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/bfab67d8-0f96-466a-95b9-6c6bb6465d57/image.png" alt=""></p>
<p><strong>SDIFF</strong> : A Set와 B Set끼리 비교해 중복되는 Member를 제외한 A Set의 Member를 반환한다. (차집합)</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/cccfd3ac-c2da-4313-90e8-b05d1d3628b3/image.png" alt=""></p>
<p>user:max:favorite_artists - user:hugo:favorite_artists</p>
<p><strong>SUNION</strong> : 여러 개의 세트를 다같이 합쳐 출력한다. 단 Set의 특징처럼 중복된 Member는 하나의 Member로 출력한다. (합집합)</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/9a9f0413-35d5-4ec4-8685-49badcba34c3/image.png" alt=""></p>
<p>Arctic Monkeys는 user:max:favorite_artists, user:hugo:favorite_artists 둘 다 중복되는 Member이므로 하나로 합쳐 출력한다.</p>
<p><strong>SRANDMEMBER</strong> : Set중 Random으로 Member를 반환한다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/553d8843-b4a3-4317-a00f-c391076f22e9/image.png" alt=""></p>
<p><strong>SISMEMBER</strong> : Member가 Set에 존재 유무를 알 수 있다.
<del>이 친구 우리 멤버 맞나요?</del>
Yes = 1, No = 2</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/2adf80f0-81a1-495e-9a4b-0716c6c91301/image.png" alt=""></p>
<p><strong>SREM</strong> : 특정 Member 삭제</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/59d11bf7-abdf-4ad9-b277-6317abceb05a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/b548dd23-1a38-4bd7-8d03-89ccb1d85d68/image.png" alt=""></p>
<p>SREM으로 “Arctic Monkeys” 삭제 후 SISMEMBER로 존재하는지 확인</p>
<p><strong>SCARD</strong> : Member 수 반환</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/45efa718-24e2-484b-9d8a-f7659800aff3/image.png" alt=""></p>
<p><strong>SMEMBERS</strong> : Set에 있는 Member 모두 출력</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/c989cc0d-abd3-4d58-ac6a-fac861cd0264/image.png" alt=""></p>
<hr>
<h1 id="sorted-sets">Sorted Sets</h1>
<p>단어 그대로 Sets에서 정렬된 Data Type</p>
<p>Sorted Set의 각 Element에는 연관된 점수가 존재한다.</p>
<p>Sorted Set은 Set보다 성능이 느리다.</p>
<p>실시간 대기 리스트 시스템, 게임 순위표 표시, 자동완성 시스템 구축등이 가능하다.</p>
<h2 id="commands-1">Commands</h2>
<p><strong>ZADD</strong> : 하나 이상의 Member를 추가하고 만약 이미 있는 Member의 경우 무시한다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/d3f244e6-875f-4d25-b27a-0bc4a7fe2f46/image.png" alt=""></p>
<p><strong>ZRANGE</strong> : 낮은 Score 순서대로 (오름차순) Member를 반환한다.
-1은 마지막 Member</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/a454a75e-ea1f-4af9-a64f-e1f48ebf9653/image.png" alt=""></p>
<p><strong>ZREVRANGE</strong> : ZRANGE의 Reverse 역순으로 Member를 반환</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/d99233f2-51a4-404b-975b-8e1ada13e307/image.png" alt=""></p>
<p><strong>WITHSCORES</strong> : ZRANGE, ZREVRANGE 맨 마지막에 넣으면 스코어와 함께 반환된다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/d9c9dc9c-c596-42fb-a103-183f2032cbc4/image.png" alt=""></p>
<p><strong>ZREM</strong> : 특정 Member 삭제</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/f0bcbc89-51e6-4696-9b4b-ef4ef3a8ec77/image.png" alt=""></p>
<p><strong>ZSCORE</strong> : 특정 Member의 Score 반환</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/659fdbea-4a68-45ff-b95e-4bd9bd066ad4/image.png" alt=""></p>
<p><strong>ZRANK</strong> : 특정 Member의 Rank 확인</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/041992ba-03df-4788-90b9-ab2a4ab1949c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/2a070311-7040-4866-9bf0-bfcf92a25e13/image.png" alt=""></p>
<p><strong>ZREVRANK</strong> : ZRANK의 역순</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/1cbc22d3-b24b-44d1-a9ab-8d9989b90cea/image.png" alt=""></p>
<hr>
<h1 id="bitmaps">Bitmaps</h1>
<p>Bitmap은 실제 데이터 타입이 아닌 String 타입이다.</p>
<p>하지만 Bitmap이 String에 대한 비트 연산이라고 말할 수 있으며, String을 Bitmap으로 간주하는 명령어가 존재하기 때문에 Bitmap으로 간주한다고한다.</p>
<p>Bitmap index를 Offset으로 참조한다.</p>
<p>비트맵은 메모리 친화적이고 빠른 데이터 조회를 지원한다.</p>
<h3 id="commands-2">Commands</h3>
<p><strong>SETBIT</strong> : Bitmap offset에 값을 부여하는데 사용 1 또는 0만 허용된다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/3fb67293-2a6b-4b60-8c0f-27a36a31883d/image.png" alt=""></p>
<p><strong>GETBIT</strong> : Bitmap offset 입력하면 Bit리턴</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/a19bddcc-dcb9-4a04-9455-63fd2f89c5a3/image.png" alt=""></p>
<p><strong>BITCOUNT</strong> : offset이 1인 개수 출력</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/60338215-67ae-48ee-8746-566d8fed70a6/image.png" alt=""></p>
<p><strong>BITOP</strong> : 대상 키, 비트 연산 및 해당 연산에 적용하고 결과를 따로 새로운 키에 저장한다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/6cef440a-682a-4c26-89ba-e5ee240407c3/image.png" alt=""></p>
<hr>
<h1 id="hyperloglogs">HyperLogLogs</h1>
<p>실제 Data Type이 아니다.</p>
<p>Element수에 대한 근사치를 제공하기 위해 무작위화(Randomization)을 사용하는 알고리즘</p>
<p>실행속도 O(1)로만 동작하고 하나의 키당 아주 작은 메모리(최대 12KB)를 사용한다.</p>
<p>HyperLogLog 알고리즘은 확률적이기 때문에 100%의 정확도를 보장하지 않는다. (약 0.81%의 표준 오차를 가진다.)</p>
<p>이론상 셀 수 있는 셋의 개수 제한은 사실 없다.</p>
<p>고유 개수 카운팅을 수행하려면 카운팅하려는 세트의 항목 수에 비례하는 메모리량이 필요</p>
<p>HyperLogLog가 100%정확하진 않지만, 일부 상황에서 99.19%의 정확도가 나온다.</p>
<h2 id="commands-3">Commands</h2>
<p><strong>PFADD</strong> : 카디널리티가 변경된 경우 1을 반환하고 동일하게 유지되면 0을 반환</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/e146cc0f-2fd9-46d3-b03d-7e6e8aab51a7/image.png" alt=""></p>
<p><strong>PFCOUNT</strong> : 하나 이상의 키를 매개 변수로 받고, 근사치 개수를 반환</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/d24b3afb-5983-4f3c-861f-b56bb137c581/image.png" alt=""></p>
<p><strong>PFMERGE</strong> : 대상키와 하나 이상의 HyperLogLog키를 매개변수로 받아야하고 병합 후 대상키에 저장</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/6c003a46-d74a-418f-8d60-156f887af233/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis 공부 - 1]]></title>
            <link>https://velog.io/@grit_munhyeok/Redis-%EA%B3%B5%EB%B6%80-1</link>
            <guid>https://velog.io/@grit_munhyeok/Redis-%EA%B3%B5%EB%B6%80-1</guid>
            <pubDate>Thu, 06 Jul 2023 03:47:33 GMT</pubDate>
            <description><![CDATA[<h1 id="chapter-1">Chapter 1</h1>
<p>Redis Essential의 내용을 정리하였습니다.
번역해서 정리했기 때문에 오역이 있을 수 있습니다.
부족하거나 모르는 내용은 구글링을 통해 정리하였습니다.</p>
<h1 id="index">Index</h1>
<ol>
<li>Redis 기본 개념</li>
<li>Redis Data Types<ol>
<li>Strings</li>
<li>Lists</li>
<li>Hashes</li>
</ol>
</li>
</ol>
<h1 id="redis-기본-개념">Redis 기본 개념</h1>
<h2 id="redis-remote-dictionary-server"><strong>Redis? (REmote DIctionary Server)</strong></h2>
<p>고성능 Key-Value를 지원하는 NoSQL Data Structure 서버</p>
<p><strong>유추해보자</strong>
Dictionary? 내가 아는 Dictionary는 파이썬의 Dictionary인데 Hash Table이라는 자료구조 기반으로 작동한다. Redis도 Hash Table를 활용해서 데이터를 저장하는 건가?</p>
<hr>
<h2 id="왜-고성능-key-value일까"><strong>왜 고성능 Key-Value일까?</strong></h2>
<p>수 많은 데이터 타입을 지원한다. (기본 데이터 타입 : Chapter1, 더 많은 데이터 타입 : Chapter 2)</p>
<hr>
<h2 id="redis의-모든-데이터는-메모리에-저장된다-inmemory-저장-방식"><strong>Redis의 모든 데이터는 메모리에 저장된다. (InMemory 저장 방식)</strong></h2>
<p>Memory는 Disk보다 속도가 빠르다. 기존 Disk 기반의 DB보다 훨씬 빠르지만 전원 공급이 끊어지면 Memory의 휘발성 특징 때문에 데이터가 전부 사라진다.</p>
<h3 id="간단하게-짚어보는-cs-지식"><strong>간단하게 짚어보는 CS 지식</strong></h3>
<ul>
<li>(CPU &gt; Memory &gt; Disk) 순으로 CPU가 가장 처리속도가 빠르고 Disk가 가장 느리다</li>
<li>Memory 종류</li>
<li><em>DRAM (Dynamic Random Access Memory)*</em> : 우리가 흔히 쓰는 램이 DRAM이다.
SRAM (Static RAM) : 용량이 작은 대신 속도가 DRAM보다 빠르다. CPU의 캐시메모리에 활용</li>
</ul>
<p>Redis의 데이터는 우리가 쓰는 DRAM에 저장된다.</p>
<hr>
<h2 id="그럼-데이터-저장을-아예-못하는가"><strong>그럼 데이터 저장을 아예 못하는가?</strong></h2>
<p>아니다. Redis는 SnapShot(RDB), 혹은 모든 명령의 시퀀스가 포함된 파일(AOF)로 Disk에 저장할 수 있다. 추후 자세하게 알아보자 😃</p>
<hr>
<h2 id="그-외에도"><strong>그 외에도</strong></h2>
<p>Redis는 Key Expiration, Transactions, Publish, Subscribe 기능들을 가지고 있다.</p>
<hr>
<h1 id="redis-data-types">Redis Data Types</h1>
<p>Redis의 기본적인 데이터 타입들을 알아보자</p>
<h2 id="strings"><strong>Strings</strong></h2>
<p><del>우리가 아는 문자열이다.</del>
String은 값과 사용 된 명령에 따라 integer, float, text string, bitmap으로 작동할 수 있다.</p>
<p><strong>String 값은 Text 혹은 Binary 데이터의 크기가 512MB를 초과할 수 없다.</strong></p>
<h3 id="cache-mechanisms캐시-매커니즘">Cache mechanisms(캐시 매커니즘)</h3>
<p>Redis는 Text 혹은 Binary 데이터를 캐시로 이용할 수 있다.</p>
<h3 id="command"><strong>Command</strong></h3>
<p><strong>SET, GET, MSET, MGET</strong>으로 간단한 캐시 시스템 구현이 가능하다.</p>
<p><strong>SET</strong> : Key - Value  추가</p>
<p><strong>GET</strong> : Key를 대입하면 Value를 출력</p>
<p><strong>MSET</strong> : 하나의 명령어로 여러개의 Key - Value 추가</p>
<p><strong>MGET</strong> : 하나의 명령어로 여려개 Key를 입력해 각각의 Value를 출력</p>
<h3 id="example"><strong>Example</strong></h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/c86874fa-79ab-4705-a45b-4b9afbd6a820/image.png" alt=""></p>
<h3 id="cache-with-automatic-expiration-캐시와-자동-만료"><strong>Cache with Automatic expiration (캐시와 자동 만료)</strong></h3>
<p>특정 시간이 지나면 자동으로 만료되고
이는 데이터베이스 쿼리를 실행하는데 오랜 시간이 걸리고, 지정된 기간동안 캐시될 수 있는 경우 매우 유용하다.</p>
<h3 id="command-1"><strong>Command</strong></h3>
<p><strong><del>SETEX</del>, EXPIRE, EXPIREAT</strong>으로 구현이 가능하다.
<strong>TTL (Time To Live)</strong></p>
<p><strong>EXPIRE</strong> : Key에 만료시간 추가 (단위 : 초, TTL)</p>
<p><strong><strong><strong><strong><strong>**</strong></strong></strong></strong></strong>EXPIREAT<strong><strong><strong><strong><strong>**</strong></strong></strong></strong></strong> : Key에 만료시간 추가 (단위 : Unix Timestamp)</p>
<p><strong>~~SETEX</strong> : Deprecated 사용하지 않음~~</p>
<p><strong>TTL (Time To Live)
표시 상태
양수</strong> : 만료 시간이 주어진 상태 초 단위로 점점 감소한다.</p>
<p><strong>-2</strong> : 키가 만료되었거나, 존재하지 않는 경우</p>
<p><strong>-1</strong> : 키가 존재하지만 Expire을 설정하지 않은 경우</p>
<h3 id="example-1">Example</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/5bcf058a-7f5b-4437-bf3f-b631d94fd0b9/image.png" alt=""></p>
<h3 id="counting">Counting</h3>
<p>간단하게 카운트를 구현할 수 있는 기능
좋은 예시로 조회수나 좋아요 같은 카운팅을 활용한 기능을 만들 수 있다.</p>
<h3 id="command-atomic"><strong>Command (Atomic)</strong></h3>
<p><strong>INCR</strong> : +1</p>
<p><strong>DECR</strong> : -1</p>
<p><strong>INCRBY</strong> : 원하는 값 증가</p>
<p><strong>DECRBY</strong> : 원하는 값 감소</p>
<p><strong>INCRBYFLOAT</strong> : 원하는 소수점 값 증가</p>
<p><strong>DECRBYFLOAT</strong> : 원하는 소수점 값 감소</p>
<p><strong>Atomic?</strong>
해당 명령을 수행하는 도중에 다른 명령이 끼어들 수 없는 명령</p>
<p><strong>Example</strong></p>
<p>SET counter 100</p>
<p>→ OK</p>
<p>INCR counter</p>
<p>→ (integer) 101</p>
<p>INCRBY counter 5</p>
<p>→ (integer) 106</p>
<p>DECR counter</p>
<p>→ (integer) 105</p>
<p>DECRBY counter 100</p>
<p>→ (integer) 5</p>
<p>GET counter</p>
<p>→ “5”</p>
<p>INCRBYFLOAT counter 2.4</p>
<p>→ “7.4”</p>
<h3 id="counting-명령어는-단일-명령으로만-처리할-수-있다"><strong>Counting 명령어는 단일 명령으로만 처리할 수 있다.</strong></h3>
<p>MSET 같은 멀티작업으로 여러개의 Key를 동시에 카운팅을 할 수 없게 설계되어있단 의미</p>
<p><strong>Example</strong></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/2129c8fd-863a-4634-8fa7-920ccc19982f/image.png" alt=""></p>
<p>만약 두 개 혹은 여러 개의 클라이언트가 동시에 INCR key 명령어를 실행할 경우 중복처리가 되지 않고 모두 처리할 수 있다.</p>
<p><strong>Why?</strong></p>
<p>“<strong>Redis is Single threaded”</strong>
레디스는 싱글 스레드로 동작하고 이 의미는 항상 하나의 명령을 수행하기 때문이다.</p>
<hr>
<h2 id="lists">Lists</h2>
<p>List는 Redis에서 매우 유연한 데이터 타입이다.</p>
<p>단순 수집, Stack, Queue 처럼 동작하게 만들 수 있다.</p>
<h3 id="blocking-command">Blocking Command</h3>
<p>Redis의 List에는 Blocking Command가 있다.
클라이언트가 비어있는 리스트에 Blocking Command를 실행할 때, 클라이언트는 List에 새로운 Element가 추가될 때까지 기다린다.</p>
<h3 id="list-성능">List 성능</h3>
<p>Redis의 List는 Linked List 라서 리스트의 처음 또는 끝에서 Element의 추가 및 삭제는 항상 O(1) 즉 일정 시간의 성능을 가진다.
Element 접근시간 : O(N)
첫번째 또는 마지막 Element 접근 시간 : O(1)</p>
<h3 id="list-encoding">List Encoding</h3>
<p>List 크기의 엘리먼트가 list-max-ziplist-entires 설정보다 작고, List 의 개별 바이트가 list-max-ziplist-value 설정보다 작으면
→ ziplist로 최적화</p>
<p>List 크기의 엘리먼트가 list-max-ziplist-entires 설정보다 크거나, List 의 개별 엘리먼트의 바이트가 list-max-ziplist-value 설정보다 크면  → Linked List로 최적화</p>
<p><strong>하지만</strong></p>
<p>현재는 <strong>QuickList</strong>만 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/0c78effe-b2e3-4963-8e37-dadd2456319b/image.png" alt=""></p>
<h3 id="quicklist"><strong>QuickList</strong></h3>
<ul>
<li>ziplist가 여러개 리스트로 되어 있는 형태</li>
<li>ziplist의 마지막보다 찾는 인덱스가 다음에 있으면 다음 ziplist로 이동하는 형식으로 다수를 skip하여 리스트 탐색 시간을 줄였다고 한다…</li>
</ul>
<p>Redis의 List는 최대 2^32 - 1의 Element를 가질 수 있다.</p>
<h3 id="command-2"><strong>Command</strong></h3>
<p><strong>LPUSH</strong> : List 처음에 Element 추가</p>
<p><strong>RPUSH</strong> : List 마지막에 Element 추가</p>
<p><strong>LLEN</strong> : List 길이 출력 (Length)</p>
<p><strong>LINDEX</strong> : List의 Index 값을 볼 수 있음 (0부터 시작)</p>
<p><strong>LRANGE</strong> : For문처럼  Index 범위를 지정해 지정된 모든 값을 출력한다.
-1은 List의 마지막 Element</p>
<p><strong>LPOP</strong> : 가장 처음 Element 삭제 후 출력</p>
<p><strong>RPOP</strong> : 가장 마지막 Element 삭제 후 출력</p>
<p><strong>BLPOP, BRPOP</strong> : LPOP과 RPOP을 Blocking하면서 기다린다.
구글링을 해보니 <strong>Message Queue</strong>처럼 활용이 가능하다고 한다. 나중에 예제를 만들어보자</p>
<h3 id="example-2"><strong>Example</strong></h3>
<p><strong>LPUSH, RPUSH</strong></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/5f436229-176a-48cc-a3c2-1201ee9ba79c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/1d3f9cae-78cc-401e-8df7-fb254bb0e33e/image.png" alt=""></p>
<p><strong>LLEN, LINDEX</strong></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/8d66bb67-5c6d-4db1-a407-b4e8225a2485/image.png" alt=""></p>
<p><strong>LRANGE</strong></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/bbb60cb7-75f7-4134-9fb2-526716e74e00/image.png" alt=""></p>
<p><strong>LPOP, RPOP</strong></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/72ecbc77-5d50-4496-82d6-3cd14e3a5d47/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/78d753c5-97a9-48c1-b036-e854627d2e0a/image.png" alt=""></p>
<p><strong>BLPOP, BRPOP</strong></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/7e4c2977-5378-4150-bdeb-db3e0117a601/image.png" alt=""></p>
<p>strings Key에 Element가 있을 경우 → strings에 있는 “안녕하세요”를 POP 하였다.</p>
<p>strings Key가 없을 경우 → Executing command 라고 뜨고 만료시간 30000초가 넘기 전까지 대기한다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/f7c25b82-f956-46c0-b64e-699b84b20d6c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/b737c6ce-23f8-4098-8fd2-269f94b08be6/image.png" alt=""></p>
<p>만료시간 이내에 strings key에 “hello” Element를 추가 → 대기하던 blpop명령어가 동작해 출력</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/862398dd-896b-4a1e-8c14-fc0c694ec186/image.png" alt=""></p>
<p>만료시간이 지나면 → (nil)을 출력</p>
<hr>
<h2 id="hashes">Hashes</h2>
<p>메모리를 효율적으로 사용하고 데이터를 빠르게 찾도록 최적화 되어있다.</p>
<p>Field name 그리고 Value 둘 다 String으로 구성되어있다.
따라서 Hash는 문자열을 문자열로 매핑한다.</p>
<p>Hash는 내부적으로 <strong>ziplist와 Hash Table</strong>이 될 수 있다.</p>
<h3 id="ziplist--hash-table">Ziplist &amp; Hash Table</h3>
<p>Ziplist? → 메모리 효율화에 목적을 둔 양쪽으로 연결된 리스트 (연결 리스트)</p>
<p>ziplist는 정수를 일련의 문자로 저장하지 않고 실제 정수의 값으로 저장한다.</p>
<p>ziplist는 메모리 최적화가 되어있어도 일정한 시간 내로 검색이 수행 되지는 않는다.</p>
<p>Hash Table에서는 일정한 시간 내로 검색은 되지만 메모리 최적화가 이루어지지 않는다.</p>
<h3 id="command-3">Command</h3>
<p><strong>HSET, HMSET</strong> : Key “Field name” Value 형식으로 등록</p>
<p><strong>HGET, HMGET</strong> : Key “Field name” 으로 조회</p>
<p><strong>HINCR</strong> : +1</p>
<p><strong>HINCRBY</strong> : 주어진 정수 값 증가</p>
<p><strong>HDECR</strong> : -1</p>
<p><strong>HDECRBY</strong> : 주어진 정수 값 감소</p>
<p><strong>HINCRBYFLOAT, HDECRBYFLOAT</strong> : 주어진 정수 혹은 부동소수점 만큼 필드를 증가 감소 시킴.</p>
<h3 id="example-3">Example</h3>
<p><strong>HSET, HMSET</strong></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/5e3a7971-5521-4765-8c53-91c3a25539fa/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/302dd9da-5faa-46d5-9799-fa5f73679756/image.png" alt=""></p>
<p><strong>HGET, HMGET</strong></p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/c0d8a3c0-2ab5-459c-ac8e-1653a5932782/image.png" alt=""></p>
<p><strong>HINCRBY, HINCRBYFLOAT</strong>
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/a707e680-3d8e-4284-83e6-d2d0f5a5558e/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[사이드 프로젝트] 쇼핑스캐너 회고]]></title>
            <link>https://velog.io/@grit_munhyeok/%EC%98%A4%ED%94%88%EB%A7%88%EC%BC%93-%EC%B5%9C%EC%A0%80%EA%B0%80-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@grit_munhyeok/%EC%98%A4%ED%94%88%EB%A7%88%EC%BC%93-%EC%B5%9C%EC%A0%80%EA%B0%80-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Wed, 07 Jun 2023 07:53:43 GMT</pubDate>
            <description><![CDATA[<h2 id="글을-시작하기-전">글을 시작하기 전</h2>
<blockquote>
<p><strong>프로젝트 기간</strong>
2022-10 ~ 2023-02</p>
</blockquote>
<blockquote>
<p>코드가 길어 Github에 업로드 합니다.
Github
<a href="https://github.com/munhyok/Zikbee-Front">Front-End</a>
<a href="https://github.com/munhyok/Zikbee-Redis">Back-End</a>
<a href="https://github.com/munhyok/Zikbee-BackCrawler">Scraping Server</a></p>
</blockquote>
<blockquote>
<p><strong>기술 스택</strong>
Front-End : React, Websocket
Back-End : FastAPI, Redis, Docker
WebScrapingServer : Websocket, Selenium, BeautifulSoup4, Python Request
Language : Python, JavaScript</p>
</blockquote>
<hr>
<h1 id="제작-의도">제작 의도</h1>
<p>용산에서 한창 일할 때
Selenium으로 네이버 쇼핑 데이터 수집 봇을 만들어달라고 하셨던 사장님께서
새로운 무언가를 만들어 보고 싶으셨다 하셨고 해외직구 물품을 자동으로 검색해 주고 최저가를 한눈에 보이면 좋겠다는 아이디어를 주셨다.</p>
<p>처음 들었을 땐 네이버 쇼핑이 너무 압도적으로 최저가를 잘 찾아주어서 이게 가능할까 싶었는데 네이버 쇼핑뿐만 아니라 다양한 오픈마켓에서 일일이 발품 뛰는 내 모습을 보면서 이 과정을 자동으로 해줬으면 좋겠다는 생각으로 제작하게 되었다.</p>
<blockquote>
<p>스카이스캐너 같은 최저가 항공권을 찾게 해주는 서비스처럼 만들어 보면 재밌을 것 같았다.</p>
</blockquote>
<p>마침 겜린더로 FastAPI와 Redis를 공부도 했겠다, 내가 공부했던 기술들을 모두 이용하면 만들 수 있을 것 같아</p>
<p>새로운 사이드 프로젝트를 시작하게 되었다.</p>
<hr>
<h1 id="견적">견적</h1>
<p>어떤 기술을 사용해야 할지 스스로 견적을 맞춰보았다.</p>
<h2 id="조건">조건</h2>
<p><strong>1. 데이터 수집 시 무조건 백엔드 단에서 모든 것을 수행해야 한다.</strong></p>
<p><strong>2. 한 사람만 사이트를 사용하는 것이 아니기 때문에 데이터 수집 할 때 싱글 쓰레드나 프로세스가 아닌 멀티 쓰레드 혹은 프로세스를 이용해야 한다.</strong></p>
<p><strong>3. 검색어 자동 완성 기능이 무조건 있어야 한다.</strong></p>
<p><strong>4. 같은 시간대에 중복 검색어 방지를 위한 최소한의 방어 수단이 있어야 한다.</strong></p>
<h2 id="front-end">Front-End</h2>
<ol>
<li><p><strong>React</strong> : 이번엔 최초로 웹 페이지를 만들어 보기로 했다. 겜린더를 통해 React Native를 해서 그나마 익숙한 React로 제작하는 것이 개발에 유리하다고 생각하였다.</p>
</li>
<li><p><strong>WebSocket</strong> : DB에 만약 데이터가 없을 때 스크래핑 서버와 통신하기 위해서 WebSocket을 사용하였다. <del>당장 생각나는 게 웹 소켓밖에 없었다...</del></p>
</li>
</ol>
<h2 id="back-end">Back-End</h2>
<ol>
<li><p><strong>Redis (Redis-Stack)</strong> : 겜린더에서도 사용했던 Redis를 활용할 예정이다. Memory 기반 DBMS라서 읽기, 쓰기 속도가 빠른 것도 있고 상대적으로 성능이 좋지 못한 하드웨어에서도 준수한 성능을 내줄 수 있을 것으로 생각했다.</p>
</li>
<li><p><strong>Docker</strong> : 겜린더를 만들었을 때 Docker를 이용해서 Local로 돌리고 있는데 Container로 분리가 되어 사용이 편리할 것 같아 개발환경에선 Docker를 사용하게 되었다.</p>
</li>
<li><p><strong>FastAPI</strong> : 이것도 겜린더에서 사용했었지만, 성능도 좋고 내가 현재 구현하려는 선에서 FastAPI가 역할을 훌륭하게 수행해 줄 것 같아 적용하였다.</p>
</li>
</ol>
<h2 id="web-scraping-server">Web Scraping Server</h2>
<ol>
<li><p><strong>Selenium</strong> : 대부분의 웹 페이지가 동적 웹페이지여서 원활하게 데이터를 받아오려면 직접 브라우저를 조작해 가며 데이터를 수집해야 한다고 생각하였다.</p>
</li>
<li><p><strong>BeautifulSoup4</strong> : Selenium으로도 HTML 태그를 불러올 수 있지만 BS4가 좀 더 속도가 빠른 것 같아 같이 사용할 것이다.</p>
</li>
<li><p><strong>WebSocket</strong> : Front-End에서 전송한 텍스트 데이터를 받아와서 스크래핑을 실행하게 해주는 역할을 할 것이다.</p>
</li>
</ol>
<hr>
<h1 id="설계">설계</h1>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/f06517dc-fbd0-4c1e-831e-2412a68c4869/image.svg" alt=""></p>
<blockquote>
<p>기존에 노트에 단순하게 적었던 설계를 구체화시켰다. 만들다보니 MiddleWare에 각각의 기능들이 의존하는 형태가 마치... <strong>SOA(Service Oriented Architecture)</strong> 형식처럼 나오게 되었다.</p>
</blockquote>
<p>설계한 부분을 하나씩 설명해 볼까 한다.
<del>(분명 복잡하지 않은데 플로우차트가 복잡하게 나왔다...)</del></p>
<hr>
<h2 id="front-end-1">Front-End</h2>
<p>React 개발한 Front-End는 엄청 단순하게 검색, 결과 이렇게 2가지의 페이지로 구성하였다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/71b0c5f9-fce8-4c56-a82b-4d94fc7df80c/image.png" alt=""></p>
<h3 id="main-page">Main Page</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/5faa3ee0-3296-468f-a6c5-0c635474328b/image.png" alt="">
단순하게 만들었고 아까 조건 중 3번 조건인 검색어 자동 완성 기능을 만들었다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/fbd8d1f6-e2bc-4d62-9633-235a1c9833b5/image.png" alt=""></p>
<p>아마존의 자동 완성 기능을 이용하였는데
상품 검색 위주의 내 프로젝트에선 아마존의 자동 완성 기능은 매우 적합했다.</p>
<h3 id="result-page">Result Page</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/0501875e-07ad-4ba6-b78d-069175e82755/image.png" alt=""></p>
<p>결과 페이지는 국내, 해외 오픈마켓으로 구역을 나눠서 나오게 하였고, 배송비로 가격을 올리는 업체도 있어 배송비도 데이터 수집을 해 합산 가격이 나오도록 만들어 보았다.</p>
<h2 id="back-end-1">Back-End</h2>
<h3 id="유효한-데이터가-있는지-판단">유효한 데이터가 있는지 판단</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/ec86cce4-e615-497f-89b3-2a2ebdee4040/image.png" alt=""></p>
<p>FastAPI를 통해 검색어를 받은 Redis에선 FT.Search를 통해 데이터가 있는지를 확인한다. 데이터가 있으면 JSON 형식의 Data를 출력하고 없으면 Null을 출력한다.</p>
<h3 id="전체적인-설계-flow-설명">전체적인 설계 Flow 설명</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/04ac5e58-1462-4cb6-955c-b03bc5a60b64/image.png" alt=""></p>
<ol>
<li>Main Page에서 입력한 검색어를 FastAPI를 통해 GET 요청을 한다.</li>
<li>FastAPI에선 Redis DB에 접속 후  FT.SEARCH 명령어를 통해 검색어에 맞는 유효한 데이터가 있는지 확인한다.</li>
<li>데이터가 있으면 JSON 형식의 Data 보내고 없으면 Null을 보내준다.</li>
<li>데이터를 받은 FastAPI는 유효한 데이터면 Status Code 200, Null이면 404를 Return 할 수 있게 하였다.</li>
<li>Status Code 200인 경우 Result 페이지를 보여준다.</li>
</ol>
<blockquote>
<p><strong>그럼 404면 어떻게 될까?</strong></p>
</blockquote>
<hr>
<h2 id="web-scraping-server-1">Web Scraping Server</h2>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/9208f98b-c1e3-45f0-9487-10b0b7f7377f/image.png" alt=""></p>
<h3 id="websocket-listenerrecevier">WebSocket Listener(Recevier)</h3>
<p>FrontEnd에서 보내준 검색어를 받아 Scraper Process Container에 새로운 Process를 추가한다.</p>
<h3 id="scraper-process">Scraper Process</h3>
<p>글 위에 작성했던 조건 중 2번 조건인</p>
<blockquote>
<p><strong>&quot;한 사람만 사이트를 사용하는 것이 아니기 때문에 데이터 수집 할 때 싱글 스레드나 프로세스가 아닌 멀티 스레드 혹은 멀티 프로세스를 이용해야 한다.&quot;</strong></p>
</blockquote>
<p>부분을 해결하였다.
Python의 <a href="https://docs.python.org/ko/3/library/multiprocessing.html"><strong>Multiprocessing</strong></a> 라이브러리를 사용해 어떤 검색어가 들어와도 바로 스크래핑 봇을 실행해 다수의 일을 해결할 수 있게 만들었다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/c3a558d6-a203-4457-b7eb-b3c3d99f262b/image.gif" alt=""></p>
<pre><code>테스트 영상.gif</code></pre><blockquote>
<h3 id="멀티-스레드-vs-멀티-프로세스">멀티 스레드 vs 멀티 프로세스</h3>
<h4 id="어떤-걸-사용해야-할까">어떤 걸 사용해야 할까?</h4>
</blockquote>
<p>스크래핑 서버는 <strong>성능보다 안정성이 중요하다는 점을 고려하였을 때</strong> 멀티 스레드보다는 <strong>메모리 공간과 CPU 시간을 더 많이 사용하지만 독립된 구조로 작동하여 안정성이 높은</strong> 멀티 프로세스를 선택하였고, 무엇보다 <strong>문제가 발생해도 다른 프로세스에 영향을 주지 않는다는 점</strong>이 안정성에 도움이 될 것 같아 선택하게되었다.</p>
<h3 id="scrape-data--convert-json">Scrape Data &amp; Convert JSON</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/8cd0ffdd-7d99-40b8-85bb-890a869bdfb9/image.png" alt=""></p>
<p>스크래핑한 데이터를 JSON 형식으로 변환하는 과정이다.</p>
<h3 id="db-저장">DB 저장</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/40220993-c15d-4272-a190-cfee07319c77/image.png" alt=""></p>
<p>JSON Data를 POST 요청을 통해 FastAPI에선 Redis에 JSON.SET 명령어를 통해 DB에 저장하게 된다.</p>
<h3 id="전제적인-flow-설명">전제적인 Flow 설명</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/8428f813-49b0-4575-bf53-d1e5d1501e3e/image.png" alt=""></p>
<ol>
<li><p>Front-End에서 WebSocket을 연결 후 검색어를 WebSocket Listener(Recevier)에 전송한다.</p>
</li>
<li><p>Scraper Process Container에 Process가 추가된다. (스크래핑 시작)</p>
</li>
<li><p>스크랩한 데이터를 JSON 형식으로 변환한다.</p>
</li>
<li><p>JSON 형식의 데이터를 FastAPI를 통해 데이터를 전송한다 (POST)</p>
</li>
<li><p>FastAPI에선 JSON.SET 명령어를 통해 Redis DB에 데이터를 저장한다.</p>
</li>
<li><p>저장한 데이터를 FrontEnd에서 다시 요청해 최종적으로
Status Code 200이 나오고 Reuslt 페이지가 나오게 된다.</p>
</li>
</ol>
<hr>
<h2 id="결과">결과</h2>
<h3 id="모든-조건을-해결했는가">모든 조건을 해결했는가?</h3>
<blockquote>
<p><strong>1. 데이터 수집 시 무조건 백엔드 단에서 모든 것을 수행해야 한다.</strong></p>
</blockquote>
<p>A. 데이터 수집과 저장 모두 백엔드 단에서 모든 걸 수행하게 되었다.</p>
<blockquote>
<p><strong>2. 한 사람만 사이트를 사용하는 것이 아니기 때문에 데이터 수집 할 때 싱글 쓰레드나 프로세스가 아닌 멀티 쓰레드 혹은 프로세스를 이용해야 한다.</strong></p>
</blockquote>
<p>Python의 MultiProcessing을 이용해 해결하였다.</p>
<blockquote>
<p><strong>3. 검색어 자동 완성 기능이 무조건 있어야 한다.</strong></p>
</blockquote>
<p>아마존의 자동 완성 기능을 이용해 해결하였다.</p>
<blockquote>
<p><strong>4. 같은 시간대에 중복 검색어 방지를 위한 최소한의 방어 수단이 있어야 한다.</strong></p>
</blockquote>
<p>여러 사용자가 같은 검색어를 입력하게 되면 중복된 데이터를 수집할 수 있는 시나리오를 예상했고
상품 가격이 실시간으로 급변하지 않기 때문에 수집한 데이터는 6시간 동안 유지되게 제작하였다.</p>
<p>즉, DB에 데이터가 있어도
<strong>6시간이 지나면 다시 데이터 수집을 해서 새로운 데이터로 유지할 수 있게 만들었다.</strong></p>
<p>Redis의 Expire 기능을 사용하려 했으나 Expire을 사용하면 데이터가 아예 사라지기 때문에 <strong>refreshtime</strong>이란 키를 만들어 현재 시각과 비교해 데이터 수집의 판단 여부를 결정하게 했다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/cddedd0c-b7f6-4c6f-befc-f3554ea3b533/image.png" alt=""></p>
<hr>
<h2 id="아쉬운-점">아쉬운 점</h2>
<blockquote>
<p><strong>1. 세상엔 다양한 상품명과 모델명 상품 코드들이 있어서 검색 정확도가 높지 못해 아쉬웠다.</strong></p>
</blockquote>
<p>이 부분은 ChatGPT를 활용해서 사전 필터링을 가져봤으면 좋았을 것 같은데 비용을 쓸 여력이 없어 패스 했다. 물론 정확도를 높이기 위해 <strong>슬라이딩 윈도우</strong>, <a href="https://wikidocs.net/24603"><strong>코사인 유사도</strong></a> 같은 문자열 유사도 AI나 알고리즘을 찾아봤지만 적용하지 못했었다.</p>
<blockquote>
<p><strong>2. 마감 기간이 정해져 있어 제대로 FrontEnd에 신경 쓰지 못한 점이 아쉬웠다.</strong></p>
</blockquote>
<p>2022년 10월에 시작한 프로젝트였는데 2023년 2월 초까지 프로토타입이 나와야 한다고 하셔서 Front-End에 신경 쓰지 못해 아쉬웠다.</p>
<blockquote>
<p><strong>3. 앱으로 제작하면 더 실용적이었을 것 같아 아쉬웠다.</strong></p>
</blockquote>
<p>이걸 앱으로 만들면 조금 더 실용적으로 만들 수 있었을 것 같다...</p>
<blockquote>
<p><strong>4. 여전히 부족한 기초 실력...</strong></p>
</blockquote>
<p>이런 사이드 프로젝트를 하면 할수록 기초가 정말 매우 부족하다는 것을 다시 한번 체감하게 된다. 특히 Redis 기초부터 제대로 공부해 보고 싶단 생각이 많이 들었다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>이번 사이드 프로젝트는 내가 원하는 기술로 전체적인 서비스를 만들어 볼 수 있어서 많이 배울 점이 많았던 프로젝트이다.</p>
<p>여기서 배운 것들을 실제로 겜린더에 적용한 것도 있고
여러모로 백엔드에 대한 이해도를 많이 높일 수 있었다.</p>
<p>조금 더 나은 개발자가 되기 위해 단순히 사이드 프로젝트만 진행하는 것이 아닌 꾸준하게
컴퓨터 사이언스를 공부해야겠다는 생각을 가지면서 글을 마친다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[업무시간 4시간에서 1.5시간으로 줄여준 자동화 시스템 후기]]></title>
            <link>https://velog.io/@grit_munhyeok/%EC%97%85%EB%AC%B4%EC%8B%9C%EA%B0%84-4%EC%8B%9C%EA%B0%84%EC%97%90%EC%84%9C-1.5%EC%8B%9C%EA%B0%84%EC%9C%BC%EB%A1%9C-%EC%A4%84%EC%97%AC%EC%A4%80-%EC%9E%90%EB%8F%99%ED%99%94-%EC%8B%9C%EC%8A%A4%ED%85%9C-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@grit_munhyeok/%EC%97%85%EB%AC%B4%EC%8B%9C%EA%B0%84-4%EC%8B%9C%EA%B0%84%EC%97%90%EC%84%9C-1.5%EC%8B%9C%EA%B0%84%EC%9C%BC%EB%A1%9C-%EC%A4%84%EC%97%AC%EC%A4%80-%EC%9E%90%EB%8F%99%ED%99%94-%EC%8B%9C%EC%8A%A4%ED%85%9C-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Tue, 30 May 2023 10:40:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h4 id="사용한-라이브러리">사용한 라이브러리</h4>
<p>(가격 수집 스크래핑 봇)
Selenium, BeautifulSoup4, Pandas, df2gspread
(가격 수정 프로그램)
Tkinter, request</p>
</blockquote>
<p><strong>글을 시작하기 전 소스 코드는 공개를 못한다는 점 양해 부탁드립니다.</strong> 😂</p>
<h1 id="제작의도">제작의도</h1>
<p>한창 용산에 있는 노트북 판매 업체에서 알바했을 때 느꼈던 점은
세상에는 내가 생각한 것보다 정말 많은 노트북들이 판매되고 있다.</p>
<p>그리고 그 노트북들의 가격은 하루가 다르게 가격이 조정되는데
그 가격을 일일이 사람이 수기로 수집해서 관리할 수 있을까라는 생각과 함께
실제로 사장님이 가격을 정리하는 모습을 보았을 때, 수많은 제품 중에서 추려내어 가격 시세를 파악하고 최저가를 찾아내고 구글 시트에 정리하고 실제로 판매하는 스토어에 가격 반영까지 4<del>5시간 이상이 걸렸었다.
_</del>(이것도 주력 제품 기준으로 걸렸던 시간이었다.)~~_</p>
<p>이런 과정을 줄이기 위해 사장님께서 데이터 수집 프로그램 제작을 부탁하셨다.</p>
<hr>
<h1 id="견적을-짜보자">견적을 짜보자</h1>
<h4 id="1-기존-사장님께서-관리하셨던-구글-시트의-불편한-점을-정리-후-기존-형식에서-크게-벗어나지-않고-개선해야-한다">1. 기존 사장님께서 관리하셨던 구글 시트의 불편한 점을 정리 후 기존 형식에서 크게 벗어나지 않고 개선해야 한다.</h4>
<h4 id="2-데이터-수집과-수집한-데이터를-정리해야-하기-때문에-selenium-beautifulsoup4-pandas-공부를-해야-한다">2. 데이터 수집과 수집한 데이터를 정리해야 하기 때문에 Selenium, BeautifulSoup4, Pandas 공부를 해야 한다.</h4>
<h4 id="3-수집한-데이터를-제조사별로-시트를-분리해-추후-데이터에-문제가-생겼을-때-별도로-쉽게-대응할-수-있도록-설계해야-한다">3. 수집한 데이터를 제조사별로 시트를 분리해 추후 데이터에 문제가 생겼을 때 별도로 쉽게 대응할 수 있도록 설계해야 한다.</h4>
<h4 id="4-24시간-프로그램을-돌릴-수-있게-하여-별도의-조작-없이도-연속적으로-수집해야-한다">4. 24시간 프로그램을 돌릴 수 있게 하여 별도의 조작 없이도 연속적으로 수집해야 한다.</h4>
<p>이렇게 총 4가지의 중요 포인트를 가지고 개발을 하게 되었고 결과를 말하자면</p>
<h1 id="결과">결과</h1>
<ol>
<li><p>다나와의 <strong>모든 노트북 제품</strong> 가격을 관리할 수 있게 되었다.
<em>(이론상으로 모든 제품을 수집할 수 있지만 수집 속도를 위해 주요 회사의 제품만 수집하게 되었다.)</em></p>
</li>
<li><p>판매 스토어에 가격 수정을 반자동화 시켜 일일이 가격을 직접 수집하고 판매 스토어 반영까지 <strong>4<del>5시간 걸렸던 시간을 1</del>1.5시간으로 단축</strong>하였다.</p>
</li>
<li><p>판매 스토어에 빠른 가격 수정을 위해 기존 사용했던 쇼핑몰 통합 관리 솔루션에서 제공한 API를 활용해 <strong>가격 수정 프로그램을 별도로 제작하여 가격 수정의 편의성을 높였다.</strong></p>
</li>
</ol>
<hr>
<h1 id="시스템-구조">시스템 구조</h1>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/08ef43e3-d7f6-4b08-bbd9-8970fc9973c9/image.png" alt=""></p>
<h1 id="selenium으로-만든-수집-프로그램">Selenium으로 만든 수집 프로그램</h1>
<p>Python의 Selenium을 활용해 데이터 수집 봇을 만들었다.
이유는 당연하게도 요즘은 한 번에 모든 데이터를 받아와서 보여주는 <strong>정적 웹(Static Web Page)</strong> 형식이 아닌
<strong>동적 웹 (Dynamic Web Page)</strong> 형식이 많기 때문이다.</p>
<p>특히나 다나와에서 각 제조사 별로 데이터를 수집하기 위해서
체크 박스에 표시를 해야 하는 과정이 필요해 Selenium을 활용해 만들어야 했다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/40847d46-f6a1-4f75-ae49-6e4f9b8536e2/image.png" alt=""></p>
<p>그리고 추가로 11번가 사이트도 스크래핑을 하였는데</p>
<p>다나와, 11번가 이렇게 스크래핑 프로그램을 나눠서 분류하여 버그가 발생할 때 수정하기 편하게 만들었다.</p>
<p>해당 프로그램은 24시간 프로그램을 실행해 데이터를 수집했으며</p>
<p>백업 데이터 저장 기능까지 있었다.</p>
<blockquote>
<p>사실 백업 기능은 df2gspread의 버그인데
가끔씩 정해진 시트가 아닌 자체적으로 새로운 시트를 만들어 데이터를 저장하는 현상이 있었다. 하지만 이런 버그 덕분에 백업데이터를 모아둘 수 있어서 가끔씩 데이터가 제대로 구글 시트에 반영이 되지 않을 때 데이터를 가져와 복구할 수 있었다.</p>
</blockquote>
<h1 id="수집한-데이터-저장하기">수집한 데이터 저장하기</h1>
<p><strong>다나와 수집 결과물</strong>
하단에 보면 각 제조사 별로 정리되어 있는 모습을 볼 수 있다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/85e19d2a-2250-4a3f-b5bf-012671889218/image.png" alt=""></p>
<p><strong>11번가 수집 결과물</strong>
검색어가 좀 부정확해서 상품번호로도 추후 통합 관리 시트에 불러올 수 있게 만들었다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/fc3675a7-c992-425a-823b-31eb94d79ef9/image.png" alt=""></p>
<h1 id="통합-관리-시트">통합 관리 시트...</h1>
<p><del>통합 관리 시트는 아쉽게도 공개가 조금 힘들다...</del>
엄청 간단하게 MATCH 함수로 다나와 그리고 11번가의 시트를 참조할 수 있게 만들었다.
이렇게 한 이유는 통합 관리 시트에 정보들이 너무 많아 분산이 필요했고
관리하기에 훨씬 용이할 것 같아 이렇게 구조를 만들었다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/05f0d18f-49c5-4fbe-bf1a-8006847eea17/image.png" alt=""></p>
<h1 id="가격-수정-프로그램">가격 수정 프로그램</h1>
<p>사용자는 자동으로 가격이 수정된 통합 관리 시트를 엑셀로 저장 후 가격 수정 프로그램만 실행하면 끝난다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/01b228d1-0167-4acb-a5c1-21647aad88b7/image.png" alt="">
프로그램은 Python의 Tkinter로 만들었다.</p>
<p>사용 방법은 간단하다.
통합 관리 시트를 엑셀로 저장해 가격 수정을 하고 싶은 시트를 선택하고,
값을 전송할 열을 작성 후 API 전송 버튼을 누르면 가격이 일괄적으로 수정하기 시작한다.</p>
<p>API 서버에 문제가 없다면 제품 하나 당 1~3초의 시간이 소요된다.</p>
<hr>
<h1 id="검증">검증</h1>
<p>아무래도 &quot;<strong>수집한 데이터가 정확한 가?!</strong>&quot;에 대해 의문을 품는 건 당연하고 약 1개월 동안 사장님께서 직접 사용하고 피드백을 주면서 데이터 검증도 끝마쳤다.</p>
<h1 id="아쉬운-점">아쉬운 점</h1>
<p>개인적으로 아쉬웠던 점은 데이터 수집 방지를 위한 게 아닌 쇼핑몰 상위권에 노출을 많이 하기 위해
보통 상품명에 여러 키워드를 많이 넣어두는데 이것 때문에 제품명 추출이 많이 어려웠다.
<del><em>차선책으로 상품코드를 참조할 수 있게 수정을 하긴 했지만... 아쉬운 건 아쉽다...</em></del>
요즘은 Chat-GPT 같은 친구를 활용해서 제품명만 따로 뽑아내주거나
별도로 추출하는 알고리즘이나 AI를 만들어보고 싶었으나... 다른 것들을 만드느라 정신없었다.</p>
<h1 id="마무리">마무리</h1>
<p>이렇게 사용자가 직접 제품 가격을 하나하나 수집하는 것을 봇을 제작해
업무시간을 확 줄였다는 것에 매우 뿌듯했다. 이래서 다들 자동화를 하는구나 싶고
개인적으로 Selenium 과 Python 내장 함수에 대해 많이 알 게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[겜린더의 DB의 문제점과 고쳐야할 것들]]></title>
            <link>https://velog.io/@grit_munhyeok/23.01.28-%EA%B2%9C%EB%A6%B0%EB%8D%94-%EA%B0%9C%EB%B0%9C-%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@grit_munhyeok/23.01.28-%EA%B2%9C%EB%A6%B0%EB%8D%94-%EA%B0%9C%EB%B0%9C-%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Sat, 28 Jan 2023 09:02:12 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>간만에 겜린더 개발 일지를 작성해 보는데
이번에는 백엔드를 중심으로 작업이 되었고
필수 기능들이 대부분 구현이 되었기 때문에 이렇게 정리한다.</p>
</blockquote>
<p><del>사실 노션에 정리해둔거 여기다가 백업한다</del></p>
<p>겜린더의 이번 목표는
단순 사이드 프로젝트에서 멈추는 게 아닌 진짜 서비스를 목표로 제작하는 것이 목표다.
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/03d192dc-885d-4358-8d6c-d887e461fe61/image.png" alt=""></p>
<p>사실 이전에도 서비스를 목표로 만들었고 열심히 업데이트를 했지만
항상 맨날 언급한 <strong>컨텐츠 문제</strong>, 그리고 <strong>스케일업이 불가능한 서버 구조</strong>로 인해 결국 지금은 서버를 그냥 내려놓은 상태다.</p>
<p>오늘의 글은 차례대로 문제점을 적고 어떻게 해결했는지 공유하기 위한 글이라고 생각하면 된다.</p>
<p>그리고 혹시나 구조와 코드를 보면서 더 좋은 방법이 있다면 언제든 댓글을 달아주면 감사하겠습니다. ☺️</p>
<hr>
<h2 id="무조건-개선-및-추가해야-할-목록">무조건 개선 및 추가해야 할 목록</h2>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/b0bf1315-6e24-4e5c-84b4-4b063cfb6a00/image.png" alt=""></p>
<h3 id="1-달력페이지"><strong>1. 달력페이지</strong></h3>
<p><strong>문제점</strong>
<strong>세일 기간 및 게임 정보 빈약</strong></p>
<p>겜린더는 기본적으로 게임이 언제 출시하는지 달력으로 정리해 보여주는 게 핵심이다.</p>
<p>그런데 세일 기간 정보는 가지고 있어도 달력에 제대로 표현하지 못했고, 직관적이지 못해 따로 영역을 만들어서 표시할 예정이고</p>
<p>게임 정보가 빈약한 이유는 여태까지 내 손으로 직접 손수 직접 검색해 일일이 추가했던 구조라 정보가 매우 부족했다.</p>
<p>이러한 문제점들이 결국 백엔드를 새롭게 만들게 된 계기가 되었다.</p>
<hr>
<h3 id="2-백엔드-재설계-및-최적화"><strong>2. 백엔드 재설계 및 최적화</strong></h3>
<p><strong>문제점</strong>
<strong>기존 백엔드는 모든 날짜 정보를 불러와서 달력이 갑자기 초기화되는 현상 발생</strong></p>
<p>이전에 겜린더를 만들고 서비스하면서 위 현상 때문에</p>
<blockquote>
<p><strong>&quot;아... 이거 잘 못 만들었다...&quot;</strong></p>
</blockquote>
<p>라는 생각을 들게 만들었다.</p>
<h3 id="--기존-구조를-보자">- 기존 구조를 보자</h3>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/16d839b9-80a0-486f-a841-0f3e8cede5cc/image.png" alt="">
ㅋㅋㅋㅋㅋㅋㅋㅋㅋ.... 진짜 충격적인 구조</p>
</blockquote>
<p>이때는 <a href="https://github.com/typicode/json-server">json-server</a>를 이용해 만들었는데</p>
<p>전혀 DB와 백엔드 자체에 이해도가 없었던 때라 야매로 만들자~ 느낌으로 했다가 이렇게 다시 제작하게 되었다.</p>
<p>예전에는 그냥 통신만 되어도 오오오오! 하면서 신났을 때라 나중에 스케일업까지는 전혀 고려해 보지 못했던 상황이었다...</p>
<p>*<em>구조를 하나씩 쪼개서 설명하자면 *</em></p>
<p>이미지에 보면 datelist라는 Array가 보일 텐데 이걸 펼치면...</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/baf9c4ec-ddc2-476b-840e-c95f68761a33/image.png" alt="">
지옥이 펼쳐진다...
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/1dac5af9-2af8-40c2-b186-3391614358ba/image.png" alt="">
저 데이터가 달력에는 12월 7일을 보면 .으로 표시가 된다.</p>
</blockquote>
<p>이 리스트 들을 앱을 실행할 때마다 항상 GET 요청을 통해 달력에 표시가 되는 구조인데</p>
<p>이러면 처음 정보가 없을 땐 괜찮지만 나중에 <strong>데이터가 쌓이면 대참사가 발생한다.</strong></p>
<p>왜냐면 <strong>원하지도 않는 날짜까지 전부다 불러오는 구</strong>조라
추후에 서비스를 운영할 때 <strong>서버가 불필요한 자원을 소비</strong>하기 때문이다.</p>
<p>실제로 저렇게 불러와서 달력을 옆으로 넘기면 오버플로우인지 뭔지는 모르겠지만 다시 초기화가 되어버리는 치명적인 버그가 있었다.. (디버깅 로그에도 안 뜬다...)</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/efd85627-ebd1-457a-bfc2-028a40fc7f9d/image.png" alt="">
게임 정보가 들어있는 Array...</p>
</blockquote>
<p>해당 날짜를 이제 누르면 localhost:1234/2021-03-02 이런 식으로 요청을 해 게임 데이터를 가져오는 구조이다
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/8c413780-58b7-452a-a377-810bde691ee9/image.png" alt=""></p>
<p>겉보기엔 괜찮아보이지만...
<strong>이건 엄청난 노가다를 요구하는 구조가 되어버린다.</strong>
한마디로 비효율적인 구조...</p>
<h3 id="--해결하기-위한-방안">- 해결하기 위한 방안</h3>
<p>이를 위해
Redis와 FastAPI를 사용하였다.
Redis는 DB의 역할을 하고
FastAPI는 RESTAPI 형식으로 GET과 POST 요청 등을 처리하기 위한 미들웨어이다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/581e4e2e-f372-4f52-8e32-08f1e2e707de/image.png" alt=""></p>
</blockquote>
<p>게임을 실제로 등록을 하게되면
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/367bac83-fd35-4ad7-a7d2-5d5ab6ea76c8/image.png" alt="">
Redis에선 JSON형식으로 DB에 저장되면서
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/a3efd5bb-c555-4dc2-954e-a02556ef9c9f/image.png" alt=""></p>
<p>이런 식으로 DB에 정리가 된다.
저장 형식을 보면
<strong>game:test indie 3</strong> 로 저장이 되는데
게임 이름으로 저장해서 게임 정보 관리에 더 용이하도록 했다.</p>
<blockquote>
<p>그럼 달력에서 게임 정보는 어떻게 불러옴?</p>
</blockquote>
<p>이라고 물어본다면 <strong>Redis에는 Search 기능이 있어서 Query 검색으로 불러올 수 있게 만들었다.</strong></p>
<p>예를 들어 <strong>2022-08-05일에 출시하는 게임을 찾고 싶으면</strong>
Redis가 JSON 데이터에서 &quot;date&quot; key를 참조하여 2022-08-05와 일치하는 것을 찾아내 표시하는 방식이다.</p>
<p>또 예를 들어 <strong>2022년 8월 게임 정보만 달력에 표시</strong>가 되었으면 좋겠다고 가정하면 JSON 데이터에서 &quot;yearmonth&quot; key를 참조하여</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/32e17ecf-63cc-4b1c-9fdc-031c866c7a56/image.png" alt="">
이런 식으로 표시가 된다.</p>
</blockquote>
<p>이렇게 만들어서 데이터관리와 접근에 훨씬 더 용이해진 것 같다.
<strong>애초에 처음부터 이렇게 만드는 게 맞다고 생각한다...</strong></p>
<p>이번 백엔드 작업을 통해서
스케일업이 상당히 편해졌다. <strong>진짜로</strong></p>
<p>자세하게 코드로 보여주고 싶지만...
노출되면 좀 곤란할 것 같아 코드는 생략하겠다...</p>
<hr>
<h3 id="3-상세-페이지-개선"><strong>3. 상세 페이지 개선</strong></h3>
<p><strong>가독성이 높게 리뉴얼 및 “Push Notification(FCM)”기능 추가</strong></p>
<p>사실 FCM은 이전에 2.0에서 추가했던 내용이라
<a href="https://velog.io/@grit_munhyeok/%EA%B2%9C%EB%A6%B0%EB%8D%94-2.0.0-%EA%B0%9C%EB%B0%9C-%ED%9B%84%EA%B8%B0">겜린더 2.0.0 개발 후기</a>를 참고하면 좋을 것 같다.</p>
<p>핵심은 상세페이지를 개선하는 것이 목표인데 이건 아직 구체적인 계획이 없다... UI를 개선해야 할지... 어떤 것을 도입해야 할지 좀 더 고민해 보고 있다.</p>
<hr>
<h3 id="4-s3object-storage사용"><strong>4. S3(Object Storage)사용</strong></h3>
<p><strong>겜린더 등록 페이지에서 추후 활용</strong>
사실 예전 서비스 출시 때부터 겜린더에 자체적으로 등록신청을 해주시는 분들을 위한 이미지 서버가 존재했는데</p>
<p>이걸 제대로 활용하지 못했고 이번에 아예 등록 페이지를 자체적으로 만들어 이미지를 업로드 시 서버에 스토리지에 저장하도록 계획 중에 있다.</p>
<hr>
<h3 id="5-신청페이지-및-심사-대시보드-제작"><strong>5. 신청페이지 및 심사 대시보드 제작</strong></h3>
<p>React로 제작하고 있고
나름 Figma를 활용해 디자인해봤지만 디자인 하나도 모르는 사람이라 정말 빈약하다.
그래서 요즘 비핸스와 핀터레스트를 미친 듯이 보고있다...(뭐라도 봐야할 것 같아서..)</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/2784d9ba-14b1-4aaf-9350-f94578bd6d8c/image.png" alt=""></p>
<hr>
<h2 id="그-외-개발-도중-이슈와-솔루션들">그 외 개발 도중 이슈와 솔루션들</h2>
<h3 id="1-쿼리-검색-기능-넣을-시----같은-특수-문자-string으로-변환-문제">1. 쿼리 검색 기능 넣을 시 “-”, “.” 같은 특수 문자 String으로 변환 문제</h3>
<p>Q. Redis에선 symbol로 인식한다 어떻게 해결해야 할까..?</p>
<p>A. 파이썬(FastAPI)에서 replace()를 이용해 “-” → “\-” 식으로 백슬래시를 넣으면 string으로 인식한다.
    참고내용 <a href="https://redis.io/docs/stack/search/reference/query_syntax/">Redis query_syntax</a></p>
<hr>
<h3 id="2-날짜-조회-시-text-or-tag로-할지-선택">2. 날짜 조회 시 TEXT or TAG로 할지 선택</h3>
<p>TEXT, TAG가 뭔지는
<a href="https://redis.io/commands/ft.create/">Redis ft.create</a>를 참고해 주세요</p>
<p><strong>TAG로 하면 월/날짜 나눠야 한다</strong>
그래서 date와 yearmonth를 만들어 월별 날짜 조회는 yearmonth로 참조하고</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/32e17ecf-63cc-4b1c-9fdc-031c866c7a56/image.png" alt="">
yearmonth를 참조해서 나온 결과 값</p>
</blockquote>
<p>특정 날짜 조회는 date를 참조하게 만들었다.</p>
<p>TEXT로 index를 만들었는데 연관된 것 까지 검색되고 원하는 결과값이 나오질 않았다.</p>
<p>하지만 TEXT를 잘 이용하여 자동완성 기능도 만들었다
게임 검색 기능에 유용하게 사용될 예정이다.</p>
<hr>
<h3 id="3-limit-문제">3. LIMIT 문제</h3>
<p>이거 때문에 반 년 이상을 지체한 것 같은데 너무 간단해서 허무하다...</p>
<p>Docs를 제대로 안 읽어본 잘 못이긴 하다...
<del>(근데 Docs 내용 너무 방대하고 많다..)</del>
<a href="https://redis-py.readthedocs.io/en/stable/redismodules.html#redis.commands.search.commands.SearchCommands.search">Redis search</a> 부분 참고</p>
<p>LIMIT가 뭐냐면
Redis는 FT.SEARCH로 검색 시 표시할 수 있는 데이터의 량을 정할 수가 있다.</p>
<p>Redis CLI에선 LIMIT로 그것을 조절할 수 있다.</p>
<p>하지만 redis-py를 사용할 땐 <strong>Query(&#39;검색 키워드&#39;).paging(0, 10)</strong> 이런 형식으로 사용하는데</p>
<p>paging이 LIMIT와 같은 역할을 한다.</p>
<p>저거 해결하려고 참 많이 찾아봤었는데 그냥 Docs 스크롤 하다가 발견해 너무 허무했었다...</p>
<hr>
<h3 id="4-docker-→-redis-stack-vs-ubuntu-server-→-redis-stack">4. Docker → Redis-Stack VS Ubuntu Server → Redis-Stack</h3>
<p>일단 지금 로컬 테스트로는 docker를 통해서 사용하고 있지만
추후 서비스할 때는 우분투 서버에 직접 Redis-Stack를 설치할 까 한다..</p>
<p>하드웨어는 역시 언제나 라즈베리파이로 할 예정이다</p>
<hr>
<h3 id="마무리">마무리</h3>
<p>오래 걸리긴 했지만 백엔드의 기반을 다져놓은 것 같아 뿌듯하다.</p>
<p>드디어 프론트엔드에 집중할 수 있게 되었고, 본격적으로 스케일업을 해도 충분히 대응할 수 있는 구조를 가지게 되어서
앞으로 개발이 많이 수월할 것 같다는 생각과 함께 글을 마치겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고록]2022년 한 해를 돌아보며]]></title>
            <link>https://velog.io/@grit_munhyeok/%ED%9A%8C%EA%B3%A0%EB%A1%9D2022%EB%85%84-%ED%95%9C-%ED%95%B4%EB%A5%BC-%EB%8F%8C%EC%95%84%EB%B3%B4%EB%A9%B0</link>
            <guid>https://velog.io/@grit_munhyeok/%ED%9A%8C%EA%B3%A0%EB%A1%9D2022%EB%85%84-%ED%95%9C-%ED%95%B4%EB%A5%BC-%EB%8F%8C%EC%95%84%EB%B3%B4%EB%A9%B0</guid>
            <pubDate>Fri, 30 Dec 2022 01:34:05 GMT</pubDate>
            <description><![CDATA[<hr>
<h2 id="2022년-나는-무엇을-했고-어떤-것을-배웠는가">2022년 나는 무엇을 했고 어떤 것을 배웠는가</h2>
<p>한 번 정리를 해볼 겸 이렇게 글을 작성해 본다.</p>
<p><strong>두서없이 작성해서 분량이 길어요...</strong></p>
<hr>
<h3 id="1-project-mod-현-메이플스토리-월드-공모전">1. Project MOD (현 메이플스토리 월드) 공모전</h3>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/ae144ec7-038f-40e9-b935-432968c8fa96/image.png" alt=""></p>
<p>친구의 권유로 2021년 11월부터 같이 시작했던 공모전이다.
한창 메타버스라는 분야가 엄청 핫한 트렌드로 올라올 때라 친구가 권유해 줬을 때 재밌을 것 같아서 하게 되었다.</p>
<p>프로젝트 런웨이의 컨셉은 사용자가 직접 코디를 만들면서 전시도 하고 추후엔 전시된 코디 중 맘에 드는 것을 살 수 있는 시스템을 구축하려고 했으나,
아쉽게도 개발할 당시엔 코디를 만들 수 있는 스프라이트 에디터 같은 것도 없었던 시기여서 단순하게 이미지만 불러와 맵을 장식했던 것이 너무 아쉬웠던 프로젝트였다.</p>
<p>그래서 이미지를 자세히 보면 코디 신청 주소도 적어두었고,
심지어 두 번째 이미지에는 접근성 좋게 QR코드도 만들었다.</p>
<p>신청받은 코디 이미지를 직접 받아서 에디터에 업로드해서 올리려고 했지만
아무래도 접근성도 그렇고 기능이 부족해서 그런가... 반응은 별로 없었다.. 😂</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/a60a801a-c3e6-4740-8521-948f0f3ac9d5/image.png" alt="">
그래도 친구가 맵 디자인과 코디를 진짜 이쁘게 꾸며서 그런지 댓글 반응은 상당히 좋았다
사실상 친구가 캐리 했다고 봐도 무방...</p>
</blockquote>
<p>물론 내 개인적으론 LUA 스크립트를 사용하는 게 처음이라 그런지 열심히 레퍼런스나 독스를 읽어가면서 하드코딩을 했던 기억이 생생하다...</p>
<p>정 모르겠으면 커뮤니티에 직접 물어보거나 몇 없는 유튜브 강좌를 보면서 했는데도
제대로 못했던 모습 그리고 레퍼런스에 의존할 수밖에 없는 모습을 보면서</p>
<p><strong>실력이 정말 부족한 나 자신을 되돌아볼 수 있었다.</strong></p>
<hr>
<h3 id="2-용산-알바">2. 용산 알바</h3>
<p>나는 컴퓨터 조립이나 기계에 관심이 많아서 예전부터 용산에서 한 번 알바를 해보고 싶다는 생각이 있었다.</p>
<p>그러다가 우연히 용산에서 알바를 할 수 있는 기회가 주어졌고,
그냥 단순한 조립 알바이니깐 몇 개월만 하고 그만 둘 생각이었다.</p>
<p>하지만 알바 사장님하고 대화를 하면서 사실 나를 고용한 이유가 자신이 프로그래머도 찾고 있었는데 
마침 내가 프로그래밍을 할 줄 아는 것 같아 고용했다고 한다.</p>
<p>그러고는 <strong>&quot;XX씨 혹시 크롤링(스크랩핑) 프로그램 만들어볼래요?&quot;</strong>라고 물어보셨고
거기서 나는 이건 기회다 싶어 무조건 &quot;네, 만들어볼게요&quot; 라고 말했다.</p>
<p>사실 이걸 주변 사람들한테 말했을 당시엔
<strong>&quot;아니 무슨 알바생한테 코딩까지 시키려고 하냐... 돈은 더 줘?&quot;</strong>라는 말을 들었다.</p>
<p>겜린더의 게임 수집을 일일이 수작업으로 찾아서 타이핑을 하는 게 너무 힘들었던 나에게 이 제안은 매우 나쁘지 않았던 제안이었다.</p>
<p>마침 알바 사장님도 스크랩핑 프로그램이 필요하다고 하고,
나도 겜린더에 사용해야 할 스크랩핑 프로그램을 만들어야 하는데, 돈도 벌면서 관련 스터디도 할 수 있다는 생각도 들면서</p>
<p>우연이라기엔 타이밍과 시기가 딱 맞물려서 그런가.. 그냥 덥석 물어버렸다</p>
<p>덕분에 셀레니움, BS4, Pandas 등 많은 라이브러리와 데이터 수집하고 엑셀로 이쁘게 정리하는 방법들을 공부했다.
그리고 <strong>사장님이 이런 정리된 데이터를 어떻게 활용해야 하는지, 사업적으로 활용할 수 있는 방법들을 알려주셔서 개인적으론 지금도 하고 있는 이 알바가 상당히 재밌다.</strong> (배울게 많다 진짜)</p>
<p>알바를 다닌 지 거의 9개월이 되어가는 지금
요즘은 알바생이 아니라 거의 스타트업처럼 <strong>신규 플랫폼 프로젝트</strong>도 같이 준비하면서
겜린더에 적용하려고 했던 Redis나 FastAPI 같은 것들을 이용해 서버도 만들고</p>
<p>React를 활용해 홈페이지도 만들면서 느리지만 꾸준하게 성장하고 있는 것 같아 뿌듯하다.
겜린더를 React Native로 개발해 React도 좀 할 수 있지 않을까? 했는데... 뭔가 같으면서도 다른... 느낌을 많이 받았다.</p>
<p>그리고 겜린더 개발할 때는 Hook이 아니라 Class Component로 개발했었는데
React Hook이 더 쉽다고 얼핏 들었던 것 같은데... 내가 멍청해서 그런가 아직 공부가 더 필요해 보인다...</p>
<p>얼른 만들어서 공개하고 싶지만 혼자 개발해서 그런지 느릿느릿하다..
심지어 아직 할 게 너무 많다.. DDNS, https(SSL)인증서 발급, 도메인 구매 등...</p>
<p>이렇게 공부한 것들을 기반으로 겜린더도 슬금슬금 다시 리뉴얼 하려고 개발 중이다.</p>
<hr>
<h3 id="3-네이버-제페토-월드-공모전">3. 네이버 제페토 월드 공모전</h3>
<p>이건 이전에 작성한 글이 있다.
<a href="https://velog.io/@grit_munhyeok/%EB%92%A4%EB%8A%A6%EA%B2%8C-%EC%9E%91%EC%84%B1%ED%95%98%EB%8A%94-%EC%A0%9C%ED%8E%98%ED%86%A0-%EC%9B%94%EB%93%9C-%EA%B3%B5%EB%AA%A8%EC%A0%84-%ED%9B%84%EA%B8%B0">뒤늦게 작성하는 제페토 월드 공모전 후기</a>
<img src="https://velog.velcdn.com/images/grit_munhyeok/post/53bbba70-ad13-456b-b3a7-0e4332d624f3/image.png" alt=""></p>
<p>후기 글은 약간 우수상을 받은 게 신기해서
우리가 어떻게 우수상을 받을 수 있었는가에 대한 분석이었다면
여기엔 내 개인적인 생각을 적어보려고 한다.</p>
<p>사실 메이플 공모전의 후유증이 있어서 그런가.. 이 공모전을 참가하는 게 좀 두려웠었다.
또 제대로 1인분도 못하고 광탈할 것 같은 느낌과 함께 같이하는 친구들에게 너무 미안하기만 할 것 같아서 같이 해야 하나...? 하는 생각을 많이 했었다.</p>
<p>그래서 내가 할 수 있는 역량이 되는가...하면서 제페토 깃허브에도 들어가보고 타입스크립트도 보면서 <strong>어느 정도 내 스스로 자기 객관화를 했던 것 같다.</strong></p>
<p>그래도 최소한 1인분은 할 수 있겠다!라는 판단과 함께 참여하게 되었고
내가 이 공모전 기간에 할 수 있는 것과 없는 것을 잘 판단해 코딩을 한 것 같다.</p>
<p><del>물론 중간에 여러 개인적인 사건들이 참 많이 발생해서 심리적으로 힘들었지만...</del></p>
<p>제출 2주일 전에는 매일 카페에 모여서 10시간 이상 앉아서 작업했던 게 지금 생각해 보면 대단하면서도 재밌었다 ㅋㅋㅋ</p>
<p>한 가지 아쉬운 게 있었는데
제페토 월드 인 게임 안에서 일반 아이템(포션이나 버프 등..)을 팔 수 있는 IWP 기능이 있었지만 
코디(옷, 신발 등) 아이템은 아직 팔 수가 없다고 해서 너무 아쉬웠다..</p>
<hr>
<h3 id="4-겜린더-이야기">4. 겜린더 이야기</h3>
<h4 id="4-1-겜린더-상표권-등록-완료">4-1 겜린더 상표권 등록 완료</h4>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/c2a880c2-3c12-4ccd-a89e-a3c54bed09d9/image.jpg" alt="">
21년 3월에 신청했던 겜린더의 상표권 심사가 22년 6월에 드디어 끝이 났다.</p>
<p>꼭 돈을 많이 벌어서 우선 심사권으로 꼭 신청해야겠다는 생각밖에 들지 않았다... :)</p>
<h4 id="4-2-겜린더-리뉴얼">4-2 겜린더 리뉴얼</h4>
<blockquote>
<p>이 정도면 겜린더 서비스 안 하고 접은 거 아니냐?!</p>
</blockquote>
<p>라는 말이 나올 수 있지만...
사실 꾸준하게 겜린더를 리뉴얼 하고 있었다.
어떤식으로 리뉴얼을 하고 있는지 정리하자면..</p>
<p><strong>Front</strong>
리액트 네이티브로 여전히 개발 중이지만 최근에 코드가 너무 더럽게 짜여 있고 코드를 어떻게 해야 할지 애매해서 그냥 새롭게 다시 짜기로 했다. 기존 프로젝트에는 state가 너무 많아서 관리가 점점 힘들어졌었는데 이를 위해 <strong>Redux</strong>를 사용할 명분이 생겨버려 요즘 Redux를 공부 중에 있다.</p>
<p><strong>Middleware</strong>
FastAPI를 활용해 API 형식으로 정보를 받을 수 있게 만들고 추후에는 다른 곳에서도 활용할 수 있게 이 정보를 오픈할 예정이다.</p>
<p><strong>Back</strong>
Redis를 이용해서 DB를 새롭게 다시 구축 중인데 용산에서 알바하면서 Redis를 계속 공부하다 보니 DB를 다시 만들어야 할 것 같은 기분이 든다... 하지만 금방 만들 수 있을 것 같다.</p>
<p><strong>게임 등록 페이지</strong>
현재 React로 게임 등록 페이지를 만들고 있다.
기존 구글 시트로 서비스를 했으나, 일일이 수동으로 입력해서 또 업로드를 해야 했던 게 현실이라
이번 기회에 등록 페이지와 그에 맞는 대시보드까지 만들려고 계획 중에 있다.</p>
<hr>
<h3 id="2022년-마무리">2022년 마무리</h3>
<p>곧 2022년이 끝나간다.
2023년에는 어떤 일들이 생길지 정말 기대되고,
내년이야말로 내가 2022년에 해왔던 작업들이 전부 완성되어 제대로 된 서비스를 할 수 있는 한 해가 될 수 있도록 노력해야겠다.</p>
<p>할 거면 끝장을 봐야 하는 내 성격이 성장을 더디게 할 수도 있지만 <strong>개인적으론 끝까지 해보고 실패해 봐야 원인 분석을 정확하게 하고 다음에는 똑같은 실수를 반복하지 않았다...</strong></p>
<p>대신 조급하게만 하지 않았으면 좋겠다... 제발...</p>
<p>내년에도 열심히 삽질해 보고 치이면서 살아봐야겠다.</p>
<p><em>2022년 회고록 끝</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[뒤늦게 작성하는 제페토 월드 공모전 후기]]></title>
            <link>https://velog.io/@grit_munhyeok/%EB%92%A4%EB%8A%A6%EA%B2%8C-%EC%9E%91%EC%84%B1%ED%95%98%EB%8A%94-%EC%A0%9C%ED%8E%98%ED%86%A0-%EC%9B%94%EB%93%9C-%EA%B3%B5%EB%AA%A8%EC%A0%84-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@grit_munhyeok/%EB%92%A4%EB%8A%A6%EA%B2%8C-%EC%9E%91%EC%84%B1%ED%95%98%EB%8A%94-%EC%A0%9C%ED%8E%98%ED%86%A0-%EC%9B%94%EB%93%9C-%EA%B3%B5%EB%AA%A8%EC%A0%84-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 19 Sep 2022 10:03:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>3월에 친구의 권유로 시작하고 7월에 마무리된
제페토 월드 공모전 후기를 뒤늦게 작성해 봅니다.</p>
</blockquote>
<blockquote>
<p><a href="https://page.zepeto.me/5662O3wK875tc5JXnhB0d09?lang=ko">수상작 발표 링크</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/6c5de4d4-25bb-4781-9aee-461ac2cdf66a/image.png" alt=""></p>
<p><strong>아트 2, 클라이언트 1</strong> 이런 조합으로 진행했고</p>
<p>처음 제출할 때 저희 팀 모두 다 같이 <strong>&quot;이걸 제출했네...?&quot;</strong> 하면서 신기했었고
우수상을 받을 때도 너무 기쁘기도 했지만 우수상까지 받을 줄은 사실 생각도 못 했었습니다.</p>
<p><img src="https://velog.velcdn.com/images/grit_munhyeok/post/023daa34-a2a1-4dfc-9448-cb71256307fd/image.png" alt=""></p>
<p>왜냐하면 팀원 모두가 <strong>여러 가지로 많이 미숙하다고 판단했고</strong>
그냥 참가에 의미를 두자라고 생각했습니다.</p>
<p>하지만 우수상을 받았기 때문에
<strong>&quot;우리가 어떻게 해서 우수상을 받을 수 있었을까?&quot;</strong>
에 대해 여러 번 고민을 했었는데</p>
<p>딱 <strong>3가지 이유</strong>가 있었다고 생각합니다.
이번 글은 그 이유를 작성해 보려고 합니다.</p>
<h2 id="1-주어진-일에-끝까지-책임진다">1. 주어진 일에 끝까지 책임진다.</h2>
<p>일단 같이 참여했던 친구들이 책임감이 매우 좋은 친구들이었습니다.</p>
<p>사실 이건 너무 당연한 게 아닌가?라고 생각할 수 있지만
생각보다 제 주변엔 많이 없더라고요..</p>
<p>물론 제 자신도 엄청 책임감이 강하다고 생각하진 않지만 주어진 일에 항상 최선을 다하려고 노력합니다.</p>
<h2 id="2-타인의-의견-수렴을-잘하고-자기-객관화를-잘한다">2. 타인의 의견 수렴을 잘하고 자기 객관화를 잘한다.</h2>
<p>기획을 했을 당시 프로그래밍을 맡은 저를 제외한 나머지
아트 2명은 게임을 하지 않았던 친구들이었고</p>
<p>심지어 저는 자바스크립트는 겜린더 개발 때문에 조금 했었지만 타입스크립트는 정말 처음이었기 때문에 사실 기술적인 이슈도 상당히 많았던 팀이었습니다.</p>
<p>그래서 처음 기획을 할 때 많은 의견 충돌이 있을까 봐 걱정을 많이 했습니다.</p>
<h3 id="하지만">하지만</h3>
<p><strong>서로 못하는 부분은 확실하게 말하면서 타협점을 찾아나갔던 모습이 이번 공모전에 입상을 할 수 있던 큰 이유라고 생각합니다.</strong></p>
<h2 id="3-꾸준하게-스터디를-했다">3. 꾸준하게 스터디를 했다.</h2>
<p>이번에 같이 아트를 한 친구들도 모델링을 배우긴 했지만 아직 미숙하기도 했고 유니티를 아예 몰랐다고 해도 무방했고</p>
<p>제 자신도 유니티를 제대로 사용해 보지도 않았던 상태였습니다.</p>
<p>아트 친구들은 학교에서 동아리나 스터디 모임으로 배우고</p>
<p>저는 제페토 깃허브 커뮤니티를 찾아보면서 수많은 레퍼런스들을 공부하고 바로바로 적용해 보는 방식으로 스터디를 꾸준하게 병행한 덕분에 좋은 결과물을 얻을 수 있었다고 생각합니다.</p>
<h2 id="공모전을-마무리하고-들었던-생각">공모전을 마무리하고 들었던 생각...</h2>
<p>다른 이유도 있겠지만 이 3가지 이유가 제일 우수상까지 받을 수 있었던 큰 원동력이라고 생각하고</p>
<p>제페토 공모전을 하면서 이번에 같이 한 친구들하고 만약 다른 프로젝트를 해도 정말 재밌게 할 수 있겠다는 생각이 많이 들었습니다.</p>
]]></description>
        </item>
    </channel>
</rss>