<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>halfmoon_mind.log</title>
        <link>https://velog.io/</link>
        <description>Do My Best</description>
        <lastBuildDate>Mon, 05 May 2025 18:12:41 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>halfmoon_mind.log</title>
            <url>https://velog.velcdn.com/images/halfmoon_mind/profile/d709c775-ef8c-465d-bc93-0106c0d2abf9/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. halfmoon_mind.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/halfmoon_mind" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Docker 환경의 PostgreSQL에서 Supabase로 데이터베이스 마이그레이션]]></title>
            <link>https://velog.io/@halfmoon_mind/Docker-%ED%99%98%EA%B2%BD%EC%9D%98-PostgreSQL%EC%97%90%EC%84%9C-Supabase%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@halfmoon_mind/Docker-%ED%99%98%EA%B2%BD%EC%9D%98-PostgreSQL%EC%97%90%EC%84%9C-Supabase%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</guid>
            <pubDate>Mon, 05 May 2025 18:12:41 GMT</pubDate>
            <description><![CDATA[<p>데이터베이스 마이그레이션은 개발자라면 한 번쯤 겪는 도전적인 과제입니다. 특히 버전 차이가 있는 경우 더욱 까다로워질 수 있습니다. 이번 글에서는 EC2에서 실행 중인 Docker 컨테이너의 PostgreSQL 17 데이터베이스를 Supabase(PostgreSQL 15)로 마이그레이션하는 과정과 그 과정에서 발생한 문제들, 그리고 최종 해결책을 공유하려고 합니다.</p>
<h2 id="목차">목차</h2>
<ol>
<li>상황 개요</li>
<li>첫 번째 시도: pg_dump와 직접 복원</li>
<li>두 번째 시도: SQL 덤프 파일 활용</li>
<li>세 번째 시도: Supabase CLI</li>
<li>최종 해결책: DBeaver를 이용한 테이블 마이그레이션</li>
<li>교훈 및 결론</li>
</ol>
<h2 id="1-상황-개요">1. 상황 개요</h2>
<p>프로젝트를 진행하면서 기존에 EC2 인스턴스 내부의 Docker 컨테이너에서 실행 중이던 PostgreSQL 17 데이터베이스를 Supabase로 이전해야 하는 상황이 발생했습니다.</p>
<p>변경하려고 했던 첫번째 문제는, 비용적인 문제로 ec2 t2.small 인스턴스에서 t2.micro로 변경하려고 하였고, cpu 보다는 메모리 사용량을 줄이기 위해서 다른 프로세스들을 최소한으로 사용하려고 했습니다.</p>
<p>또한 같은 ec2 내에서 DB와 서버 같이 떠있는 경우에는 해당 ec2에 접근하지 못하거나, 꺼져버리는 경우에는 자칫하면 DB까지 복구할 수 없을 정도로 큰 문제가 발생할 수도 있었습니다. 따라서 DB를 따로 클라우드 서비스를 활용하려고 했지만, AWS RDS에 비해서 무료 플랜을 사용할 수 있는 supabase를 사용하려고 하였습니다.</p>
<p>하지만 현재 Supabase는 PostgreSQL 15를 사용하고 있는데, 우리의 기존 데이터베이스는 최신 버전인 PostgreSQL 17을 사용하면서 버전이 맞지 않는 문제가 발생했습니다.</p>
<h2 id="2-첫-번째-시도-pg_dump와-직접-복원">2. 첫 번째 시도: pg_dump와 직접 복원</h2>
<p>가장 먼저 시도한 방법은 PostgreSQL의 기본 백업 도구인 <code>pg_dump</code>를 사용하여 데이터를 덤프하고, 이를 Supabase에 복원하는 것이었습니다.</p>
<pre><code class="language-bash"># Docker 컨테이너에서 데이터베이스 덤프
docker exec -it [container_name] pg_dump -U [username] -F c -b -v -f /tmp/database_dump.backup [database_name]

# 덤프 파일을 로컬로 복사
docker cp [container_name]:/tmp/database_dump.backup ./database_dump.backup
</code></pre>
<p>이후 Supabase에서 이 백업 파일을 복원하려고 했으나 다음과 같은 오류가 발생했습니다:</p>
<pre><code>pg_restore: 오류: 파일 헤더에 있는 1.16 버전은 지원되지 않습니다
</code></pre><p>이 오류는 PostgreSQL 17에서 생성된 덤프 파일이 PostgreSQL 15에서 직접 복원될 수 없음을 의미합니다. PostgreSQL은 대체로 상위 버전과의 호환성은 보장하지만, 하위 버전과의 호환성은 보장하지 않기 때문입니다.</p>
<h2 id="3-두-번째-시도-sql-덤프-파일-활용">3. 두 번째 시도: SQL 덤프 파일 활용</h2>
<p>다음으로 ChatGPT의 도움을 받아 일반 SQL 형식으로 덤프를 생성하여 Supabase의 SQL Editor에서 실행하는 방법을 시도했습니다.</p>
<pre><code class="language-bash"># 일반 SQL 형식으로 덤프 생성
docker exec -it [container_name] pg_dump -U [username] --schema-only --no-owner -f /tmp/schema.sql [database_name]
docker exec -it [container_name] pg_dump -U [username] --data-only -f /tmp/data.sql [database_name]

# SQL 파일을 로컬로 복사
docker cp [container_name]:/tmp/schema.sql ./schema.sql
docker cp [container_name]:/tmp/data.sql ./data.sql
</code></pre>
<p>하지만 이 방법 역시 문제가 발생했습니다:</p>
<pre><code>psql:backup.sql:224: 오류: 잘못된 명령: \N</code></pre><p>또한 UUID 타입의 데이터가 제대로 인식되지 않는 문제도 발생했습니다. PostgreSQL 17과 15 사이의 데이터 타입 변경이나 SQL 문법 차이가 원인으로 보였습니다.</p>
<h2 id="4-세-번째-시도-supabase-cli">4. 세 번째 시도: Supabase CLI</h2>
<p>Supabase CLI를 활용한 방법도 고려했습니다. Supabase CLI는 로컬 개발 환경과 Supabase 프로젝트 간의 데이터 동기화를 위한 도구를 제공합니다.</p>
<pre><code class="language-bash"># Supabase CLI 설치
npm install -g supabase

# 로그인
supabase login

# 데이터베이스 참조
supabase db remote set --db-url postgresql://[connection_string]

# 마이그레이션 시도
supabase db push</code></pre>
<p>하지만 이 접근법 역시 버전 차이로 인한 호환성 문제로 완전한 해결책이 되지 못했습니다.</p>
<h2 id="5-최종-해결책-dbeaver를-이용한-테이블-마이그레이션">5. 최종 해결책: DBeaver를 이용한 테이블 마이그레이션</h2>
<p>여러 시도 끝에 발견한 해결책은 오픈소스 데이터베이스 툴인 <strong>DBeaver</strong>를 활용하는 것이었습니다. DBeaver는 DataGrip과 유사한 무료 도구로, 다양한 데이터베이스 간의 마이그레이션 기능을 제공합니다.</p>
<h3 id="dbeaver를-활용한-마이그레이션-단계">DBeaver를 활용한 마이그레이션 단계:</h3>
<ol>
<li>DBeaver 설치 및 실행</li>
<li>소스 데이터베이스(EC2의 Docker PostgreSQL 17) 연결 설정</li>
<li>대상 데이터베이스(Supabase PostgreSQL 15) 연결 설정</li>
<li>소스 데이터베이스에서 마이그레이션할 테이블 선택</li>
<li>우클릭 후 &quot;Export Data&quot; → &quot;Database Transfer&quot; 선택</li>
<li>대상 데이터베이스 선택 및 옵션 설정<ul>
<li>스키마 생성 옵션 선택</li>
<li>데이터 마이그레이션 옵션 선택</li>
<li>제약 조건 및 인덱스 옵션 설정</li>
</ul>
</li>
<li>마이그레이션 실행</li>
</ol>
<p>이 방법을 통해 성공적으로 데이터를 마이그레이션할 수 있었습니다. DBeaver는 데이터베이스 버전 차이를 자동으로 처리하고, 필요한 변환을 수행하여 데이터 및 스키마를 올바르게 이전해주었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발의 문서화와 문서 공유]]></title>
            <link>https://velog.io/@halfmoon_mind/%EA%B0%9C%EB%B0%9C%EC%9D%98-%EB%AC%B8%EC%84%9C%ED%99%94%EC%99%80-%EB%AC%B8%EC%84%9C-%EA%B3%B5%EC%9C%A0</link>
            <guid>https://velog.io/@halfmoon_mind/%EA%B0%9C%EB%B0%9C%EC%9D%98-%EB%AC%B8%EC%84%9C%ED%99%94%EC%99%80-%EB%AC%B8%EC%84%9C-%EA%B3%B5%EC%9C%A0</guid>
            <pubDate>Mon, 06 May 2024 07:25:53 GMT</pubDate>
            <description><![CDATA[<blockquote>
<img src="https://velog.velcdn.com/images/halfmoon_mind/post/8f417203-a55f-450e-bc1a-4fdf63018269/image.jpg" width=300>
이 기록은 "구글 개발자는 이렇게 일한다"라는 책을 보고 느낀 점을 정리한 글입니다. 글쓴이의 주관적인 경험이 들어있음을 알려드립니다.
</blockquote>
<h2 id="버스-지수">버스 지수</h2>
<p>책에서는 <strong>버스 지수</strong>라는 개념에 대해서 소개한다.</p>
<blockquote>
<p>버스 지수란 <strong>몇 명의 팀원이 버스에 치여서 일을 할 수 없을 때, 프로젝트가 망하게 되는지에 대한 지수</strong>를 의미한다.</p>
</blockquote>
<p>결국 버스 지수가 낮으면 낮을수록, 적은 팀원들에게 정보가 집중되었다는 의미이고, 적으면 적을 수록 위험한 상황인 것이다.
팀원 중 누군가가 퇴사하거나, 휴가를 내는 경우 해당 인원이 없는 만큼 서비스가 정상적으로 작동하지 않을 가능성이 높아지는 것이고 담당자는 결국 업무 과다로 힘들어 할 것이다.</p>
<p>그렇다면 이것을 어떻게 버스 지수를 높여 안정적인 상황으로 만들 수 있을까?
방법은 여러가지가 있다.
요즘 IT 회사에서 많이 사용하는 &quot;페어 프로그래밍&quot;과 같은 방법이나, 문제점이나 어려웠던 점, 고민한 점을 공개적으로 다른 사람들과 공유하는 것이다.</p>
<p>페어 프로그래밍을 활용한다면 코드를 짤 때, 강제로 피드백을 주고 받으면서 코드안에 들어간 누군가의 생각을 2명이서 공유하게 된다.
따라서 기존에 버스에 1명이 치이면 해당 기능을 유지보수 할 수 없는 인원이 없어졌다면, 이제는 2명이 모두 치여야 해당 기능을 유지보수 할 수 없는 것이다.</p>
<p>다른 방법은 공개적으로 다른 사람과 공유하는 것이다.</p>
<h2 id="구글의-지식-공유">구글의 지식 공유</h2>
<p>구글에는 &quot;메일링 리스트&quot;라는 것이 존재한다고 한다.
특정한 주제에 대해서 구글 그룹스를 만들고, 누구든지 가입하고 메일을 보낼 수 있도록 하다는 것이다. 이를 통해 해당 주제에 대해서 답을 줄 수 있는 많은 사람들에게 전달되고, 모두가 해당 질문을 보면서 해당 개념을 잘 모르던 사람들도 지식이 공유될 수 있다는 것이다.
나는 이 기능이 정말 유용하다고 생각이 든다. 단순히 내가 어떤 문제를 맞닥뜨리게 되었을 때, 그것을 해결한 사람 혹은 고민한 사람들과 공유하고 더 좋은 방식에 대해서 고민하게 되는 선순환이 되기 때문이다.</p>
<h2 id="내-경험">내 경험</h2>
<p>가장 인상 깊었던 기억은 <a href="https://apps.apple.com/kr/app/%EA%B2%8C%EB%8D%94%EB%A7%81-%ED%95%A8%EA%BB%98-%EC%93%B0%EB%8A%94-%EA%B3%B5%EC%9C%A0-%EC%BA%98%EB%A6%B0%EB%8D%94/id1643475991">게더링</a>을 만들던 시절이었다.
당시에 게더링을 만들 때는 내가 서비스 개발을 처음 해보았을 때였다. (슬랙도 처음 써보았다..)
이 때는 어떤 것들을 물어볼 때, 개인 DM으로만 묻고 답하기를 좋아했다. 굳이 다른 사람들에게 알릴 필요가 있을까?라는 생각이었다.
하지만 PM의 생각은 달랐다. 자신이 어떤 어려운 점이 있고, 어떤 일들을 하는 것을 모두에게 공개해야한다는 것이었다. 그래서 DM 보다는 전체 채팅방에서 공유해달라고 하는 것이었다.
물론 PM 입장에서는 모든 사람들이 어떤 일을 하고 있는지에 대해서 트래킹이 되어야 정상적으로 서비스 운영이 되겠지만, 나에게는 크게 와닿지 않았다.
하지만 내가 2시간 동안 끙끙 앓고 어려웠던 문제를 공개적으로 모든 팀원한테 물어보니 10분만에 해결된 경험을 통해서 나도 자연스럽게 모든 팀원에게 공유하는 습관을 들이려고 노력했다.
물론 일을 하는 도중에 흐름이 끊기는 일이 있어서는 안되겠지만, 그게 아니라면 팀 전체의 생산성을 높이는데 크게 도움이 되는 것처럼 보인다.</p>
<h2 id="적용하고-싶은-개념">적용하고 싶은 개념</h2>
<p>나중에 시간이 된다면 Slack과 같은 메신저에 자유로운 질의응답을 받을 수 있는 채널을 하나 만들고 싶다.
내가 해결하다가 어려웠던 것들은 다른 누군가에게도 어려울 것이다. 만약 같은 문제를 겪는다면 어떻게 해결했다는 것을 기억하는 것보다 기록을 찾는 것이 훨씬 빠를 것이기 때문이다.
추후에 어려웠던 점을 해결한 방법에 대해서 팀단위로 기술 블로그를 작성하기에도 좋은 것 같고!
결과적으로 처음에 얘기했던 버스 지수를 낮추는데 크게 도움이 될 것이다.</p>
<hr>
<h3 id="여담">여담</h3>
<p>지금 회사에서는 데일리스크럼을 통해서 서로 어려웠던 점, 공유하고 싶은 점을 따로 시간을 내서 공유하고 있는데, 이건 생각보다 비효율이 많은 것 같다.
내가 어떤 것들을 했고, 어떤 일을 할 것이다라는 것은 공유되기 좋은데, 다른 사람이 어떤 일을 하고 있다는 걸 굳이 알 필요가 없다고 생각이 든다.
그래서 공유할 안건들만 딱딱 공유하면 좋을 것 같다.
위에서 얘기했던 자신이 경험한 어려운 점과 해결 방법 정도를 서로 공유하는 시간으로 활용하면 어떨까?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS] Switch란?]]></title>
            <link>https://velog.io/@halfmoon_mind/CS-Switch%EB%9E%80</link>
            <guid>https://velog.io/@halfmoon_mind/CS-Switch%EB%9E%80</guid>
            <pubDate>Sat, 20 Apr 2024 09:32:27 GMT</pubDate>
            <description><![CDATA[<h1 id="학습-목표">학습 목표</h1>
<ul>
<li>Switch에 대한 개념을 공부한다.</li>
<li>Switch와 Router의 차이점을 알 수 있다.<h1 id="switch란">Switch란?</h1>
스위치는 2개 이상의 IT 디바이스가 서로 통신하도록 허용하는 장비이다.
이더넷 프레임을 MAC 주소 기반으로 스위칭하고 포워딩한다.</li>
</ul>
<ol>
<li>프레임 포워딩</li>
<li>MAC 주소 학습</li>
<li>콜리전 도메인 분리</li>
<li>플로우 컨트롤</li>
<li>VLAN</li>
<li>STP (Spanning Tree Protocol)
또한 내부에서 사용할 수 있는 기능은 5가지가 있다.<h4 id="switch의-기능">Switch의 기능</h4>
</li>
<li>Learning</li>
<li>Flooding</li>
<li>Fowarding</li>
<li>Filtering</li>
<li>Aging</li>
</ol>
<p>또한 라우터를 L3 스위치라고 말하기도 하는데, 단순하게 기기끼리 네트워크 연결이 이루어지게 하는 기기라면 스위치라고 할 수 있기 때문입니다.</p>
<h1 id="switch의-작동-방식">Switch의 작동 방식</h1>
<p>디바이스 간에 통신하는 방식은 스위치 자체를 포함하여 연결된 모든 시스템이 표준 통신 프로토콜 세트를 따르도록 하는 것이다. 이러한 표준은 IEEE 및 IETF 등의 국제 표준 기구가 정의하고 관리한다.
통신은 패킷 단위로 이루어지고, 패킷은 헤더와 페이로드를 포함하고 있는데 헤더에는 해당 통신에 참여하는 디바이스의 출발지 및 목적지 주소와 같은 정보가 들어 있고, 페이로드에는 네트워크상의 디바이스가 실제로 교환하려는 데이터가 들어 있습니다. 네트워크상의 각 디바이스에는 패킷을 보낼 수 있는 주소가 한 개 이상 있습니다.
스위치는 패킷 헤더의 주소를 읽은 다음 이 패킷을 해당되는 목적지로 전달한다.</p>
<h1 id="switch를-사용하는-이유">Switch를 사용하는 이유</h1>
<p>스위치는 네트워크상에서 사용자와 어플라이언스 및 장비를 연결하여 이들이 서로 통신하고 리소스를 공유할 수 있게 한다.</p>
<p>스위치의 중요한 역할은 &quot;가상 네트워크&quot;을 생성하는 것이다. 가상 네트워크는 네트워크 관리자가 제공하는 구성에 따라 네트워크 연결된 시스템을 여러 그룹으로 나누어 분리가 가능하다.
특정 시스템을 나머지 시스템으로부터 안전하게 분리하면서 다수의 시스템을 한 개의 물리적 네트워크에 연결할 수 있다. 
가상 네트워크 유형에는 VPN(Virtual Private Network), 가상 LAN(VLAN) 등이 있다.</p>
<h2 id="learning-mac-주소-학습-방법">Learning (MAC 주소 학습 방법)</h2>
<ol>
<li>프레임 수신<ol>
<li>스위치는 프레임을 수신하고, 해당 프레임은 출발지 MAC 주소, 목적지 MAC 주소를 담고 있다.</li>
</ol>
</li>
<li>출발지 MAC 주소 학습<ul>
<li>스위치는 수신한 프레임의 출발지 MAC 주소를 확인하고, 이 주소와 프레임을 수신한 포트 번호를 연결하여 MAC 주소 테이블에 저장한다. 이를 통해 스위치는 특정 MAC 주소가 어떤 포트와 연결되어 있는지 학습한다.</li>
</ul>
</li>
<li>MAC 주소 테이블 업데이트<ul>
<li>스위치는 지속적으로 네트워크 통신을 모니터링하며, 새로운 MAC 주소를 학습하고 기존의 MAC 주소 정보를 업데이트합니다. 만약 특정 MAC 주소로부터 오랜 시간 동안 통신이 없다면, 해당 MAC 주소 정보는 테이블에서 제거될 수 있습니다.</li>
</ul>
</li>
<li>프레임 전달<ul>
<li>스위치는 수신한 프레임의 목적지 MAC 주소를 확인하고, MAC 주소 테이블을 참조하여 해당 주소가 어떤 포트에 연결되어 있는지 확인하고 프레임을 해당 포트로 전달한다. 만약 목적지 MAC 주소가 테이블에 없다면, 스위치는 프레임을 모든 포트로 전달하여 올바른 목적지를 찾습니다(Flooding).</li>
</ul>
</li>
</ol>
<h3 id="flooding">Flooding</h3>
<p>예를 들어 PC 1번에서 PC 2번으로 통신을 요청하는데, 그 중간에 있는 Switch의 MAC Table에 2번 PC의 MAC 주소가 저장되어 있지 않다면, 자신과 연결된 모든 포트에게 요청을 보냅니다.
이 기능을 flooding이라고 하고, 이것을 통해서 자신이 해당하는 MAC 주소라면 reply를 제공하고, 이를 통해 목적지 MAC주소를 알 수 있습니다.</p>
<h3 id="fowarding">Fowarding</h3>
<p>가장 중요한 기능인 프레임 포워딩입니다.
스위치는 2개 이상의 기기가 연결되어 있고, 목적지의 주소를 알고 있다면 목적지까지 Frame을 이동하도록 만들 수 있습니다.</p>
<h3 id="filtering">Filtering</h3>
<p>데이터를 보낼 때 출발 MAC주소, 도착 MAC주소를 명시해서 보낼 텐데, 만약 양끝점 주소가 MAC 주소 테이블에 존재한다면 해당 맥 주소가 아닌 주소들에게는 데이터를 보내지 않습니다.</p>
<h3 id="aging">Aging</h3>
<p>맥 주소 테이블을 영원이 유지할 수는 없습니다.
따라서 맥 주소에 대한 유효시간을 놔둡니다.
해당 시간이 만료되면 맥 주소 테이블에서 삭제합니다.</p>
<h2 id="vlan">VLAN</h2>
<p>VLAN이란 Virtual LAN으로서 물리적 배치와 상관없이 LAN을 논리적으로 분할, 구성하는 기술이다.
각 VLAN은 별도의 네트워크가 됨으로, VLAN간의 통신에는 3계층의 도움이 필요하다.
Trunking을 통해서 Tagged Port를 사용해 VLAN 사이에서 패킷을 주고 받을 수 있다.
이더넷 프레임에 VLAN ID 필드를 추가하여 태그 정보를 사용할 수 있다.</p>
<p>태그를 보낼 때는 VLAN ID를 붙이고, 수신측에서 VLAN ID를 제거하여 VLAN ID의 VLAN으로 패킷을 전송할 수 있다.</p>
<p>+) <img src="https://velog.velcdn.com/images/halfmoon_mind/post/88896459-fc3a-4d92-baaa-672e48fd1a1c/image.png" alt=""></p>
<p>VLAN을 사용하면 MAC 테이블에서도 VLAN 값이 함께 기록된다.</p>
<h2 id="stp-spanning-tree-protocol">STP (Spanning Tree Protocol)</h2>
<p>stp 프로토콜이란, Spanning Tree Protocol로서 Ethernet Frame이 장비들에서 빙빙 도는 Looping하는 현상을 방지하기 위한 프로토콜이다.
Frame Looping이 발생하면 다음과 같은 문제가 생길 수 있다.</p>
<ol>
<li>BroadCasting Storm (브로드캐스팅 폭풍) 발생
 특정 스위치에서 BroadCast Frame을 보낸 경우, 받은 스위치는 연겨로딘 모든 포트로 Flooding을 진행한다. 이 경우 STP가 작동하지 않는다면, 모든 스위치에서 브로드캐스팅을 각자 진행하게 됨으로 정상적인 작동이 되지 않거나 시스템이 다운될 수 있다.</li>
<li>MAC Address Table 불안정
 Looping이 발생하면 동일한 MAC Address를 다른 인터페이스에서 받게 된다.
 이 경우 MAC Address Table이 계속 변화하는 MAC Table Instability(불안정) 현상이 발생한다.
 다음 예시를 들으면 쉽게 이해할 수 있다.<blockquote>
<ol>
<li>장치 A가 포트 1에 연결되어 있고, 스위치는 장치 A의 MAC 주소가 포트 1로부터 왔다는 것을 MAC 주소 테이블에 기록한다.</li>
<li>루핑이 발생한 경우, 장치 A로부터 온 패킷이 다른 경로를 통해 포트 2로도 들어올 수 있다.</li>
<li>스위치는 이 패킷을 포트 2에서 수신하고, MAC 주소 테이블을 업데이트하여 장치 A의 MAC 주소가 포트 2로부터 왔다고 기록한다.</li>
<li>이런 식으로 테이블은 빠르게 변동하게 되며, 이는 네트워크의 안정성을 저해한다.</li>
</ol>
</blockquote>
</li>
</ol>
<p>STP는 다음과 같은 순서로 동작한다.</p>
<ol>
<li>Root Bridge 선출<ul>
<li>내부 네트워크의 모든 스위치 중 하나를 Root Bridge로 선출한다.</li>
</ul>
</li>
<li>경로 계산<ul>
<li>각 스위치는 Root Bridge까지의 최적 경로를 계산한다.</li>
</ul>
</li>
<li>Blocking<ul>
<li>최적 경로가 아닌 다른 경로는 차단(Block)된 상태로 변경되고, 이로서 Looping이 발생하지 않는다.</li>
</ul>
</li>
</ol>
<p><strong>Root Bridge를 선출하는 방법</strong>은 다음과 같다.</p>
<ol>
<li><strong>초기 상태</strong><ol>
<li>모든 스위치는 자신을 Root Bridge로 간주하고, 각 스위치는 자신의 Bridge ID (BID)를 사용하여 BPDU를 생성하고 전송한다. BID는 스위치의 Priority 값과 MAC 주소를 결합하여 생성됩니다.</li>
</ol>
</li>
<li><strong>BPDU의 비교</strong><ol>
<li>스위치는 다른 스위치들로부터 수신한 BPDU들과 자신이 보낸 BPDU를 비교한다. BPDU의 비교는 우선순위(Priority) 값으로 시작하며, 동일한 경우에는 MAC 주소를 비교하여 더 낮은 값을 갖는 스위치의 BPDU를 선호하게 된다.</li>
</ol>
</li>
<li><strong>BPDU 전파</strong><ol>
<li>스위치가 다른 스위치로부터 더 우선적인 BPDU를 수신하면, 해당 스위치는 자신이 Root Bridge가 아니라는 것을 인지하고, 수신한 &quot;더 좋은&quot; BPDU를 다른 포트를 통해 전파한다.</li>
</ol>
</li>
<li><strong>Root Bridge 선출</strong><ol>
<li>위의 과정을 통해 네트워크 내의 모든 스위치들이 동일한 BPDU (즉, 가장 우선적인 BPDU)를 수신하게 되면, 해당 BPDU를 생성한 스위치가 Root Bridge로 선출된다. 이 스위치는 네트워크에서 가장 낮은 BID 값을 가지게 된다.</li>
</ol>
</li>
<li><strong>정기적인 BPDU 전송</strong><ol>
<li>Root Bridge는 정기적으로 BPDU를 전송하여 자신이 Root Bridge임을 네트워크 내의 다른 스위치들에게 알리고, 이를 통해 네트워크 구성의 변화에 따라 Root Bridge의 선출이 다시 이루어질 수 있다.</li>
</ol>
</li>
</ol>
<p><strong>최적 경로를 계산하는 방법</strong>은 다음과 같다.</p>
<ol>
<li><strong>경로 비용의 설정</strong><ol>
<li>각 포트는 특정 비용 값을 가지고, 비용은 전송 속도, 링크의 품질 등에 기반하여 정해진다. 일반적으로 빠른 링크는 낮은 비용을 가지며, 느린 링크는 높은 비용을 가진다.</li>
</ol>
</li>
<li><strong>BPDU의 수신과 업데이트</strong><ol>
<li>스위치는 Root Bridge로부터 BPDU를 수신할 때마다 BPDU에 포함된 경로 비용을 업데이트한다. 스위치는 자신의 포트 비용을 BPDU의 경로 비용에 더하고, 이 업데이트된 비용 정보를 다시 BPDU에 포함시켜 다음 스위치로 전달한다.</li>
</ol>
</li>
<li><strong>최적 경로의 선정</strong><ol>
<li>스위치는 다양한 경로를 통해 여러 BPDU를 수신할 수 있다. 스위치는 수신된 BPDU들 중에서 가장 낮은 경로 비용을 가진 BPDU를 선택하고, 해당 BPDU를 통해 받은 정보를 기반으로 Root Bridge까지의 최적 경로를 선정한다.</li>
</ol>
</li>
<li><strong>블로킹 상태의 설정</strong><ol>
<li>스위치는 최적 경로를 제외한 다른 경로의 포트를 블로킹 상태로 설정한다. 이렇게 하면 해당 포트를 통한 데이터 전송이 차단되어 Looping을 방지하게 된다.</li>
</ol>
</li>
<li><strong>정기적인 BPDU 전송과 경로 비용의 업데이트</strong><ol>
<li>네트워크의 구성이나 링크의 상태가 변할 때마다 최적 경로도 변할 수 있다. 따라서 스위치는 정기적으로 BPDU를 전송하고 수신하여 경로 비용을 업데이트하고, 필요한 경우 최적 경로를 재선정한다.</li>
</ol>
</li>
</ol>
<p>이후 Broadcast Storm을 방지하기 위해서 Block State로 들어가게 되는데, 다음은 각 스위치의 State에 대해 설명한다.</p>
<ol>
<li><strong>Blocking State (블로킹 상태)</strong>:<ul>
<li>BPDU(Bridge Protocol Data Units) 프레임은 수신 가능하지만, 일반 데이터 트래픽에 대해서는 차단한다.</li>
<li>네트워크에서 루핑을 방지하기 위해, 최적 경로가 아닌 다른 경로의 포트는 이 상태로 설정된다.</li>
</ul>
</li>
<li><strong>Listening State (리스닝 상태)</strong>:<ul>
<li>BPDUs만 수신하고, 일반 데이터 프레임은 전송 및 수신하지 않는다.</li>
<li>네트워크의 변화를 모니터링하며, 최적 경로가 변경되었는지 확인한다.</li>
</ul>
</li>
<li><strong>Learning State (러닝 상태)</strong>:<ul>
<li>스위치는 이 상태에서 MAC Address Table을 학습한다.</li>
<li>일반 데이터 프레임은 여전히 차단되지만, 스위치는 연결된 장치들의 MAC 주소를 MAC 주소 테이블에 추가할 수 있다.</li>
<li>이 상태는 15초(전송 지연 타이머) 후에 Forwarding 상태로 전환됩니다.</li>
</ul>
</li>
<li><strong>Forwarding State (전송 상태)</strong>:<ul>
<li>Data Frame을 정상적으로 송/수신 한다.</li>
</ul>
</li>
</ol>
<h3 id="switch와-router의-차이점">Switch와 Router의 차이점</h3>
<p>일단 계층별로 다른 데이터를 다룬 점에서는 당연하게 다르다.
둘 다 데이터를 특정한 목적지로 이동시킨다는 점에서는 동일하다.
하지만 목적지가 불분명한 경우(테이블에 없는 경우), 스위치는 브로드캐스트를 통해 주변 노드에게 퍼뜨리지만 라우터는 해당 값을 버린다. </p>
<h3 id="reference">reference</h3>
<p>스위치란 무엇인가 : <a href="https://www.cloudflare.com/ko-kr/learning/network-layer/what-is-a-network-switch/">https://www.cloudflare.com/ko-kr/learning/network-layer/what-is-a-network-switch/</a>
STP란 : <a href="https://net-study.club/entry/%EC%8A%A4%ED%8C%A8%EB%8B%9D-%ED%8A%B8%EB%A6%AC-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9CSTP-Spanning-Tree-Protocol">https://net-study.club/entry/%EC%8A%A4%ED%8C%A8%EB%8B%9D-%ED%8A%B8%EB%A6%AC-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9CSTP-Spanning-Tree-Protocol</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Flutter로 업데이트 없이 다채로운 이벤트 구축하기 (2) (feat. RFW)]]></title>
            <link>https://velog.io/@halfmoon_mind/Flutter-Flutter%EB%A1%9C-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%97%86%EC%9D%B4-%EB%8B%A4%EC%B1%84%EB%A1%9C%EC%9A%B4-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-2-feat.-RFW</link>
            <guid>https://velog.io/@halfmoon_mind/Flutter-Flutter%EB%A1%9C-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%97%86%EC%9D%B4-%EB%8B%A4%EC%B1%84%EB%A1%9C%EC%9A%B4-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-2-feat.-RFW</guid>
            <pubDate>Tue, 02 Apr 2024 12:54:31 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@halfmoon_mind/Flutter%EB%A1%9C-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%97%86%EC%9D%B4-%EB%8B%A4%EC%B1%84%EB%A1%9C%EC%9A%B4-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-1">1편</a>에서 이어집니다.</p>
<h1 id="서론">서론</h1>
<p>이벤트 페이지를 구축하는 방법에 대해서는 간단하게 저번 글에서 요약해보았습니다.
그렇다면 웹뷰와 observer로만 작업할 수 있을까요?</p>
<h1 id="rfw">RFW</h1>
<p><a href="https://pub.dev/packages/rfw">RFW</a>(Remote Flutter Widget) 패키지는 다음과 같은 설명을 하고 있습니다.</p>
<pre><code>a library for rendering declarative widget description files at runtime.</code></pre><p>해석을 해보자면 <code>런타임에 선언적 위젯 설명 파일을 렌더링하기 위한 라이브러리입니다</code> 라는 뜻인데요.
즉 동적으로 랜더링되는 위젯을 런타임에 뷰에 추가할 수 있다는 뜻입니다.</p>
<h1 id="계기">계기</h1>
<p>사실 이 패키지를 알게된 계기는 앱에서 <a href="https://learn.microsoft.com/ko-kr/appcenter/distribution/codepush/">코드푸시</a>와 같은 기능을 찾아보다가 발견하였습니다. ㅎㅎ
Flutter는 매년 다음년도에 어떤 작업을 중점적으로 할 것이고, 하지 않은 것인지에 대해서 발표를 하는데요, <a href="https://github.com/flutter/flutter/wiki/Roadmap">Roadmap</a>에서 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/halfmoon_mind/post/f8b8a1a8-2ca8-426d-be76-2069deb51ee3/image.png" alt="">
여기서 <code>Non-goals</code>라는 섹션에서는 다음 년도에 작업하지 않을 것을 얘기해주는데, 확인해보니 code push는 지원하지 않는다(...)라고 얘기를 하였는데 다른 패키지들은 존재하는 것을 보고 이건 뭘까 하면서 호기심을 가졌습니다.
shorebird는 code push와 동일한 기능을 지원하지만, 서드파티 패키지인 것과 아직 iOS가 베타 릴리즈(라고 얘기하면서 확인해보니 정식 릴리즈가 되었다...!)여서 도입이 꺼려졌었는데, rfw는 무엇일까 하고 궁금하였습니다.</p>
<h1 id="한계">한계</h1>
<p>말만 들으면 모든 뷰를 동적으로 그려줄 수 있다면 앱 내에 코드가 아예 없어도 될 것처럼 보여지는데, 그것보다는 일부 UI코드를 서버에서 내려줄 수 있다고 생각하면 편하다. 모든 UI나 비즈니스 로직을 업데이트 하는 것은 어려울 수 있다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/halfmoon_mind/post/2efbcd82-6e26-4ed8-bdf8-77a5f44bbc72/image.png" alt="">
공식문서에도 한계점이 나와있다...</p>
</blockquote>
<h1 id="사용법">사용법</h1>
<p>먼저 패키지를 pubspec.yaml에 추가한다.
예제 코드는 다음 링크에서 확인할 수 있다. (<a href="https://github.com/flutter/packages/tree/main/packages/rfw/example">Github</a>)</p>
<p>먼저 각 속성에 들어갈 수 있는 정보는 String 혹은 num 타입이다.
예시 코드를 보자.</p>
<pre><code class="language-dart">import core.widgets;
import core.material;

widget Counter = Scaffold(
  appBar: AppBar(title: Text(text: &quot;Counter Demo&quot;)),
  body: Center(
    child: Column(
      mainAxisAlignment: &quot;center&quot;,
      children: [
        Text(text: &#39;You have pushed the button this many times:&#39;, textAlign: &quot;center&quot;),
        Text(text: data.counter, style: {
          fontSize: 36.0,
        }),
      ],
    ),
  ),
  floatingActionButton: FloatingActionButton(
    onPressed: event &quot;increment&quot; { },
    tooltip: &quot;Increment&quot;,
    child: Icon(icon: 0xE047, fontFamily: &quot;MaterialIcons&quot;),
  ),
);</code></pre>
<p>또한 해당 뷰가 들어갈 실제 앱에서 사용하고 있는 정보(<code>data.counter</code>)도 볼 수 있다.
혹은 앱 영역에 데이터를 전송할 event인 <code>increment</code> 도 볼 수 있다.</p>
<p>다른 예제 코드를 보자.</p>
<pre><code class="language-dart">import core.widgets;
import core.material;

widget Counter = Container(
  color: 0xFF66AACC,
  child: Center(
    child: Button(
      child: Padding(
        padding: [ 20.0 ],
        child: Text(text: data.counter, style: {
          fontSize: 56.0,
          color: 0xFF000000,
        }),
      ),
      onPressed: event &#39;increment&#39; { },
    ),
  ),
);

widget Button { down: false } = GestureDetector(
  onTap: args.onPressed,
  onTapDown: set state.down = true,
  onTapUp: set state.down = false,
  onTapCancel: set state.down = false,
  child: Container(
    duration: 50,
    margin: switch state.down {
      false: [ 0.0, 0.0, 2.0, 2.0 ],
      true: [ 2.0, 2.0, 0.0, 0.0 ],
    },
    padding: [ 12.0, 8.0 ],
    decoration: {
      type: &quot;shape&quot;,
      shape: {
        type: &quot;stadium&quot;,
        side: { width: 1.0 },
      },
      gradient: {
        type: &quot;linear&quot;,
        begin: { x: -0.5, y: -0.25 },
        end: { x: 0.0, y: 0.5 },
        colors: [ 0xFFFFFF99, 0xFFEEDD00 ],
        stops: [ 0.0, 1.0 ],
        tileMode: &quot;mirror&quot;,
      },
      shadows: switch state.down {
        false: [ { blurRadius: 4.0, spreadRadius: 0.5, offset: { x: 1.0, y: 1.0, } } ],
        default: [],
      },
    },
    child: DefaultTextStyle(
      style: {
        color: 0xFF000000,
        fontSize: 32.0,
      },
      child: args.child,
    ),
  ),
);</code></pre>
<p>조금 더 복잡한 뷰인데, 실제 flutter에서 사용하는 속성 중, material widget의 기능 대부분을 지원하는 것으로 보인다.</p>
<p>다른 예제 코드를 보자.</p>
<pre><code class="language-dart">static WidgetLibrary _createLocalWidgets() {
    return LocalWidgetLibrary(&lt;String, LocalWidgetBuilder&gt;{
      &#39;GreenBox&#39;: (BuildContext context, DataSource source) {
        return ColoredBox(
          color: const Color(0xFF002211),
          child: source.child(&lt;Object&gt;[&#39;child&#39;]),
        );
      },
      &#39;Hello&#39;: (BuildContext context, DataSource source) {
        return Center(
          child: Text(
            &#39;Hello, ${source.v&lt;String&gt;(&lt;Object&gt;[&quot;name&quot;])}!&#39;,
            textDirection: TextDirection.ltr,
          ),
        );
      },
    });
  }

  // ...

void _update() {
    _runtime.update(localName, _createLocalWidgets());
    _runtime.update(remoteName, parseLibraryFile(&#39;&#39;&#39;
      import local;
      widget root = GreenBox(
        child: Hello(name: &quot;World&quot;),
      );
    &#39;&#39;&#39;));
  }</code></pre>
<p>해당 코드는 로컬에 정의된 widget을 보여준다.
여기서 _runtime.update 안에 file 정보만 서버에서 지정해주면, 로컬에 선언된 원하는 widget에 대해서 지정해줄 수 있을 것이다.</p>
<h1 id="내가-만든-rfw뷰">내가 만든 RFW뷰</h1>
<p>먼저 rfw에 원하는 뷰를 선언해준다.</p>
<pre><code class="language-dart">import core.widgets;
import core.material;

widget Counter = GestureDetector(
    child: Container(
        height: 200,
        color: 0xFF002211,
        child: Column( 
            mainAxisAlignment: &quot;center&quot;,
            crossAxisAlignment: &quot;center&quot;,
            children: [Text(text: [&quot;Hello, &quot;, data.greet.name, &quot;!&quot;], textDirection: &quot;ltr&quot;),],
        ),
    ),
    onTap: event &quot;greeting&quot; { data: &quot;GO&quot; },
);</code></pre>
<p><code>Counter</code>라는 변수에 GestureDector로 클릭했을 때, greeting이라는 이벤트에 &quot;GO&quot;라는 정보를 전달한다. 또한 greet.name이라는 변수 값도 client 사이드에서 받는다.</p>
<p>client(앱) 사이드에서는 다음과 같이 선언한다.</p>
<pre><code class="language-dart">// https://github.com/halfmoon-mind/remote-flutter-widget/raw/main/lib/remote/counter_app1.rfw
// https://github.com/halfmoon-mind/remote-flutter-widget/raw/main/lib/remote/counter_app2.rfw

import &#39;dart:io&#39;;

import &#39;package:flutter/material.dart&#39;;
import &#39;package:fluttertoast/fluttertoast.dart&#39;;
import &#39;package:path/path.dart&#39;;
import &#39;package:path_provider/path_provider.dart&#39;;
import &#39;package:rfw/rfw.dart&#39;;

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const RemoveView(),
    ),
  );
}

class RemoveView extends StatefulWidget {
  const RemoveView({super.key});

  @override
  State&lt;RemoveView&gt; createState() =&gt; _RemoveViewState();
}

class _RemoveViewState extends State&lt;RemoveView&gt; {
  final Runtime _runtime = Runtime();
  final DynamicContent _data = DynamicContent();

  int _counter = 0;
  bool _ready = false;

  @override
  void initState() {
    super.initState();
    _runtime.update(
        const LibraryName(&lt;String&gt;[&#39;core&#39;, &#39;widgets&#39;]), createCoreWidgets());
    _runtime.update(const LibraryName(&lt;String&gt;[&#39;core&#39;, &#39;material&#39;]),
        createMaterialWidgets());

    _updateData();
    _updateWidgets();
  }

  void _updateData() {
    _data.update(&#39;counter&#39;, _counter.toString());
  }

  void _updateWidgets() async {
    final Directory home = await getApplicationSupportDirectory();
    const baseUrl =
        &quot;https://github.com/halfmoon-mind/remote-flutter-widget/raw/main/lib/remote/&quot;;
    const firstFileName = &quot;counter_app1.rfw&quot;;
    const secondFileName = &quot;counter_app2.rfw&quot;;

    String targetFileName = firstFileName;

    // 항상 새로운 값으로 업데이트
    File targetFile = File(join(home.path, firstFileName));
    if (targetFile.existsSync()) {
      targetFile.deleteSync();
      targetFileName = secondFileName;
      targetFile = File(join(home.path, secondFileName));
    }
    final client =
        await (await HttpClient().getUrl(Uri.parse(&#39;$baseUrl$targetFileName&#39;)))
            .close();
    await targetFile
        .writeAsBytes(await client.expand((element) =&gt; element).toList());
    _runtime.update(const LibraryName(&lt;String&gt;[&#39;main&#39;]),
        decodeLibraryBlob(await targetFile.readAsBytes()));
    setState(() {
      _ready = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (!_ready) {
      return const Material(
        child: Scaffold(
          body: Center(
            child: Text(
              &quot;NOT READY&quot;,
              style: TextStyle(fontSize: 40),
            ),
          ),
        ),
      );
    }
    return RemoteWidget(
      runtime: _runtime,
      widget: const FullyQualifiedWidgetName(
        LibraryName(&lt;String&gt;[&#39;main&#39;]),
        &#39;Counter&#39;,
      ),
      data: _data,
      onEvent: (eventName, eventArguments) {
        if (eventName == &#39;increment&#39;) {
          setState(() {
            _counter++;
            _updateData();
          });
        }
        if (eventName == &quot;greeting&quot;) {
          Fluttertoast.showToast(msg: &quot;Hello, ${eventArguments[&#39;data&#39;]}&quot;);
        }
      },
    );
  }
}</code></pre>
<p>해당 코드는 계속 다른 화면을 보여주기 위해서 1번과 2번을 번갈아서 저장하였다.
이 코드에서 flutter 영역의 데이터를 rfw 측에 넘겨주기, rfw 측의 정보를 flutter에 event의 형태로 전달하기, 동적으로 화면 그리는 것을 실제로 보여주기와 같은 것을 보여준다.</p>
<h1 id="후기">후기</h1>
<p>먼저 <strong>실제 플러터와의 문법이 조금 달라</strong>서 어려웠다.
이게 실제로 동작하는지는 rfw의 파서가 정상적으로 해당 코드를 분석할 수 있으면 가능한데, 문법이 약간 다르다보니 해당 값에 어떻게 넣어야하는지 감이 잘 안왔다.</p>
<pre><code class="language-dart">// 예시
import core.widgets;
import core.material;
widget Counter = Scaffold(
  appBar: AppBar(title: Text(text: &quot;Counter Demo&quot;)),
  body: Center(
    child: Column(
      mainAxisAlignment: &quot;center&quot;,
      children: [
        Text(text: &#39;You have pushed the button this many times:&#39;, textAlign: &quot;center&quot;),
        Text(text: data.counter, style: {
          fontSize: 36.0,
        }),
      ],
    ),
  ),
  floatingActionButton: FloatingActionButton(
    onPressed: event &quot;increment&quot; { },
    tooltip: &quot;Increment&quot;,
    child: Icon(icon: 0xE047, fontFamily: &quot;MaterialIcons&quot;),
  ),
);</code></pre>
<p>예제 코드를 보면 우리가 생각하는 Scaffold에 들어갈 수 있는 값이 약간씩 다르다. 기존에는 <code>MainAxisAlignment.center</code>와 같은 형태가 들어갔지만, rfw에서는 String의 형태로 입력한다던지...</p>
<p>이러한 불편함을 제외하고는 굉장히 획기적이었다.
데이터 로딩 중에 임시 뷰를 미리 그려줄 수도 있는 부분과 함께 무엇보다 <strong>동적으로 데이터를 내려줄 수 있다</strong>는 장점이 있는듯 했다. 필요하다면 서버에서 직접 코드를 가공해서 내려줄 수도 있을 것이다.</p>
<p>단점으로 하나 꼽히는건 <strong>runner로 코드를 컴파일해줘야</strong>한다는 것이다.
<a href="https://github.com/flutter/packages/blob/main/packages/rfw/example/remote/remote_widget_libraries/encode.dart">Github</a>를 보면 encode하는 방식이 나와있는데, 완전히 동적으로 가능하다는건 아닌 것 같아서 아쉬웠다.</p>
<p>이것과 관련해서 학교에서 발표를 하였는데 질문에서 <strong>RFW의 성능이 flutter에서 dart 코드로 작성한 것과 동일한 성능을 유지할 수 있는가</strong>에 대해서 질문을 받았는데, 대답을 못했다. 이건 한 번 찾아보고 고민해보면 좋을 것 같다.</p>
<p>해당 패키지를 찾아보다보니, 관련된 글이 많지 않아서 해당 패키지에 관심이 있는 사람에게 도움이 되었으면 좋겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Flutter로 업데이트 없이 다채로운 이벤트 구축하기 (1)]]></title>
            <link>https://velog.io/@halfmoon_mind/Flutter%EB%A1%9C-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%97%86%EC%9D%B4-%EB%8B%A4%EC%B1%84%EB%A1%9C%EC%9A%B4-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@halfmoon_mind/Flutter%EB%A1%9C-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%97%86%EC%9D%B4-%EB%8B%A4%EC%B1%84%EB%A1%9C%EC%9A%B4-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-1</guid>
            <pubDate>Tue, 02 Apr 2024 12:00:30 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요
현재 테일크루라는 회사에서 Flutter Engineer로 웹소설 플랫폼 &quot;투댓&quot;이라는 서비스를 만들고 있는 심상현입니다.</p>
<blockquote>
<p><em>숭실대학교 Developer Conference, DEVCON에서 발표한 이야기를 요약한 내용입니다</em></p>
</blockquote>
<h1 id="문제-상황">문제 상황</h1>
<p>현재 저희 회사에는 웹소설 플랫폼을 만들고 있는데요, 해당 서비스를 만들다보니 마주쳤던 문제를 소개하려고 합니다.
저희는 새로운 기능으로 이벤트 페이지를 구축하려고 하였습니다. 이를 통해서 유저들에게 어떤 이벤트가 있는지 바로 알려줄 수 있도록 하려고 했었습니다.
여기서 이벤트 페이지를 구현하는 방식에는 여러가지 방식이 있을 것 같은데요, 다음과 같은 선택지가 있을 것입니다.</p>
<h3 id="선택1-웹뷰로-만들자">선택1. 웹뷰로 만들자.</h3>
<p>처음에는 당연하게 웹뷰로 만들려고 했습니다.
해당 화면은 지속적으로 변화할 수 있고, 변경될 여지가 굉장히 클 것이기 때문입니다.
하지만 문제가 하나 있었습니다.
이벤트 페이지 내에서 동적인 인터렉션이 들어갈 수 있었던 것입니다.</p>
<blockquote>
</blockquote>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/halfmoon_mind/post/ead3c762-c339-4dad-ad96-cba9e916ef37/image.png"/></th>
<th><img src="https://velog.velcdn.com/images/halfmoon_mind/post/02010652-8145-4ad5-ab49-4d43fed61b41/image.png"/></th>
</tr>
</thead>
<tbody><tr>
<td>왼쪽에 사진을 누르면 오른쪽 소설 상세페이지로 이동해야한다.</td>
<td></td>
</tr>
</tbody></table>
<p>소설로 이동하는 것 뿐만 아니라, API 호출이라던가, 앱 내부에 있는 dialog가 뜬다거나, 다양한 인터렉션이 발생해야하였습니다.
이러한 상황에서 웹뷰만 사용하기에는 웹뷰 영역에서 Flutter내의 코드를 어떻게 사용할 수 있는지에 대해서 고민을 하였습니다.</p>
<h3 id="선택2-앱-내의-페이지로-만들자-flutter내-dart-코드로-만들어서-배포하기">선택2. 앱 내의 페이지로 만들자. (Flutter내 Dart 코드로 만들어서 배포하기)</h3>
<p>그렇다면 앱 내의 화면으로 구현하면 어떨까로 생각해보았습니다.
이러한 경우에는 앱 내에서 모든 작업이 이루어지다보니, 아까전에 말했던 앱 내에 있는 코드를 실행(dialog 띄우기, 페이지 이동 등)하는 것은 쉬웠지만, 앱 배포마다 새로운 업데이트가 강제적으로 필요했습니다.</p>
<h1 id="해결방안">해결방안</h1>
<p>결국 저는 고심 끝에 Method Channel과 WebView를 합쳐서 해결하였습니다.
Method Channel이란 Native언어의 코드를 실행하거나, Native 영역에서 Flutter 영역으로 데이터를 전달하거나 함수를 호출할 수 있는 기술을 말합니다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/halfmoon_mind/post/5757ff5a-7e64-4668-b31c-5800516a7803/image.png" alt="https://docs.flutter.dev/platform-integration/platform-channels">
Flutter의 Method Channel, Android와 iOS Native 코드를 실행하거나 Native 영역에서 Flutter의 코드를 실행할 수 있다.</p>
</blockquote>
<p>또한 웹뷰를 사용하면 앱스토어 / 플레이스토어를 거치지 않고도 앱 내의 콘텐츠를 업데이트할 수 있습니다. 웹만 다시 배포하면 되니깐요!</p>
<p>그래서 다음과 같은 구조가 탄생하였습니다.
<img src="https://velog.velcdn.com/images/halfmoon_mind/post/e19be1b4-5ead-463c-9561-2fe1b54166af/image.png" alt="">
코드로 한번 확인해볼까요? <strong><em>모든 코드는 <a href="https://github.com/halfmoon-mind/remote-flutter-widget/blob/main/lib/webview/webview.dart">Github</a>에 올라가 있습니다</em></strong>
먼저 HTML 코드를 확인해보겠습니다.
<strong>위치 : assets/test.html</strong></p>
<pre><code class="language-HTML">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
  &lt;body&gt;
    &lt;script&gt;
      // dialog라는 이벤트에 첫번째 인자로 &quot;EVENT_TITLE_HELLO&quot; 데이터 전달
        function dialog() {
            window.flutter_inappwebview.callHandler(&#39;dialog&#39;, &#39;EVENT_TITLE_HELLO&#39;);
        }
    &lt;/script&gt;
    &lt;h1 style=&quot;color: black&quot; onclick=&quot;dialog()&quot; &gt;ON TAP DIALOG EVENT!&lt;/p&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>dialog라는 함수 안에 flutter 영역으로 dialog라는 이벤트에 &quot;EVENT_TITLE_HELLO&quot;라는 데이터를 첫 번째 인자로 전달하는 것을 알 수 있습니다.</p>
<p><strong>위치 : lib/webview/webview.dart</strong>
주의 : 현재 코드에서는 <code>flutter_inappwebview: ^5.8.0</code> 버전을 사용하고 있습니다. 패키지 버전 업데이트 따라서 해당 코드의 인자나 값이 정상적으로 실행되지 않을 수도 있습니다.</p>
<pre><code class="language-dart">  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: InAppWebView(
        initialFile: &#39;/assets/test.html&#39;,
        initialOptions: InAppWebViewGroupOptions(
          crossPlatform: InAppWebViewOptions(
            javaScriptEnabled: true,
            useShouldOverrideUrlLoading: true,
            mediaPlaybackRequiresUserGesture: false,
            useOnLoadResource: true,
            allowFileAccessFromFileURLs: true,
            allowUniversalAccessFromFileURLs: true,
            transparentBackground: true,
          ),
        ),
        onLoadStop: (controller, url) {
          webViewController = controller;
          addDialogEvent(context);
        },
      ),
    );
  }
}</code></pre>
<p>우리가 눈여겨볼 곳은 addDialogEvent인데요, WebView가 로딩이 완료된 시점에 addDialogEvent함수를 통해서 dialog 실행에 대한 observer를 추가해줍니다.</p>
<pre><code class="language-dart">void addDialogEvent(BuildContext context) {
    webViewController?.addJavaScriptHandler(
    // event handler 이름 등록
      handlerName: &quot;dialog&quot;,
      callback: (args) {
        showDialog(
          context: context,
          builder: (context) =&gt; AlertDialog(
            title: const Text(&quot;Dialog&quot;),
            // argument 사용
            content: Text(args[0]),
            actions: [
              TextButton(
                onPressed: () {
                  Navigator.of(context).pop();
                  Navigator.of(context).push(
                    MaterialPageRoute(
                      builder: (context) =&gt; const LocalView(),
                    ),
                  );
                },
                child: const Text(&quot;OK&quot;),
              ),
            ],
          ),
        );
      },
    );
  }</code></pre>
<p>이러한 방식을 사용한다면, dialog라는 이벤트에 대해서 argument로 받는 데이터를 flutter 앱 내에 있는 코드에서 활용할 수 있을 것입니다.
또한 이러한 방식을 채택한다면 앱 내에서 동작해야하는 것들에 대해서 미리 인터페이스를 선언해주고, 웹 영역에서 해당 event 이름으로 호출하면 플러터 영역에서 이루어지는 형태로 작업하면 쉽게 작업할 수 있습니다. (앱 내에서 observing 해야하는 event가 생각보다 많지 않아 구현하기 용이합니다.)</p>
<h1 id="마무리">마무리</h1>
<p>저는 제가 생각한 방법이 최선의 방법이라고 생각하지 않는데요. 이벤트 페이지 구축에 다양한 방식이 있지만, 제가 이 문제를 해결할 당시 다양한 방법을 고려해보았을 때, 관련된 best practice가 없던 것으로 알고 있었고 빠르게 구현하고 유지보수하기 쉽다고 생각이 들었기 때문입니다.
해당 부분을 구현하기 위해서는 백엔드 및 웹 개발하는 사람과의 협업이 필요합니다. Flutter 영역에서 호출할 인터페이스와 Backend에서 어떤 데이터를 내려줄지도 고민이 필요합니다.
혹시라도 문제점 혹은 조언해주실 부분 있으시면 댓글로 얼마든지 달아주셔도 좋습니다.</p>
<h4 id="발표-자료도-공유해드려요-ㅎㅎ"><a href="https://docs.google.com/presentation/d/1h1k59DlG17EkKn8x8BKJWyMSrG0xvtWC4TrrjrclaJg/edit?usp=sharing">발표 자료</a>도 공유해드려요 ㅎㅎ</h4>
<p>감사합니다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DevFest Cloud 2023] 프론트엔드 엔지니어의 DevFest 후기]]></title>
            <link>https://velog.io/@halfmoon_mind/DevFest-Cloud-2023-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EC%9D%98-DevFest-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@halfmoon_mind/DevFest-Cloud-2023-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EC%9D%98-DevFest-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sun, 17 Dec 2023 04:57:58 GMT</pubDate>
            <description><![CDATA[<p>나는 지금 작은 스타트업에서 flutter engineer 직무로 앱 개발을 하고 있다.
개발을 하다보면 느끼는 것은 개발 도메인에 1개만 알고 할 수 있는게 아니라 다양한 방면에 대해서 알고 있어야 한다는 것이다.</p>
<p>하다보니 재미도 없고 다른 직무에서는 어떤 일을 하고 어떤 기술을 사용해서 다른 사람과 협업하는지 궁금했다.</p>
<p>그러던 중, devfest cloud라는 이벤트가 열리는 것을 확인했다.</p>
<blockquote>
<p><a href="https://festa.io/events/4385">https://festa.io/events/4385</a>
<img src="https://cf.festa.io/img/2023-11-21/903139bc-4bbc-4576-a5c5-d382041b6b3c.png" alt=""></p>
</blockquote>
<p>클라우드라는 분야는 사실 나와는 크게 상관 없는 분야이긴 하다. 백엔드 엔지니어라면 요즘 Cloud Native로 전환하는 회사도 많고 관심도 많겠지만..?</p>
<p>그래도 동아리에서 봤던 선배님들도 오랜만에 보고, 주말에 할 것도 없어서 보러 갔다.
위치는 연세대 공학원이었고, 신촌에서 하는지라 점심에 신촌에 가서 맛있는 것도 먹었다.</p>
<blockquote>
<p>맛있는 규카츠를 먹었다...!
<img src="https://velog.velcdn.com/images/halfmoon_mind/post/7cfa6469-7076-42b1-8a15-c4e22ef95d6a/image.jpeg" width="300"></p>
</blockquote>
<img src="https://velog.velcdn.com/images/halfmoon_mind/post/fb28096e-1f20-44b5-bd2b-3399cce5104b/image.jpeg" width="300">

<p>연세대를 가다보니, 옛날에 신촌에서 자취했던 기억이 새록새록 났다. 연대가 밤에 산책하기 정말 좋았어서 자주 왔었는데...</p>
<p>처음에 들어와서는 체크인? 등록?을 하고 지하 1층에서 세션 시작을 했다.
GDG Cloud의 우수연님(우리 학교 선배님 ㅎㅎ)의 오프닝 멘트를 듣고, Google의 DevRel Kristine song님의 말씀을 들었다.</p>
<img src="https://velog.velcdn.com/images/halfmoon_mind/post/0aecdf83-d63b-47d5-b000-5e9b512eb9de/image.jpeg" width="300">

<p>그리고 다양한 연사분들의 말씀을 듣고, 당근마켓의 SRE팀 OutSider님(<a href="https://blog.outsider.ne.kr/)%EA%B3%BC">https://blog.outsider.ne.kr/)과</a> 커피챗도 진행했다...!
SRE이 어떤 직무이고 어떤 문제를 해결하려는 조직이라는 설명을 들으니 너무 인상 깊었다.
(yml 상하차라는 말을 듣고는 약간 고민이 되었지만...ㅋㅋㅋㅋ)</p>
<p>마지막에 컨퍼런스 참가한 사람들과 다양한 얘기를 하면서 어떤 일을 하고 관심이 있구나를 알 수 있었다.</p>
<h2 id="후기">후기</h2>
<p>사실 이러한 컨퍼런스에 참석하는 경우, 대단한 것을 얻어가려고 하지 않는다.
컨퍼런스 자체는 퀄리티가 높지 않은 경우가 많고, 깊게 들어가기 위해서는 너무 짧은 시간이기 때문이다.
이번 컨퍼런스에서 얻은 것은 Cloud 서비스에도 많은 사람들이 흥미를 가지고 혁신을 시도하고 있구나라는 것을 깨달았다.
생각보다 서버에만 관심있는 사람 뿐만 아니라, 나처럼 FrontEnd Engineer나 다른 직군의 사람들도 많이 관심을 찾고 참가한 것이 신기했다.</p>
<blockquote>
<p><strong><em>너무나 슬픈 얘기지만, 맞는 말이라 더 슬프다...</em></strong>
<img src="https://velog.velcdn.com/images/halfmoon_mind/post/91c71447-c06c-4f4c-9e27-fc08c0198faf/image.jpeg" alt=""></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Making Todo List App with Clean Architecture and GetX]]></title>
            <link>https://velog.io/@halfmoon_mind/Flutter-Making-Todo-List-App-with-Clean-Architecture-and-GetX</link>
            <guid>https://velog.io/@halfmoon_mind/Flutter-Making-Todo-List-App-with-Clean-Architecture-and-GetX</guid>
            <pubDate>Thu, 14 Sep 2023 08:22:58 GMT</pubDate>
            <description><![CDATA[<h1 id="시작하며">시작하며</h1>
<p>요즘 앱 개발을 하면 여러가지 아키텍쳐, 디자인 패턴을 사용하게 된다.
결국 서비스를 유지보수 / 코드 가독성 측면을 높이려고 사용하는 방법이라고 생각된다.</p>
<p>하지만 개인적인 생각으로 앱 서비스 개발을 처음 배우는 단계에서 아키텍쳐에 대해서 배우는 것에 대해서는 별로 추천하지 않는다.
내가 flutter를 처음 배웠던 시점에서는 모든 Widget을 Screen에 모두 때려박은 다음, Model 정도만 분리하고 작업했던 것으로 기억한다. 그 시점에서는 뷰를 구현하는 것마저도 쉽지 않았고, 디자인 패턴은 그런 뷰를 만드는 것이 익숙해진 이후에 필요해진 것 같다.
지금에 이르러서는 아키텍쳐에 대한 지식이 필요한 부분이라고 생각해서 공부하고 있는데, 돌이켜보면 지금까지 만들어본 대부분의 토이 프로젝트에서는 크게 필요 없어 보이긴 한다.
따라서 이 글은 처음 flutter를 배우는 사람 보다는 어느 정도 만들어본 경험이 있고, 여러가지 아키텍쳐를 구현하는 방식에 대해서 고민해본 사람에게 추천한다.</p>
<h1 id="서비스-구현하기">서비스 구현하기</h1>
<p>사실 이 글을 쓰려고 클린 아키텍쳐를 찾아보고 작성한 것은 아니고, 회사 입사 과제로 작성한 부분이다.
코드를 작성하면서 들었던 생각들을 공유하고 싶어서 작성한 것이니, 틀린 부분이 있을 수도 있고 불완전한 부분이 있을 수도 있다.</p>
<h2 id="getx">GetX</h2>
<p><a href="https://pub.dev/packages/get">GetX</a>은 flutter에 존재하는 상태관리 라이브러리 중 하나이다.
이것 외로도 Provider, Riverpod, Bloc 과 같은 라이브러리가 존재한다.
그 중 나는 GetX를 활용해서 todo list를 만들어 볼 것이다.</p>
<p>flutter의 여러 커뮤니티에서 항상 나오는 얘기가 GetX에 대해서 별로다 / 정말 좋다 라는 의견이 충돌하는 것으로 보인다.
내가 서비스를 만들어 보면서 느꼈던 부분은 GetX 자체는 괜찮아 보인다.
Observer 패턴을 잘 적용하고 있는 라이브러리라고 생각하지만, 단점은 상태 관리 라이브러리 하나치고는 너무 방대한 부분(라우팅, 모달, DI 등)을 적용할 수 있고, 결국 서비스 자체가 하나의 패키지에 종속되어버리는 부분이 좋지 않다고 생각한다.
또한 flutter에서 잘 사용하라고 만든 context를 무시하고 독립적으로 사용할 수 있도록 만들 수 있어, 좋지 않은 코딩 습관을 기를 수 있다는 단점이 생길 수도 있다. (내가 현재 GetX로 flutter 입문을 하고 다시 돌아가려하니 너무 어려운 경험에서 시작했다)</p>
<h2 id="clean-architecture">Clean Architecture</h2>
<p>flutter의 상태관리 라이브러리들은 각각 결합하면 좋은 디자인 패턴이 있는 것으로 보인다.
내가 생각하기에는 GetX는 다른 디자인 패턴이 크게 필요하지는 않아 보이고, 클린 아키텍쳐 정도만 도입해도 괜찮아 보여서 적용해보았다.
<img src="https://velog.velcdn.com/images/halfmoon_mind/post/617cb8a5-693e-40a9-acd0-5ff82d19b4ae/image.png" alt="">
Clean Architecture란 각 레이어 별로 로직을 나누어서 관심사를 분리하고, 코드를 유지보수하거나 가독성을 높이는 아키텍쳐라고 생각하면 좋을 것 같다.
예를 들어 실제 데이터를 만나는 (로컬DB나 api 호출 등) 경우는 Entity, 그것을 감싸는 UseCase들. Usecase를 활용하는 Presenter들로 구성되게 된다.</p>
<h3 id="폴더-구조">폴더 구조</h3>
<pre><code>lib
 ┣ data
 ┃ ┣ datasources
 ┃ ┃ ┗ todo_local_datasource.dart
 ┃ ┗ repositories
 ┃ ┃ ┗ todo_repository_impl.dart
 ┣ domain
 ┃ ┣ entities
 ┃ ┃ ┣ todo_entity.dart
 ┃ ┃ ┗ todo_entity.g.dart
 ┃ ┣ repositories
 ┃ ┃ ┗ todo_repository.dart
 ┃ ┗ usecases
 ┃ ┃ ┣ get_todos.dart
 ┃ ┃ ┗ save_todos.dart
 ┣ presentation
 ┃ ┣ controllers
 ┃ ┃ ┗ todo_controller.dart
 ┃ ┣ pages
 ┃ ┃ ┣ edit_todo_page.dart
 ┃ ┃ ┗ todo_page.dart
 ┃ ┗ widgets
 ┃ ┃ ┗ todo_widget.dart
 ┣ routes
 ┃ ┗ routes.dart
 ┗ main.dart</code></pre><p>처음 배울 때는 매우 추상적인 개념이여서 어떻게 구현하면 좋을지에 대해서 고민이 깊었는데, 예제 코드를 보면서 참고하면 좋을 것 같아서 이 글을 작성하게 되었다.</p>
<h3 id="data-영역">data 영역</h3>
<p>todo_local_datasource.dart
실제 로컬 DB에 있는 정보를 불러오는 방식으로 구현하였다.</p>
<pre><code class="language-dart">import &#39;package:flutter_todo_example/domain/entities/todo_entity.dart&#39;;
import &#39;package:hive/hive.dart&#39;;

class TodoLocalDataSource {
  final String _boxName = &#39;todos&#39;;
  Future&lt;Box&lt;TodoEntity&gt;&gt; get _todosBox async =&gt; await Hive.openBox(_boxName);

  Future&lt;List&lt;TodoEntity&gt;&gt; getTodos() async {
    final box = await _todosBox;
    return box.values.toList();
  }

  Future&lt;void&gt; saveTodos(List&lt;TodoEntity&gt; todos) async {
    final box = await _todosBox;
    for (final todo in todos) {
      await box.put(todo.id, todo);
    }
  }
}</code></pre>
<p>todo_repository_impl.dart
repository를 구현한 구현체이다.</p>
<pre><code class="language-dart">import &#39;package:flutter_todo_example/data/datasources/todo_local_datasource.dart&#39;;
import &#39;package:flutter_todo_example/domain/entities/todo_entity.dart&#39;;
import &#39;package:flutter_todo_example/domain/repositories/todo_repository.dart&#39;;

class TodoRepositoryImpl implements TodoRepository {
  final TodoLocalDataSource localDataSource;

  TodoRepositoryImpl(this.localDataSource);

  @override
  Future&lt;List&lt;TodoEntity&gt;&gt; getTodos() {
    return localDataSource.getTodos();
  }

  @override
  Future&lt;void&gt; saveTodos(List&lt;TodoEntity&gt; todos) {
    return localDataSource.saveTodos(todos);
  }
}</code></pre>
<h3 id="domain-영역">domain 영역</h3>
<p>todo_entity.dart
직렬화 할 수 있는 방법으로 모델링 하였다.</p>
<pre><code class="language-dart">import &#39;package:hive/hive.dart&#39;;

part &#39;todo_entity.g.dart&#39;;

@HiveType(typeId: 0)
class TodoEntity {
  @HiveField(0)
  final String id;
  @HiveField(1)
  final String title;
  @HiveField(2)
  final bool isCompleted;

  TodoEntity(this.id, this.title, this.isCompleted);

  TodoEntity copyWith({String? id, String? title, bool? isCompleted}) {
    return TodoEntity(
      id ?? this.id,
      title ?? this.title,
      isCompleted ?? this.isCompleted,
    );
  }
}</code></pre>
<p> todo_repository.dart
 data 영역의 repository의 interface를 만들어준다.</p>
<pre><code class="language-dart">import &#39;package:flutter_todo_example/domain/entities/todo_entity.dart&#39;;

abstract class TodoRepository {
  Future&lt;List&lt;TodoEntity&gt;&gt; getTodos();
  Future&lt;void&gt; saveTodos(List&lt;TodoEntity&gt; todos);
}</code></pre>
<p>get_todos.dart
dart의 특이한 문법인 <code>call()</code>이 등장하는데, 이는 클래스를 마치 함수처럼 부를 수 있도록 만들어둔 것이다.
이러한 방식으로 repository를 받은 후에 실제로 동작하는 부분이 나온다.</p>
<pre><code class="language-dart">import &#39;package:flutter_todo_example/domain/entities/todo_entity.dart&#39;;
import &#39;package:flutter_todo_example/domain/repositories/todo_repository.dart&#39;;

class GetTodos {
  final TodoRepository repository;

  GetTodos(this.reposity);

  Future&lt;List&lt;TodoEntity&gt;&gt; call() async {
    return await reposity.getTodos();
  }
}</code></pre>
<h3 id="presentation-영역">presentation 영역</h3>
<p>todo_page.dart
getx를 활용하여 obx를 사용하여 더 깔끔한 코드를 만들 수 있었다.</p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:flutter_todo_example/domain/entities/todo_entity.dart&#39;;
import &#39;package:flutter_todo_example/domain/usecases/get_todos.dart&#39;;
import &#39;package:flutter_todo_example/domain/usecases/save_todos.dart&#39;;
import &#39;package:flutter_todo_example/presentation/controllers/todo_controller.dart&#39;;
import &#39;package:flutter_todo_example/presentation/pages/edit_todo_page.dart&#39;;
import &#39;package:flutter_todo_example/presentation/widgets/todo_widget.dart&#39;;
import &#39;package:get/get.dart&#39;;

class TodoPage extends StatelessWidget {
  const TodoPage({super.key});

  @override
  Widget build(BuildContext context) {
    return GetBuilder&lt;TodoController&gt;(
        init: TodoController(Get.find&lt;GetTodos&gt;(), Get.find&lt;SaveTodos&gt;()),
        builder: (controller) {
          return DefaultTabController(
            length: 2,
            child: Scaffold(
              appBar: AppBar(
                title: const Text(&#39;Todo&#39;),
                bottom: const TabBar(
                  tabs: [
                    Tab(text: &#39;Todo&#39;, icon: Icon(Icons.list)),
                    Tab(text: &#39;Done&#39;, icon: Icon(Icons.check)),
                  ],
                ),
                actions: [
                  IconButton(
                    onPressed: () {
                      controller.textEditingController.clear();
                      Get.to(() =&gt; const EditTodoPage());
                    },
                    icon: const Icon(Icons.add),
                  ),
                ],
              ),
              body: TabBarView(children: [
                Obx(() {
                  final inProgressTodos =
                      controller.todos.where((t) =&gt; !t.isCompleted).toList();
                  return _buildTodoList(inProgressTodos);
                }),
                Obx(() {
                  final completedTodos =
                      controller.todos.where((t) =&gt; t.isCompleted).toList();
                  return _buildTodoList(completedTodos);
                })
              ]),
            ),
          );
        });
  }

  Widget _buildTodoList(List&lt;TodoEntity&gt; todos) {
    return ListView.builder(
      itemCount: todos.length,
      itemBuilder: (context, index) {
        final todo = todos[index].obs;
        return TodoWidget(todo: todo, key: Key(todo.value.id));
      },
    );
  }
}</code></pre>
<p>todo_controller.dart
todo와 관련된 getx controller를 선언하여, 내부적으로 사용하는 데이터들을 메모리에서 관리한다.</p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:flutter_todo_example/domain/entities/todo_entity.dart&#39;;
import &#39;package:flutter_todo_example/domain/usecases/get_todos.dart&#39;;
import &#39;package:flutter_todo_example/domain/usecases/save_todos.dart&#39;;
import &#39;package:get/get.dart&#39;;

class TodoController extends GetxController {
  final GetTodos getTodos;
  final SaveTodos saveTodos;
  final textEditingController = TextEditingController();

  TodoController(this.getTodos, this.saveTodos);

  final todos = &lt;TodoEntity&gt;[].obs;

  @override
  void onInit() {
    super.onInit();
    loadTodos();
  }

  Future&lt;void&gt; loadTodos() async {
    final result = await getTodos();
    todos.value = result;
    update();
  }

  void addTodo(TodoEntity todo) {
    todos.add(todo);
    saveTodos(todos);
    textEditingController.clear();
    update();
  }

  void toggleIsCompleted(TodoEntity todo) {
    final index = todos.indexWhere((element) =&gt; element.id == todo.id);
    todos[index] = todo.copyWith(isCompleted: !todo.isCompleted);
    saveTodos(todos);
    update();
  }

  void editTodo(TodoEntity todo) {
    final index = todos.indexWhere((element) =&gt; element.id == todo.id);
    todos[index] = todo;
    saveTodos(todos);
    textEditingController.clear();
    update();
  }
}</code></pre>
<h2 id="후기">후기</h2>
<p>앞에서 getx를 사용하니까 뭔가 코딩 실력이 나빠진다는 생각이 들어서 최근 provider를 많이 사용하였는데, 확실히 getx는 서비스를 빠르게 만들기에는 좋다는 생각이 들었다.
물론 그만큼 직관적이고, obx를 활용하여 StatelessWidget들로 쌓아나가는 것이 가능하다는 것이 좋은 부분인 것같다.
또한 클린 아키텍쳐를 이용하면 영역들마다 코드들이 분리되어 있으니, 가독성이 매우 높아졌다는 사실을 알 수 있었다.</p>
<h2 id="예제-github">예제 Github</h2>
<p><a href="https://github.com/halfmoon-mind/flutter_todo_example">예제 레포지토리 링크</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[VSCode 권한 에러]]></title>
            <link>https://velog.io/@halfmoon_mind/VSCode-%EA%B6%8C%ED%95%9C-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@halfmoon_mind/VSCode-%EA%B6%8C%ED%95%9C-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Fri, 23 Jun 2023 07:50:35 GMT</pubDate>
            <description><![CDATA[<p>파일을 저장하려고 할 때, 다음과 같은 에러가 발생할 수 있다.
<code>Failed to save &#39;Fastfile&#39;: Insufficient permissions. Select &#39;Retry as Sudo&#39; to retry as superuser.</code></p>
<p>다음과 같은 명령을 터미널에 입력한다.</p>
<pre><code>echo $USER
simsanghyeon</code></pre><p>출력을 통해 현재 터미널에 접속한 유저의 닉네임을 확인할 수 있다.</p>
<pre><code>sudo chown -R [username] .
Password:</code></pre><p>관리자 권한을 해당 계정에게 주면, 파일이 정상적으로 저장할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[크롤링에 대한 보안 기술]]></title>
            <link>https://velog.io/@halfmoon_mind/%ED%81%AC%EB%A1%A4%EB%A7%81%EC%97%90-%EB%8C%80%ED%95%9C-%EB%B3%B4%EC%95%88-%EA%B8%B0%EC%88%A0</link>
            <guid>https://velog.io/@halfmoon_mind/%ED%81%AC%EB%A1%A4%EB%A7%81%EC%97%90-%EB%8C%80%ED%95%9C-%EB%B3%B4%EC%95%88-%EA%B8%B0%EC%88%A0</guid>
            <pubDate>Mon, 12 Jun 2023 05:49:20 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<h3 id="분야">분야</h3>
<p>요즘 크롤링과 관련된 이슈가 많이 생겨나고 있다.</p>
<p>사람들은 방대한 정보 속에서 살아가고 있고, 자신의 원하는 데이터들을 한 번에 모아서 정제된 형태로 보고 싶어하는 경향이 있다. 이러한 배경에서 크롤링을 통해 자신이 수집하고 싶은 데이터를 저장하고 싶어하는 욕구는 크게 늘어나고 있다.</p>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F2141a855-355a-4ec2-8b40-5608eca18332%2F%25E1%2584%2589%25E1%2585%25B3%25E1%2584%258F%25E1%2585%25B3%25E1%2584%2585%25E1%2585%25B5%25E1%2586%25AB%25E1%2584%2589%25E1%2585%25A3%25E1%2586%25BA_2023-06-04_%25E1%2584%258B%25E1%2585%25A9%25E1%2584%258C%25E1%2585%25A5%25E1%2586%25AB_1.44.16.png?id=4d568e61-e58b-43bf-824a-12f63758e2e0&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p>지난 1년간 “Crawling”에 대한 구글 트랜드 결과. 하루에 평균적으로 꾸준히 90회 이상의 검색이 존재하였다.</p>
<p>크롤링(crawling)이란 웹사이트, 하이퍼링크, 데이터, 정보 자원을 자동화된 방법으로 수집, 분류, 저장하는 것입니다. 보통 파이썬을 이용한 Requests를 자동화 하거나 자동화된 입력을 해주는 Selenium과 같은 툴을 사용하여 데이터를 수집하거나, 스파이더 같은 프로그램을 사용하여 웹페이지에 검색에 대한 인덱싱을 수행된다.</p>
<p>2022년에는 여행·숙박 어플리케이션 운영사 ‘여기어때’가 경쟁업체 ‘야놀자’의 데이터를 숙박업소 정보를 무단으로 유출해가며 ‘여기어때’에게 10억원 배상을 하라는 판결이 나오기도 하였다.</p>
<h3 id="동기">동기</h3>
<p>데이터를 실제 유저가 서비스를 이용하기 위해 데이터를 요청하는 것이 아니라 단순히 데이터를 수집하기 위한 목적, 혹은 특정한 작업을 자동화 하기 위한 수단으로 사용된다. 이러한 경우 대부분 대용량의 쿼리를 서버에 한 번에 보내게 되어 서버에 큰 부담을 주게 된다. </p>
<p>하지만 이를 막기 위해 많은 조건이나 제약을 걸게 되면 사용자가 불편함을 느끼거나, 이것을 처리하는 과정이 더 많은 자원이 소모될 수도 있기 때문에 사용자의 불편함을 최소화하면서 서버 부담을 줄일 수 있는 기술이 필요함을 인지하게 되었다.</p>
<h3 id="목적">목적</h3>
<p>봇이나 크롤러가 아닌 것을 인증하는 것에 사용자의 불편함을 최소화 하면서, 서비스에서 주요한 지적 재산권을 침해당하지 않고 서비스 제공자 입장에서 봇이나 크롤러에 의해 서버에 부담이 가는 것을 최소화 할 수 있는 방법을 고안하고자 하였다.</p>
<p>그리고 크롤링 차단 방법을 이미 시행하고 있는 기업에 대해서 연구하여 해당 기업들이 어떠한 방식으로 자신들의 지적 재산권을 지키고 서비스를 유지하는지 연구하였다.</p>
<p>이를 통해 추후에 관련된 서비스를 운영하는 경우에 해당 연구를 했던 경험을 기반하여 보안 및 봇에 대한 대비를 하여 개선된 API 및 아키텍쳐를 구상할 수 있을 것이다.</p>
<h3 id="연구-방법">연구 방법</h3>
<p>유저들이 존재하는 서비스가 있어 실제로 어떠한 방법을 통해 주로 크롤링 해가는지 로그를 확인해보면서 알아가는 방식이 가장 유효하지만, 본 연구자는 이러한 서비스를 갖고 있지 않으며, 그러한 데이터들은 서비스 운영측의 내부 데이터로만 존재할 것으로 예상하였다.
따라서 해당 크롤링 과정을 서비스 제공자 입장이 아니라, 유저 입장에서 데이터를 수집하기에 얼마나 어려운가에 대해서 조사하기 위해 직접 관련된 사이트들을 크롤링 시도해보거나 크롤링을 해본 경험이 있는 컴퓨터 관련 전공자들의 설문을 받아 진행하였다.</p>
<p>설문조사 대상 - 숭실대학교 IT대학 관련 학생
설문조사 기간 - 2023.05.22. ~ 2023.06.04
설문조사 인원 - 40명</p>
<h1 id="본론">본론</h1>
<h2 id="크롤링이란">크롤링이란?</h2>
<p>크롤링(crawling)이란 웹사이트, 하이퍼링크, 데이터, 정보 자원을 자동화된 방법으로 수집, 분류, 저장하는 것을 칭한다. 보통 파이썬을 이용한 Requests를 자동화 하거나 자동화된 입력을 해주는 Selenium과 같은 툴을 사용하여 데이터를 수집하거나, 스파이더 같은 프로그램을 사용하여 인덱싱을 수행된다.
크롤링은 검색 엔진이 웹 페이지를 수집하고 색인화하여 사용자의 검색 쿼리에 가장 적합한 페이지를 찾아주는 데 사용됩니다. 이 외에도 데이터 분석, 웹 마이닝, 데이터 마이닝 등에도 크롤링이 사용된다.</p>
<p><strong>robots.txt</strong></p>
<p>크롤링 기술이 악용되어 정보를 무단으로 복제하게 되면 지식재산권 침해 문제가 발생할 수 있다. 이러한 경우를 사전에 방지하기 위해 웹 페이지 소유자는 웹 페이지에 로봇 배제 표준을 사용하여 접근 제한에 대한 설명을 robots.txt에 기술한다. 하지만 이러한 노력에도 robots.txt에 명시된 부분 이상으로 데이터를 수집하는 경우가 비일비재하다.</p>
<p><strong><a href="https://www.google.com/robots.txt">robots.txt</a></strong></p>
<p>구글에 대한 robots.txt 문서. 어떠한 URI로 봇으로 접근이 가능하고, 아닌지를 명시하여 지적 재산권을 지킨다.</p>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F54686683-cf01-4eb4-8386-ebe70cfc76a4%2FUntitled.png?id=1d02315d-9b5d-4aa7-9c82-f3624bf1e4ff&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F3baa61fe-2e67-4047-b499-930bb80b311e%2FUntitled.png?id=78ca3726-fce2-47f2-924d-006a477a3720&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p>robots.txt에 대한 인식 조사를 진행하였을 때, 응답자의 31.7%(13명)가 robots.txt에 대해서 모르는 것을 확인하였다. 또한 크롤링을 해본 유저의 52.5%(21명)이 robots.txt에 명시된 수집 범위 이상으로 데이터를 수집해본 경험이 있었다. </p>
<p>이를 통해 크롤링을 시도한다면 많은 경우에 robots.txt에 기재된 범위 이상으로 데이터를 수집한다고 보여진다.</p>
<hr>
<h2 id="크롤링-하는-방식">크롤링 하는 방식</h2>
<p>여기서 서술하는 크롤링 방식은 기업에서 공개된 Open API(Meta의 GraphAPI, 카카오의 Kakao API 등)를 사용하는 것이 아닌, 유저가 원하는 데이터를 자동화하여 수집하는 방법을 말한다. 
다음에서 소개하는 방법 이외에도 다양한 방법으로 데이터를 수집할 수 있지만, 주요하고 많이 사용하는 크롤링 방식에 대해서 소개하였다.</p>
<ul>
<li><strong>Google Bot과 같은 spider류</strong></li>
</ul>
<p>웹 사이트를 방문한 뒤, 데이터를 얻기 위해 다른 페이지에 대한 링크를 반복적으로 따라 들어간다. 특정 데이터를 얻기위해 필요한 대상을 크롤링하고, 페이지로부터 원하는 데이터를 추출할 수 있도록 HTML parser를 함께 사용하게 된다. 주로 검색엔진의 인덱싱을 위해 사용되는 방식이다.</p>
<ul>
<li><strong>Shell-script</strong></li>
</ul>
<p>Shell-script는 주로 Unix도구를 이용하여 크롤링을 시도할 때 사용된다. 주로 <code>curl</code> 또는 <code>wget</code> 명령어를 사용하여 웹 상의 데이터를 가져온다. 가져온 데이터에서 특정한 조건을 grep과 같은 텍스트 처리 도구를 사용하여 HTML을 파싱하여 정보를 가공한다. http 요청에서 추출된 데이터를 기반으로 정규식(regex)를 이용하여 원하는 데이터를 추출할 수 있다.</p>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F57415cba-c47e-4866-953c-437772ce078b%2F%25E1%2584%2589%25E1%2585%25B3%25E1%2584%258F%25E1%2585%25B3%25E1%2584%2585%25E1%2585%25B5%25E1%2586%25AB%25E1%2584%2589%25E1%2585%25A3%25E1%2586%25BA_2023-06-04_%25E1%2584%258B%25E1%2585%25A9%25E1%2584%2592%25E1%2585%25AE_11.35.06.png?id=c0e7018b-be49-48d9-b678-9813d3f8bcea&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p>curl 명령을 통해 <a href="http://www.google.com">www.google.com</a>에 요청을 보내본 예제.</p>
<ul>
<li><strong>jsoup, scrapy, Beautifulsoup 등 HTML parser</strong></li>
</ul>
<p>Shell-script 정규 표현식 기반으로 된 도구들과 유사하게, HTML내의 패턴을 기반으로 해서 페이지 내의 데이터를 추출할 수 있다. 파이썬의 경우 주로 Requests나 Selenium과 같은 라이브러리와 결합하여 특정한 조건의 HTML 태그 내부 정보를 수집하는 형태로 많이 사용된다.</p>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F3c83ff8f-6fef-4e96-9a2f-074a99dde669%2F%25E1%2584%2589%25E1%2585%25B3%25E1%2584%258F%25E1%2585%25B3%25E1%2584%2585%25E1%2585%25B5%25E1%2586%25AB%25E1%2584%2589%25E1%2585%25A3%25E1%2586%25BA_2023-06-04_%25E1%2584%258B%25E1%2585%25A9%25E1%2584%2592%25E1%2585%25AE_11.43.29.png?id=90b89aff-3ff2-46b2-9d28-a5063148f2d1&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p>Selenium과 BeautifulSoup을 결합하여 대학교 시간표 데이터를 가져오는 예제 코드.</p>
<ul>
<li><p><strong>Selenium과 같은 가상 브라우저</strong></p>
<p>  웹 브라우저의 자동화를 가능하게 하는 도구로 Javascript, AJAX와 같은 요소들을 실행하고, 웹 페이지로부터 원하는 텍스트를 수집하기 위해 주로 사용되고 있다.
  작동하는 방식은 페이지가 로드되고 자바스크립트가 실행된 후, 브라우저에서 HTML을 가져온 다음 BeautifulSoup과 같은 HTML 구문 분석기를 사용하여 원하는 데이터 또는 텍스트를 추출한다. 혹은 렌더링된 페이지의 스크린 샷을 만들고, OCR을 사용하여 스크린샷으로부터 원하는 텍스트를 추출하기도 한다.
  단, Selenium은 웹 브라우저 인스턴스를 실제로 실행하기 때문에 다른 HTTP 라이브러리나 웹 크롤링 라이브러리에 비해 더 많은 리소스를 사용하고 속도가 느리다.</p>
</li>
<li><p><strong>사람이 수행하는 복사와 붙여넣기</strong></p>
<p>  사람이 직접 웹사이트에 방문해서 서비스 내부의 컨텐츠를 복사하고 붙여 넣을 수 있다. 인건비가 싼 나라에서 이러한 방법을 사용하여 적용할 수 있을 것이다.</p>
</li>
</ul>
<h2 id="크롤링을-효과적으로-막았다는-조건">크롤링을 효과적으로 막았다는 조건</h2>
<p>보고서에서 정의하는 크롤링을 효과적으로 막는다는 것은 다음과 같은 조건에 해당된다는 것으로 한다.</p>
<ul>
<li><strong>Requests만으로는 작동되지 않거나 일부 데이터만이 수집 가능하여 Selenium으로만 작동이 가능한 경우</strong></li>
</ul>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F63fe9d31-f6b4-45e6-bb4a-4461218cef35%2FUntitled.png?id=db2aa803-6c10-4df1-82f4-898b87f76bf8&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p>Selenium을 사용 시 CPU와 Memory 사용량. Python의 Requests 라이브러리를 사용하는 경우 CPU나 메모리 사용량은 획기적으로 감소한다.</p>
<p>웹 브라우저를 띄우는 Selenium을 사용하는 방식을 사용하는 경우 높은 자원 소모와 단순한 requests보다 적게는 2~3배, 많게는 100배 이상의 속도 차이와 압도적인 메모리 사용량 때문에 Selenium을 사용하게 하는 것 자체만으로 큰 단위의 요청을 못하게 할 수 있다.</p>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fc8ee8628-eb0f-40c3-9c25-50eddcb0b058%2FUntitled.png?id=89a37749-20bf-4d6a-8ebc-0a668fa5f6fc&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p>사이트들을 크롤링하기 가장 어려웠던 이유는 Selenium과 같은 브라우저를 띄워야하는 것으로 확인되었다.</p>
<ul>
<li><strong>스크롤이 될 때마다 특정한 Javascript의 함수가 실행되어 실제 브라우저 혹은 자동화된 실행을 거부하는 경우</strong></li>
</ul>
<p>데이터를 지속적으로 특정 Javascript 함수가 실행되어 특정 쿠키나 헤더에 추가된 인증 요건이 추가되어야 정상적인 응답이 도착하는 경우이다.</p>
<p>해당 로직을 모두 알고 있어야 정보를 수집할 수 있어, 크롤링하는 측에서 이를 해결하기 위해 많은 자원을 사용해야 한다. 또한 인증과 관련된 정보를 주기적으로 변경하는 경우에는 크롤링하는 측에서도 해당 방식을 알아내기 위해 지속적인 자원을 소모해야 하는 경우에도 효과적으로 막았다고 해당할 수 있다.</p>
<ul>
<li><strong>HTML 태그나 구조가 자주 변경되어 완전한 자동화가 되지 않고, 크롤링하는 측에서도 지속적인 인적자원을 소모해야하는 경우</strong></li>
</ul>
<p>이러한 경우에는 지속적으로 변경되는 값을 확인하기 위해서 인원들이 크롤링 서비스를 유지보수하고 있어야하고, 아예 구조가 변경되는 경우에는 크롤링 로직을 아예 다시 짜야함으로 서비스의 지적 재산권이나 크롤링 하는 측의 막대한 분량의 요청에 대해서 요청 자체에서 빠르게 제한할 수 있다. </p>
<h2 id="크롤링을-막는-방법">크롤링을 막는 방법</h2>
<p>데이터를 사용자에게 제공하는 IT 관련 기업들은 이미 이러한 크롤러에 대한 방어를 위한 대책이 잘 되어 있다. </p>
<p>따라서 주요하게 크롤링을 방지하는 방법들을 소개하고, 기업들에서는 실제로 해당 기술을 어떻게 적용하고 있는지에 대해서 조사하였다.</p>
<ul>
<li><strong>로그와 트래픽 패턴 감시를 통한 비정상적인 활동에 대한 접근 제한</strong></li>
</ul>
<p>비슷한거나 같은 IP 주소로부터 많은 수의 유사한 작업을 수행하는 계정 혹은 요청이 발생할 경우, 이를 크롤러를 이용한 자동화된 접근을 나타내는 비정상적인 활동으로 인지하고 접근을 차단하거나 속도를 제한할 수 있다.</p>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F130789af-09dc-4eb7-aa63-0e7ae7096f23%2FUntitled.png?id=dba308ac-50ff-4c3e-9ee7-46a45c66de35&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p>Too Many Requests에 해당되는 HTTP error 429</p>
<p>예를 들어 특정 IP 주소 또는 사용자로부터 초당 몇번만의 검색을 허용하는 방식으로 제한할 수 있다. 이런 제한을 하는 경우 크롤러의 속도가 느려지고, 작업을 위한 효율성이 떨어지게 됩니다. 이 경우에는 실제 유저가 속도 제한이 걸리거나 접근이 제한되지 않을 정도의 상한선 조정이 필요하다.</p>
<p>이것을 차단하는 경우 클라우드 서비스를 사용하는 크롤러도 효과적으로 방지할 수 있다. 클라우드 서비스로 크롤링을 시도하는 경우, 다른 크롤러들이 해당 서비스에 대해서 크롤링을 이미 시도했다면 IP 주소가 유사하기 때문에 다른 크롤러의 사용을 효과적으로 차단할 수도 있을 것이다.</p>
<p><strong><em>인스타그램</em></strong></p>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F2668e9b3-cd72-4cd6-b0ce-da02af9a1e2e%2FUntitled.png?id=dcec8e08-7642-4790-ac21-2595d6e575ef&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p>인스타그램의 경우 댓글은 약 200회, 좋아요는 약 1000회 이상의 요청을 하루 내에 보내는 경우나 계정당 1시간에 5000회의 HTTP Requests를 요청한 경우에도 <code>나중에 다시 시도하세요</code> 창이 뜨면서 크롤링 작업이 중지 되는 것으로 확인하였다.</p>
<blockquote>
<p><strong><em><a href="https://about.fb.com/ko/news/2021/04/facebook%EC%9D%B4-%EC%8A%A4%ED%81%AC%EB%9E%98%ED%95%91%EC%97%90-%EB%8C%80%EC%B2%98%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95/">Facebook이 스크래핑에 대처하는 방법</a></em></strong>
스크래핑을 보다 어렵게 만드는 첫 번째 방법은 실행 횟수와 데이터 제한입니다. 실행 횟수 제한은 주어진 시간 동안 Facebook 제품을 활용할 수 있는 횟수의 제한을 뜻하며, 데이터 제한은 저희 제품을 정상적으로 이용하는 데 필요한 양보다 더 많은 데이터를 얻지 못하도록 합니다.
스크래핑 용의자의 활동을 조사하고 파악함으로써 Facebook 시스템을 강화하며, 현재까지 데이터 오용에 대한 다양한 조치를 취했습니다. 대표적인 조치로는 <a href="https://about.fb.com/news/2020/06/automation-software-lawsuits/">중지서한 발송</a>, <a href="https://about.fb.com/news/2021/01/combating-scraping-by-malicious-browser-extensions/">계정 비활성화</a>, 악의적 목적으로 스크래핑에 가담하는 스크래퍼에 대한 <a href="https://about.fb.com/news/2020/10/taking-legal-action-against-data-scraping/">소송 제기</a>, 스크래핑된 데이터를 호스팅하는 회사에 대한 삭제 요청 등이 포함됩니다. 정부 차원에서 불법 스크래핑 행위를 조사하고 조치를 취해야 하는 이유이기도 합니다.</p>
</blockquote>
<ul>
<li><strong>Captcha</strong><blockquote>
</blockquote>
<img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fd82e10aa-3526-40b5-9307-1050b10fb0ec%2FUntitled.png?id=59d4b0af-e091-49e7-abb7-29fed32de63b&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""><blockquote>
</blockquote>
웹 서핑을 하다보면 한번쯤 마주치는 Captcha. 통과하기 어려워 사용자 경험을 크게 저하한다.</li>
</ul>
<p>로봇이나 스크립트 등 자동화된 방식으로 접근하는 것을 방지하고, 인간 사용자만이 웹 사이트나 애플리케이션에 접근할 수 있도록 보안을 강화한다. 이를 통해 API를 찾아서 Request를 통해 데이터를 받아오는 것 뿐만 아니라 Selenium과 같은 가상 브라우저를 작동하여 데이터를 저장하는 크롤러에 대해서도 크게 효과적이다. 또한 OCR을 통해 이미지나 텍스트를 자동으로 읽어내는 공격을 방지하는 데 도움을 준다.</p>
<p>하지만 Captcha를 도입하는 것 자체가 사용자 경험을 크게 저하하게 된다. 도입 이후에 이를 통과하는 부분 또한 서비스 이용자의 경험을 크게 해칠 수 있다</p>
<ul>
<li><strong>사용자 등록 및 로그인 요구</strong></li>
</ul>
<p>웹 사이트에서 계정을 생성하고, 로그인하도록 요구하는 경우, 사용자와 크롤러의 작업을 정확하게 추적할 수 있다. 이 방식의 경우 특정 계정이 크롤링에 이용되는지를 쉽게 감지하고 차단할 수 있다. 
단순히 IP 주소를 이용하는 것 대신 크롤링 계정의 동작을 식별할 수 있으며, 속도 제한이나 짧은 시간동안에 수행되는 많은 수의 검색과 같은 잘못된 사용의 검출이 매우 쉬워질 수 있다.</p>
<p>계정으로만 유저를 구분하는 경우 여러 개의 계정을 번갈아가면서 동작하는 크롤러의 경우에는 막기 어려울 수 있다. 따라서 위에 작성한 IP 기반 요청 제한과 계정 기반 요청 제한을 같이 사용한다면 더욱 효과적으로 막을 수 있을 것이다.</p>
<p>또한 로그인을 강제한다면 실제 사용자가 서비스를 이용하는 부분에서 불편함이 생기거나 SEO(검색 엔진 최적화)에 나쁜 영향을 줄 수 있다.</p>
<ul>
<li><strong>Cloud-hosting 및 VPN 서비스 IP 주소 접근 차단</strong></li>
</ul>
<p>전문적으로 크롤링을 하거나, 동일 IP 접속을 회피하기 위해 AWS 또는 Google app-engine과 같은 웹 호스팅 서비스 또는 VPS 들에서 실행될 수 있다. 이런 종류의 서비스에서 이용하는 IP 주소에서 발생한 요청에 대해서는 웹 사이트 접속을 차단할 수 있다. 
이러한 클라우드 호스팅 서비스의 경우에는 비슷한 IP 주소를 사용하는 경우가 많아, 위에서 설명했듯 비슷한 IP에 요청 제한을 둔다면 효율적으로 크롤링을 제한할 수 있다.</p>
<ul>
<li><strong>컨텐츠의 텍스트를 이미지로 제공</strong></li>
</ul>
<p>서비스에서 사용하는 정보, 텍스트나 여러 데이터들을 이미지 서버 측에서 렌더링하여 이미지로 정보들을 보내준다. 이러한 경우에는 이미지 자체로 데이터를 제공하기 때문에 크롤러가 텍스트를 추출하는데 방해하거나 저해하는 방법이 될 수 있다.</p>
<p>하지만 제공해야하는 네트워크 비용이 훨씬 커져야 하고, 데이터를 보고 싶어 하는 실제 유저의 편의성이 크게 떨어지고, 검색엔진 최적화가 불가능한 부분이 있다. 또한 일부 크롤러들은 OCR로 우회할 수도 있습니다.</p>
<ul>
<li><strong>제공하는 이미지를 변형</strong></li>
</ul>
<p>이미지 내의 순서를 변경하여 순수하기 API 요청만 보내어 이미지 데이터를 받아보는 경우에 읽기 어려운 정보를 제공할 수 있다.</p>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fe5448df9-8719-41ec-a8f0-f56b1b128f2b%2FUntitled.png?id=1aab3411-79b4-4122-933a-003d38552951&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p>만화 공유 사이트에 API 요청만 한 경우. 제대로된 정보가 보이지 않는다.</p>
<p>해당 이미지를 서버에서 받는 순간에는 해당 이미지가 셔플되어서 도착하기 때문에 이미지 자체를 보는 API요청 만으로는 이미지를 제대로 볼 수 없다. 하지만 해당 이미지를 프론트엔드에서 받아오면서 특정 JavaScript 함수를 실행함으로 다시 원래 이미지로 복원하는 방식을 사용할 수 있다.</p>
<ul>
<li><strong>웹 사이트 HTML 구조를 자주 변경</strong></li>
</ul>
<p>해당 방법은 서버에서 작업들을 수행하여 HTML 자체를 보내주는 SSR(Server Side Rendering) 방식에서 크게 유효하게 작동할 수 있다. </p>
<p>웹 사이트에서 사용하는 HTML의 각 요소의 ID와 클래스가 자주 변경하거나, 혹은 자동으로 변경되도록 한다. 이 경우에는 기존의 ID나 클래스 이름으로 데이터를 찾던 크롤러가 작동하지 않고, 서비스 제공자쪽에서 데이터를 바꿀 때마다 다시 동작하도록 설정해야 한다.</p>
<p><strong><em>숭실대학교 u-saint</em></strong></p>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F8a2733b1-ac86-4f32-8313-963a41eac60c%2FUntitled.png?id=39768563-ca17-4159-a1a7-3cb7d4a14ece&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F51092ba7-864b-45a3-8660-edce1fef11e0%2FUntitled.png?id=09b93d2d-c5f7-4a5c-9356-d26e9474cd46&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p>같은 데이터를 요청하는 태그를 찾아도, 다른 세션 및 탭에서 이를 요청할 시에 id값이 다르게 확인되는 것을 확인할 수 있다.</p>
<ul>
<li><strong>쿠키나 헤더를 사용해 인증 로직 추가</strong></li>
</ul>
<p>자바스크립트를 사용하여 쿠키를 설정하고 검색하면 자바스크립트를 실행하지 않는 크롤러는 요청과 함께 쿠키를 보낼 수 없으므로 크롤러를 차단할 수 있다. 만약 쿠키나 헤더를 추가하는 로직을 만들 수도 있지만 관련된 정보를 찾기 위해서 많은 인적 자원이 소모됩니다.
예를 들어 어느 정도 스크롤을 하거나 특정한 버튼을 누른다면 헤더나 쿠키를 추가하여 추후 데이터 요청에는 관련된 데이터가 있어야 응답하고, 아니라면 응답을 거절하는 상황이 있을 수 있다.</p>
<blockquote>
</blockquote>
<p><strong>네이버 쇼핑</strong></p>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Ffa78e13e-8643-4f30-a4c1-302ce3c221db%2F%25E1%2584%2589%25E1%2585%25B3%25E1%2584%258F%25E1%2585%25B3%25E1%2584%2585%25E1%2585%25B5%25E1%2586%25AB%25E1%2584%2589%25E1%2585%25A3%25E1%2586%25BA_2023-06-07_%25E1%2584%258B%25E1%2585%25A9%25E1%2584%2592%25E1%2585%25AE_2.20.13.png?id=410a0d0c-9948-47c2-8152-485192f3752c&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Ffad07ef8-2149-4fbf-9917-1f66c8ce5e09%2F%25E1%2584%2589%25E1%2585%25B3%25E1%2584%258F%25E1%2585%25B3%25E1%2584%2585%25E1%2585%25B5%25E1%2586%25AB%25E1%2584%2589%25E1%2585%25A3%25E1%2586%25BA_2023-06-07_%25E1%2584%258B%25E1%2585%25A9%25E1%2584%2592%25E1%2585%25AE_2.20.38.png?id=da18ac00-3b06-4c39-952b-49334e5662ba&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p>동일한 URL에 대한 요청이지만, API 요청이 일어난 URL에 GET 요청을 보낸 것과 실제 사이트의 네트워크에서 찍히는 데이터가 다르게 요청만 보낸 경우에는 <code>401 Unauthorized</code> 응답이 도착했다. 인증과 관련된 쿠키 정보가 있어야 정보를 받아올 수 있는 것으로 확인되었다.</p>
<ul>
<li><strong>마크업, 스크립트로부터 네트워크 요청에 대한 난독화</strong></li>
</ul>
<p>네트워크 요청에서 데이터를 직접적으로 볼 수 있다면 페이지 혹은 데이터에 대한 API 명세가 전부 드러나는 형태가 된다.
따라서 네트워크 요청이 발생하더라도 서버측에서 데이터를 보내줄 때, 이를 난독화하여 클라이언트에게 전송하고, 클라이언트는 해당 데이터를 사이트 내부에 숨겨진 특정 Javascript 함수를 실행하여 이를 다시 복호화하여 유저에게 정보를 보여주는 방식을 사용할 수 있다.
크롤링하는 측에서 해당 데이터를 보고 싶다면 난독화된 데이터를 다시 복호화하는 작업을 거쳐야 크롤러가 작동할 수 있어, Javascript를 실행하지 않는 HTML 파서 기반 크롤링 방식에 대응하기 매우 효과적이다.</p>
<blockquote>
</blockquote>
<p><strong>숭실대학교 u-Saint</strong></p>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fcdf91612-ce16-424f-92b2-c9db53308bfd%2F%25E1%2584%2589%25E1%2585%25B3%25E1%2584%258F%25E1%2585%25B3%25E1%2584%2585%25E1%2585%25B5%25E1%2586%25AB%25E1%2584%2589%25E1%2585%25A3%25E1%2586%25BA_2023-06-07_%25E1%2584%258B%25E1%2585%25A9%25E1%2584%2592%25E1%2585%25AE_2.02.19.png?id=7dfd7149-3a82-4dc2-9e10-88e2215e7a71&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p>유세인트의 강의시간표에서 “다음학기” 버튼을 눌렀을 때 도착한 응답. 해당 요청이 어떤 데이터를 불러오고, 새롭게 저장하는지 네트워크 도구에서 보기 어렵다.</p>
<h1 id="결론">결론</h1>
<p>연구해본 기업들에서는 모두 하나의 크롤링 방지 기술만을 사용하는게 아니라, 다양한 방지 기술을 복합적으로 사용하는 것으로 확인되었다. 특히 인스타그램의 경우 쿠키를 통한 인증 추가, 응답 요청 수 제한, Captcha 등 다양한 방지 기술을 구현한 것으로 확인되었다.</p>
<blockquote>
</blockquote>
<p><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F2a797442-387f-44e9-a0b9-29df65289082%2FUntitled.png?id=7aada1f2-e578-4232-899d-bc1834ddd009&table=block&spaceId=3e915a9b-e334-4059-bb5c-66f25bea08a1&width=2000&userId=01c34b45-7f0e-43af-99eb-e7105d12e8df&cache=v2" alt=""></p>
<blockquote>
</blockquote>
<p>전체 47.4%의 응답자(18명)가 인스타그램을 크롤링하는게 제일 어려웠다고 대답하였다. (중복 허용)</p>
<p>하나의 서비스에 대해서 완벽하게 막는 것은 불가능에 가깝다. 데이터 수집이 필수적인 상황에 놓인다면 사람을 고용하여 자동화가 안되더라도 직접 사람이 모든 요청을 브라우저에서 보내고 복사/붙여넣기를 수행하는 방법도 존재하기 때문이다. 
또한 너무 많은 크롤링 방지 기술을 사용하는 경우에는 오히려 방지 기술을 유지하고 정상적인 서비스 운영하는 데에 더 큰 간접 비용(Overhead)가 발생할 것이다.</p>
<p>따라서 자신이 제공하고 있는 서비스에서 제공하는 정보가 얼마나 중요한가를 고려해봐야 한다. 크롤링하기 가장 어려웠던 인스타그램의 경우 자신의 서비스 내부 데이터들이 큰 자산이고, 내부 데이터로 서비스가 운영되는 부분이니 크롤링을 막는 것에 큰 노력을 기울인다고 보여진다. 만약 자신의 서비스가 봇이나 크롤러에게 수집되면 민감하거나 주요한 지적재산권이 침해된다고 생각되면 본 보고서에서 작성된 여러 크롤링 방지 대책을 사용하여 수집을 막을 수 있을 것이다.</p>
<h1 id="레퍼런스">레퍼런스</h1>
<ul>
<li><a href="https://biz.chosun.com/topics/law_firm/2022/08/26/BNRMJG45ZNB7BCW7UMAJ47ACII/">https://biz.chosun.com/topics/law_firm/2022/08/26/BNRMJG45ZNB7BCW7UMAJ47ACII/</a> - 야놀자 10억원 배상</li>
<li><a href="https://ragisamton.tistory.com/71">https://ragisamton.tistory.com/71</a> - 인스타그램 이용제한 범위</li>
<li><a href="https://about.fb.com/ko/news/2021/04/facebook%EC%9D%B4-%EC%8A%A4%ED%81%AC%EB%9E%98%ED%95%91%EC%97%90-%EB%8C%80%EC%B2%98%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95/">https://about.fb.com/ko/news/2021/04/facebook이-스크래핑에-대처하는-방법/</a> - 인스타그램 제한 공식문서</li>
<li><a href="https://blog.naver.com/shino1025/221458924033">https://blog.naver.com/shino1025/221458924033</a> - 만화 사이트에서 크롤링 방지 솔루션</li>
<li><a href="https://redfox.tistory.com/31">https://redfox.tistory.com/31</a> - 크롤링을 불법으로 발생하는 경우</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL에서 emoji 저장]]></title>
            <link>https://velog.io/@halfmoon_mind/MySQL%EC%97%90%EC%84%9C-emoji-%EC%A0%80%EC%9E%A5</link>
            <guid>https://velog.io/@halfmoon_mind/MySQL%EC%97%90%EC%84%9C-emoji-%EC%A0%80%EC%9E%A5</guid>
            <pubDate>Sun, 30 Apr 2023 06:01:13 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>Flutter에서 emoji_picker 라이브러리를 통해서 이모지를 저장해서 데이터 베이스에 저장했지만, 정확한 이모지가 저장되지 않고, 다른 이모지가 저장되는 오류가 발생하고 있었다.</p>
<p>컬럼의 스키마는 <code>Name : emoji, Type : varchar(4), Character Set : utf8mb4, Collation : utf8mb4_general_ci</code> 로 구성되었다.</p>
<p>클라이언트에서는 정확한 값으로 띄워지나, 새로고침(서버에서 데이터를 다시 로드)하는 경우에 이상한 값이 계속 들어오는 것이다. 데이터베이스를 확인해본 결과, 서버까지는 정확한 값이 들어가고 있으나, MySQL 데이터베이스 자체에서 잘못된 값이 저장되고 있었다.</p>
<h3 id="문제-예시">문제 예시</h3>
<p>👩‍👩‍👦‍👦 와 같은 이모지를 저장하는 경우 👩‍👦 로 변경되서 저장됨.</p>
<h2 id="문제-해결-방안-1">문제 해결 방안 1</h2>
<p>MySQL의 utf-8은 4바이트로 저장하는 것처럼 보이지만, 저장은 실제로 3바이트로 char형을 저장한다. → 이모지나 특수 문자들을 제외하면 일반적인 string들은 3바이트로 충분히 저장할 수 있기 때문에, utf-8로 저장하면 3바이트만 저장된다.</p>
<p>따라서 유니코드에 이모지와 같은 실제로 4바이트로 저장되야 하는 값 같은 경우에는 저장할 수 없다.</p>
<p>따라서 이러한 string을 저장하는 경우에는 저장 방식을 일반적인 utf-8이 아닌, utf8mb4로 저장해야 한다.</p>
<p><a href="https://velog.io/@lake/MySQLMaria-DB%EC%97%90-%EC%9D%B4%EB%AA%A8%ED%8B%B0%EC%BD%98Emoji-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0">MySQL(Maria DB)에 이모지(Emoji) 저장하기</a></p>
<p>→ 이미 utf8mb4로 string을 저장하고 있던 상태였었음. 그리고 utf-8로 저장하고 있던 경우에는 아예 이모지 자체가 표현되거나, 저장되지 않았을 것으로 추측할 수 있다.</p>
<h2 id="문제-해결-방안-2">문제 해결 방안 2</h2>
<p>현재 데이터를 저장하고 있는 Character Set이 utf8mb4 형태이지만, Collation이 utf8mb4_general_ci로 저장되고 있음. 찾아본 다른 해결 방안의 경우에는 utf8mb4_unicode_ci로 저장하는 경우를 생각하였다.</p>
<p><a href="https://stackoverflow.com/questions/39463134/how-to-store-emoji-character-in-mysql-database">How to store Emoji Character in MySQL Database</a></p>
<p>→ utf8mb4_unicode_ci와 utf8mb4_general_ci의 차이는 과거에는 cpu 연산 속도의 차이때문에 utf8mb4_general_ci 버전을 사용하였다. 하지만 요즘과 같이 컴퓨터 성능이 좋아진 경우에는 unicode 버전과 general 버전의 차이가 거의 없기 때문에, unicode로 사용해도 상관이 없다는 것이다. 그래서 조금 더 범용적인 unicode 버전으로 바꾸는 추세라고 한다.</p>
<ul>
<li><p><strong>utf8mb4_unicode_ci VS utf8mb4_general_ci</strong></p>
<p>  <code>utf8mb4_general_ci</code>성능 차이가 중요할 정도로 CPU 속도가 낮은 지점을 남겨두었기 때문에 더 이상 사용할 이유가 거의 없습니다 . 귀하의 데이터베이스는 거의 확실하게 이것 이외의 다른 병목 현상에 의해 제한될 것입니다.</p>
<p>  과거에는 <code>utf8mb4_general_ci</code>성능 비용을 정당화할 만큼 정확한 정렬이 중요할 때를 제외하고 일부 사람들이 사용을 권장했습니다. 오늘날 이러한 성능 비용은 거의 사라졌으며 개발자는 국제화를 더욱 심각하게 다루고 있습니다.</p>
<p>  정확도보다 속도가 더 중요하다면 정렬을 전혀 수행하지 않는 것이 나을 수도 있다는 주장이 있습니다. 정확할 필요가 없다면 알고리즘을 더 빠르게 만드는 것은 사소한 일입니다. 따라서 <code>utf8mb4_general_ci</code>속도상의 이유로 필요하지 않고 정확성상의 이유로도 적합하지 않은 절충안입니다.</p>
<p>  내가 추가할 또 다른 사항은 응용 프로그램이 영어만 지원한다는 것을 알고 있더라도 사람들의 이름을 처리해야 할 수 있다는 것입니다. 이 이름에는 종종 올바르게 정렬하는 것이 중요한 다른 언어에서 사용되는 문자가 포함될 수 있습니다. . 모든 것에 유니코드 규칙을 사용하면 매우 똑똑한 유니코드 사람들이 정렬 작업을 제대로 수행하기 위해 열심히 노력했다는 사실에 안심할 수 있습니다.</p>
</li>
</ul>
<p><a href="https://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci">What&#39;s the difference between utf8_general_ci and utf8_unicode_ci?</a></p>
<h2 id="문제-해결-방안-3">문제 해결 방안 3</h2>
<p>아예 이상한 값이 저장되는 것이 아니라, 조금 잘려서 저장되는 값을 보고 이것이 충분한 길이가 안되는지 확인보았다.
<img src="https://velog.velcdn.com/images/halfmoon_mind/post/642cf83f-eaea-40c0-bbfd-8e540365f7c3/image.png" alt=""></p>
<p>데이터를 저장하는 이모지를 보니, 👨‍👩‍👧‍👧 의 경우 제일 긴 길이인 25바이트를 저장하고 있고, 다음은 21바이트, 다음은 14바이트를 저장하고 있었다.</p>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/halfmoon_mind/post/c8f0fd4c-c55d-4411-ab05-5ebc8f819219/image.png" alt="">
매우 공신력 있는 chat gpt. VARCHAR(4)와 VARCHAR(8)의 크기가 각각 16과 32바이트를 저장한다고 알려준다.</p>
<p>위에서 알 수 있듯이, VARCHAR(4)의 경우에는 각 4개의 character를 저장할 수 있는데, 이를 4바이트를 저장하기 때문에 16바이트를 저장하게 된다.</p>
<p>위의 결과를 보니, 이모지를 임의로 큰 값을 넣었을 경우에 <code>ERROR 1265 : Data truncated for column ’emoji’ at row 1</code> 와 같은 에러를 발생시키는지 알 수 있었다. 즉 16바이트까지 밖에 저장되지 않는 데이터베이스 컬럼에 그것보다 큰 값이 들어가니, 오류가 나지 않는 선에서 데이터가 저장되고 있던 것이었다.</p>
<h2 id="최종-해결-방안">최종 해결 방안</h2>
<p>데이터베이스의 emoji 컬럼 값의 Type을 VARCHAR(4)에서 VARCHAR(8)로 저장해서 더 큰 값을 저장할 수 있도록 변경하니, 제대로 저장되는 것을 확인할 수 있었다.</p>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/halfmoon_mind/post/ef45b21b-0c6b-447f-bd47-3ca0971e992b/image.png" alt="">
잘 저장된다!</p>
<h2 id="느낀-점">느낀 점</h2>
<p>MySQL에서 기본적인 utf-8을 저장하면 3바이트만 저장된다는 것. 그리고 이모지가 다른 여러가지 옵션이 들어가면 점점 데이터의 크기가 커진다는 것을 알 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[시각장애인의 위한 지하철 내부 길 유도 시스템]]></title>
            <link>https://velog.io/@halfmoon_mind/%EC%8B%9C%EC%9E%91%EC%9E%A5%EC%95%A0%EC%9D%B8%EC%9D%98-%EC%9C%84%ED%95%9C-%EC%A7%80%ED%95%98%EC%B2%A0-%EB%82%B4%EB%B6%80-%EA%B8%B8-%EC%9C%A0%EB%8F%84-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@halfmoon_mind/%EC%8B%9C%EC%9E%91%EC%9E%A5%EC%95%A0%EC%9D%B8%EC%9D%98-%EC%9C%84%ED%95%9C-%EC%A7%80%ED%95%98%EC%B2%A0-%EB%82%B4%EB%B6%80-%EA%B8%B8-%EC%9C%A0%EB%8F%84-%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Thu, 23 Mar 2023 16:47:24 GMT</pubDate>
            <description><![CDATA[<h2 id="공모전-설명">공모전 설명</h2>
<p>장애인을 위한 기술을 공모하는 D-Tech 공모전에 참여하였습니다.</p>
<h2 id="지원-동기">지원 동기</h2>
<p>일반 사람들은 아무런 불편함 없이 이용하는 시설들이 장애를 가진 사람들에게는 일상생활을 하면서 모든 것에서 불편함을 느낄 것이라고 생각했다. 특히 최근 지하철을 비롯한 대중교통, 공공시설의 이용에 있어서 장애를 가진 사람들의 불편 호소가 사회적으로 대두되고 있다. 이에 인간공학과 유니버설 디자인을 바탕으로 장애가 없는 사용자는 물론 장애를 갖고 있는 사람들을 포함하여, 모두를 위한 제품이나 서비스 디자인에 대한 관심이 높아졌다. 그러던 중 D-Tech 기술 | 디자인 공모전을 접하게 되었다. D-Tech 기술 | 디자인 공모전의 지향점인 신체기능의 일부가 본래 기능을 못 하게 되는 경우에 그 기능을 구현하기 위해 적용하는 재활과학기술을 직접 실현하여 장애로부터 소외된 장애 당사자분들의 불편을 덜어줄 수 있는 제품과 서비스를 고안해 D-Tech 기술 | 디자인 공모전에 출품하고자 하였다.</p>
<h2 id="기획-의도">기획 의도</h2>
<p>현재 장애인 분들이 대중교통을 이용할 때는 여러 위험이 따른다. 특히 시각장애인 분들이 대중교통을 이용할 때 인프라나 서비스가 중요하지만, 실제 이용에서는 편리함을 크게 느끼지 못하는 상황이다. 그나마 지상에서 이용하는 버스 같은 경우에는 노면을 이용하여 G PS 를 통해 위치를 확인할 수 있다. 그러나, 지하철 같은 경우엔 지하에 위치하여 G PS 신호를 감지할 수 없기 때문에 이동하는 데에 있어서 G PS 를 이용하여 시각장애인들의 위치를 알려주는 보조장치를 이용하기 어렵다. 또한, 지하철역에는 계단, 에스컬레이터, 복잡한 구조 등으로 다른 대중교통에 비해 지하철은 시각장애인이 혼자서 스스로 이용하기 어렵다. 따라서 주변 사람들이나 역무원의 도움이 필요한 경우가 대부분이다. 이를 문제점으로 두고 시각장애인을 위한 지하철 내부 길 유도 시스템을 기획하게 되었다. 이 시스템은 지하철에서 시각장애인들이 지하철역과 지하철 내부에서 스스로 이동하는 데에 어려움이 없도록 도움을 주고자 한다. 이를 통해 시각장애인들이 도움 없이 혼자 지하철을 타고 목적지까지 안전히 이동할 수 있을 것으로 기대한다.</p>
<h2 id="설명">설명</h2>
<p>사용자의 위치를 추적하는 방식에는 GPS, BLE(비콘) 등을 활용하는 여러가지 방식이 있다.</p>
<p>GPS는 3개 이상의 GPS 수신 위성에서 송신된 신호를 수신하여 위치를 결정하는 방식인데, 요즘 스마트폰과 같이 여러가지 기기에서 위치를 특정하기 위해서 GPS를 활용하고 있다. 하지만 GPS의 단점은 지하로 들어가거나 도심 속에서 신호의 변형, 감쇄가 일어나면 위치를 정확하게 잡지 못하는 단점이 있다. 또한 지하로 들어가면 위성과의 통신이 약해짐에 따라 아예 신호가 끊어지는 경우도 발생한다.</p>
<p>우리는 이러한 GPS의 단점을 보완하고 지하로 들어갔을 경우에도 위치를 추적하는 방법을 고안하고자 하고, 이를 위한 추적 방식에 BLE(비콘)을 사용하고자 한다. BLE(비콘) 이하 비콘,은 Bluetooth Low Energy의 줄임말로 블루투스 저전력 모델을 의미한다. 비콘은 블루투스 4.0 프로토콜 기반의 근거리 무선통신 장치로, 반경 50~70m 범위 안에 있는 사용자의 위치를 찾아 메시지 전송, 모바일 결제 등을 가능하게 하는 기술이다. 또한 블루투스 4.0 프로토콜 기반이라 일반적인 블루투스와 다르게 별도의 페어링 없이 기기와 통신할 수 있다. 실내로 들어서면 전파가 차단되어 위치를 알 수 없는 GPS와 달리 실내로 들어서도 위치 파악이 가능하고 전력 소모가 적다는 장점이 있다.</p>
<p>위와 같은 비콘의 특징을 이용해서 위치를 유도하는 방법을 다음과 같다.</p>
<ol>
<li>지하철에 들어오면 가장 가까이에 있는 비콘이 보내오는 신호를 스마트폰 혹은 모바일 기기를 통해 받는다.</li>
<li>해당 비콘이 보내오는 신호의 세기를 계산해서 해당 비콘으로 올 수 있도록 유도를 하고, 이것은 지하철 내부로 이동하는 것으로 자연스럽게 유도된다. </li>
</ol>
<img     src="https://velog.velcdn.com/images/halfmoon_mind/post/fee53497-4a83-43bb-8b42-bbd528b8e747/image.png" alt="그림1" style="width:100%" />
<b>그림 1.</b>

  <img     src="https://velog.velcdn.com/images/halfmoon_mind/post/aa4f5e4d-644f-4164-adb4-acfccb9daa74/image.png" alt="그림2" style="width:100%">
<b>그림 2.</b>

<p>그림 1과 같이 비콘 A로 지하철 내부로 안내된다면, 다음은 그림 2와 같이 비콘 B쪽으로 이동하고, 다음 비콘 C로 이동할 수 있게 하여 자연스럽게 원하는 경로로 유도한다.</p>
<ol start="3">
<li>특정 위치로 사용자를 유도하는 방법은 비콘에서 송신되는 신호를 모바일 기기에서 신호의 세기를 판단하여, 모바일 기기에서 가장 강한 신호가 수신된 비콘의 정보를 토대로 사용자의 행동을 정하는 방법이다. 예를 들면 A 비콘에서는 앞으로 가라는 신호, B 비콘에서는 1초 이동 후에 왼쪽으로 90도 회전하라는 신호를 준다면, 만약 수신된 신호 중 가장 강한 신호가 B 비콘이라면 모바일 기기는 1초 이동 후 왼쪽으로 90도 회전하는 방식으로 작동하면 된다.   <img     src="https://velog.velcdn.com/images/halfmoon_mind/post/25465b1e-5a82-49b8-9ce4-b3d59197f8c9/image.png" alt="그림3" style="width:100%">
<figcaption align = "center"><b>그림 3.</b></li>
<li>지팡이에 달린 자이로센서로 사용자가 이동하는 방향을 인식하여, 비콘의 정보를 바탕으로 사용자가 올바른 방향으로 갈 수 있도록 유도를 하고, 만약 잘못된 경로로 사용자가 이동할 시에는 진동 발생기를 이용하여 올바른 방향으로 이동할 수 있도록 한다. 그림 3과 같이 사용자의 지팡이에 자이로센서를 부착하여 주변의 비콘 중에서 가장 강한 신호를 주는 정보를 토대로 사용자의 이동 방향을 결정하게 된다.</li>
<li>최종적으로 사용자가 원하는 위치에 도달하게 된다.</li>
</ol>
<p>위와 같은 순서를 통해 사용자가 어디로 갈지 유도한다면, 지하철 내부와 같은 지하 공간에서도 시각장애인을 위한 길 안내 유도를 할 수 있을 것이다. 유도를 위한 비콘은 BoT-nLE523D, 방향 탐색을 위한 자이로센서는 QMC5883L 센서를 사용하였다.</p>
<div class="row">
  <div class="column">
  <img     src="https://velog.velcdn.com/images/halfmoon_mind/post/0f655fea-1203-4d0d-826d-cb31d327af9f/image.png" alt="그림4. 자이로센서 예제 코드">
<figcaption align = "center"><b>그림 4. 자이로센서 예제 코드</b></div>
  <div class="column">
    <img     src="https://velog.velcdn.com/images/halfmoon_mind/post/01b2a837-e48c-40e3-9308-9462b5843b2a/image.png" alt="그림 5. 그림 4 를 활용한 자이로센서 사용 예시">
<figcaption align = "center"><b>그림 5. 그림 4 를 활용한 자이로센서 사용 예시</b>
  </div>
</div>

  <style>
    .column {
  float: left;
  width: 50%;
}

/* Clear floats after the columns */
.row:after {
  content: "";
  display: table;
  clear: both;
}
    </style>

<p>그림 4와 같이 자이로센서를 아두이노와 결합하여 예시로 방향 탐색을 진행하였지만, 이것을 상용화하여 조금 더 작고 가벼운 모듈을 붙여서 사용한다면 사용자에게 더욱 편리한 이동을 제공할 수 있을 것이다. 또한 사용자의 이동을 유도하기 위한 정보를 활용하여 비콘에 접속됐던 기기들에 대해서 정보를 확인하여 지하철의 사용 정보라던지, 운영 정보로서 활용하는 방식까지 확장할 수 있을 것으로 전망된다.</p>
<p>종합하면, GPS는 지하철 내부와 같은 실내에서는 정확한 위치 파악이 어렵고, 지하로 들어가면 위성과의 통신이 끊어지는 등의 단점이 있다. 따라서 이러한 문제를 해결하기 위해 BLE(비콘) 기술을 활용하여 사용자의 위치를 추적하고 유도하는 방식을 고안하였다. 비콘은 블루투스 4.0 프로토콜 기반의 근거리 무선통신 장치로, 지하철 내부에서도 위치 파악이 가능하고 전력 소모가 적다는 장점이 있다. 따라서 사용자의 이동을 유도하기 위해 비콘과 자이로센서를 활용하여 위치 추적 및 방향 탐색 기능을 구현하였다. 이러한 기술을 활용하면 시각장애인을 포함한 다양한 사용자들에게 지하철 내부에서도 편리하게 이동할 수 있는 환경을 제공할 수 있을 것이다.</p>
]]></description>
        </item>
    </channel>
</rss>