<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>zero-black.log</title>
        <link>https://velog.io/</link>
        <description>나의 삽질이 미래의 누군가를 구할 수 있다면...</description>
        <lastBuildDate>Fri, 07 Feb 2025 14:13:45 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>zero-black.log</title>
            <url>https://velog.velcdn.com/images/zero-black/profile/52b4d104-a51d-4ea8-b4a7-333750233330/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. zero-black.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/zero-black" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[클라우드 용어 정리]]></title>
            <link>https://velog.io/@zero-black/%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EC%9A%A9%EC%96%B4-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@zero-black/%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EC%9A%A9%EC%96%B4-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 07 Feb 2025 14:13:45 GMT</pubDate>
            <description><![CDATA[<h1 id="as-a-service">as-a-Service</h1>
<p><a href="https://news.mt.co.kr/mtview.php?no=2023111414345763661">https://news.mt.co.kr/mtview.php?no=2023111414345763661</a></p>
<ul>
<li>제 3사에서 클라우드 컴퓨팅 서비스를 제공한다는 의미</li>
<li>cf. on-premise: 기업이 자체 시설 내에 설치하여 운영하는 경우</li>
</ul>
<p><img src="https://velog.velcdn.com/images/zero-black/post/b6b220f3-3263-47b3-9897-1d1ab388df48/image.png" alt=""></p>
<h2 id="iaas-infrastructure-as-a-service">IaaS: Infrastructure as a Service</h2>
<ul>
<li>서버, 네트워크, 스토리지 등을 제공하지만 OS설치, 네트워크 설정 등은 사용자가 운영해야 함</li>
<li>사용자는 API 또는 대시보드를 통해 인프라에 접근, 제어</li>
<li>장점<ul>
<li>필요한 구성 요소만 구매, 확장 또는 축소할 수 있는 유연성 제공</li>
<li>on-premise에 비해 개발 및 테스트 환경의 구축&amp;제거가 빠르고 유연함</li>
</ul>
</li>
<li>AWS EC2, Microsoft Azure VM, Google Cloud</li>
</ul>
<h2 id="paas-platform-as-a-sercice">PaaS: Platform as a Sercice</h2>
<ul>
<li><p>OS, 개발 도구 등을 제공하여 사용자는 어플리케이션 코드만 업로드하면 됨</p>
</li>
<li><p>AWS Elastic Beanstalk, Heroku, Vercel, Netflify, Firebase Hosting</p>
</li>
<li><p>Q. github-pages는 PaaS일까?</p>
<p>  → 단순히 static file을 호스팅 해주는 것으로, PaaS로 보기는 힘듦</p>
<ul>
<li>유사한 정적 호스팅<ul>
<li>S3 + CloudFront</li>
</ul>
</li>
<li>호스팅 플렛폼 (ex. WordPress)은 오히려 SaaS에 가까움 (사용자의 서버 설정이 필요 없음)</li>
</ul>
</li>
</ul>
<h3 id="aiaas-ai-as-a-service">AIaaS: AI as a Service</h3>
<p><img src="https://velog.velcdn.com/images/zero-black/post/6f236465-009a-409a-9176-961a34417dab/image.png" alt=""></p>
<ul>
<li>클라우드 기반의 AI 기술을 통해 기업이 AI 인프라 없이도 다양한 AI 기능을 활용할 수 있게하는 서비스</li>
<li>쉽게 말해, API 형태로 호출되어 클라우드에서 실행되는 AI</li>
<li>ex. 파파고</li>
</ul>
<h2 id="saas-software-as-a-service">SaaS: Software as a Service</h2>
<ul>
<li>모든 애플리케이션은 제공업체가 관리하며 웹 브라우져를 통해 제공</li>
<li>Gmail, Notion, Google Drive</li>
</ul>
<p>⇒ 특성상 SaaS는 B2C, IaaS와 PaaS는 B2B의 경향성을 보임</p>
<ul>
<li>예외<ul>
<li>Notion for Business: B2B SaaS</li>
<li>AWS의 개인적 사용: B2C IaaS/PaaS</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/zero-black/post/3a3b733a-89d0-4df4-820b-d7d848d24a60/image.png" alt=""></p>
<h1 id="serverless">Serverless</h1>
<ul>
<li>개발자가 서버를 관리할 필요 없이 애플리케이션을 빌드 &amp; 실행할 수 있도록 하는 개발 모델</li>
<li>PaaS의 일종으로 볼 수 있음</li>
<li>서버가 없다는 뜻이 아니라, 서버 관리가 필요 없다는 뜻<ul>
<li>서버의 배포, 확장, 유지보수를 Service Provider가 대신함</li>
</ul>
</li>
</ul>
<h2 id="cf-baas-backend-as-a-service">cf. BaaS: Backend-as-a-Service</h2>
<ul>
<li>인증 서비스와 추가 암호화, 등 제 3사 서비스와 애플리케이션에 액세스 할 수 있도록 함</li>
<li>일반적으로 API를 통해 호출</li>
<li>단점<ul>
<li>제공되는 기능을 사용하는 것으로, 커스터마이징이 어려움</li>
<li>플랫폼에 종속성이 높아짐</li>
</ul>
</li>
<li>ex. Firebase Cloud Messaging, Firebase Authentication, AWS Amplify</li>
</ul>
<h2 id="faas-function-as-a-service-←-일반적으로-칭하는-서버리스">FaaS: Function-as-a-Service ← 일반적으로 칭하는 서버리스</h2>
<ul>
<li>애플리케이션의 기능을 작은 단위로 나누어 각 기능을 클라우드에서 독립적으로 실행</li>
<li>트래픽이 많을 때는 자동으로 확장되어 서버 자원을 동적으로 할당 (로드 밸런싱)</li>
<li>서버를 상시 운영하지 않고, 요청이 있는 경우(이벤트 기반)에만 리소스 할당<ul>
<li>실제로 코드가 실행된 시간에 대해서만 과금 → 자원의 효율적 사용</li>
<li><strong>Cold Start 문제: 함수가 호출될 때, 새로운 서버를 할당하기 위한 지연시간 발생</strong></li>
<li>Stateless: 세션이나 상태 정보를 저장할 필요가 있으면 외부 저장소와의 연동이 필요함</li>
<li>처리 시간이 긴 작업에는 부적합<ul>
<li>ex. AWS lambda - 15분</li>
</ul>
</li>
</ul>
</li>
<li>ex. AWS Lambda, Azure Functions, Google Cloud Function</li>
</ul>
<h1 id="hybrid-cloud--multi-cloud">Hybrid Cloud &amp; Multi Cloud</h1>
<p><img src="https://velog.velcdn.com/images/zero-black/post/16c7975f-d0e4-4076-9a17-f1871092855d/image.png" alt=""></p>
<h3 id="public-cloud">public cloud</h3>
<ul>
<li>일반 대중 &amp; 기업에게 공개된 클라우드 서비스</li>
<li>ex. AWS, MS Azure, GCP, NHN</li>
</ul>
<h3 id="private-cloud">private cloud</h3>
<ul>
<li>조직 내부에서 운영되는 클라우드 인프라</li>
<li>가상화 기술을 사용하여 구축할 수 있음</li>
<li>보안이 중요한 경우 사용</li>
</ul>
<h3 id="hybrid-cloud">Hybrid Cloud</h3>
<ul>
<li>public cloud &amp; private cloud를 병행해서 사용하고, 클라우드 간 데이터와 애플리케이션 공유</li>
<li>민감한 데이터는 on-premise에 저장, 그 외는 클라우드를 활용하여 비용 절약</li>
</ul>
<h3 id="multi-cloud">Multi cloud</h3>
<ul>
<li>여러 클라우드 서비스 제공자를 동시에 사용하는 전략</li>
<li>ex. AWS와 Azure를 병렬적으로 사용<ul>
<li>비용 최적화</li>
<li>안정성 증가</li>
<li>플랫폼에 대한 의존도 낮춤</li>
</ul>
</li>
</ul>
<h3 id="finops">FinOps</h3>
<ul>
<li>Finance + Operation</li>
<li>클라우드 환경의 재무 관리를 최적화하여 비즈니스 가치 극대화</li>
</ul>
<h1 id="엣지-컴퓨팅">엣지 컴퓨팅</h1>
<ul>
<li>데이터 소스의 물리적인 위치, 혹은 근처에서 컴퓨팅을 수행하는 것<ul>
<li>지연 시간 감소</li>
<li>대역폭의 효율적인 이용 → 중요한 데이터만 중앙서버로</li>
<li>데이터 보안 강화</li>
</ul>
</li>
<li>포그 컴퓨팅이라고도 함</li>
<li>ex. 연합학습</li>
</ul>
<h1 id="reference">Reference</h1>
<p><a href="https://www.redhat.com/ko/topics/cloud-computing/iaas-vs-paas-vs-saas">https://www.redhat.com/ko/topics/cloud-computing/iaas-vs-paas-vs-saas</a>
<a href="https://www.redhat.com/ko/topics/cloud-native-apps/what-is-serverless">https://www.redhat.com/ko/topics/cloud-native-apps/what-is-serverless</a>
<a href="https://www.samsungsds.com/kr/cloud-glossary/cloud-computing.html">https://www.samsungsds.com/kr/cloud-glossary/cloud-computing.html</a>
<a href="https://www.etnews.com/20250114000263">https://www.etnews.com/20250114000263</a>
<a href="https://enterprise.kt.com/bt/P_BT_TI_VW_001.do?bbsId=2735&amp;bbsTP=A">https://enterprise.kt.com/bt/P_BT_TI_VW_001.do?bbsId=2735&amp;bbsTP=A</a>
<a href="https://news.mt.co.kr/mtview.php?no=2023111414345763661">https://news.mt.co.kr/mtview.php?no=2023111414345763661</a>
<a href="https://www.samsungsds.com/kr/insights/the-future-of-ai-as-a-service.html">https://www.samsungsds.com/kr/insights/the-future-of-ai-as-a-service.html</a>
<a href="https://learn.microsoft.com/ko-kr/cloud-computing/finops/overview?utm_source=chatgpt.com">https://learn.microsoft.com/ko-kr/cloud-computing/finops/overview?utm_source=chatgpt.com</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로보틱스 자료조사]]></title>
            <link>https://velog.io/@zero-black/%EB%A1%9C%EB%B3%B4%ED%8B%B1%EC%8A%A4-%EC%9E%90%EB%A3%8C%EC%A1%B0%EC%82%AC</link>
            <guid>https://velog.io/@zero-black/%EB%A1%9C%EB%B3%B4%ED%8B%B1%EC%8A%A4-%EC%9E%90%EB%A3%8C%EC%A1%B0%EC%82%AC</guid>
            <pubDate>Sun, 26 Jan 2025 13:59:34 GMT</pubDate>
            <description><![CDATA[<h3 id="산업-로봇과-비산업-로봇">산업 로봇과 비산업 로봇</h3>
<h4 id="산업-로봇">산업 로봇</h4>
<ul>
<li>제조업, 물류, 조립 라인에서 활용되어 <strong>반복적인 작업</strong>을 <strong>자동화</strong>하는데 초점</li>
</ul>
<h4 id="비산업-로봇">비산업 로봇</h4>
<ul>
<li>서비스, 의료, 가정, 교육 등에서 활용되며,특정한(Specific)한 작업보다는 <strong>유연성</strong>을 필요로함</li>
<li>ex. 로봇 청소기, 수술로봇, 서빙 로봇</li>
</ul>
<h3 id="sw와의-결합">SW와의 결합</h3>
<ul>
<li>ROS</li>
<li>AI / ML</li>
<li>Computer Vision</li>
<li>Cloud</li>
<li>Edge Computing</li>
</ul>
<p>⇒ 결국 로봇의 자유도를 크게 증가시키는 역할을 함</p>
<h4 id="ros-robot-os">ROS (Robot OS)</h4>
<ul>
<li>로봇 소프트웨어 개발을 위한 오픈소스 프레임워크<ul>
<li>라이브러리 제공</li>
</ul>
</li>
<li>로봇 제어에 필요한 3가지 요소들을 통합하여 관리할 수 있도록 함<ul>
<li>actuators: 실제로 로봇이 움직이도록 하는 장치</li>
<li>sensors: 외부 환경을 인식 &amp; 데이터 수집</li>
<li>control system: sensor를 통해 수집한 데이터로 actuator를 조정하는 역할을 함</li>
</ul>
</li>
</ul>
<h4 id="robot-with-ai">Robot with AI</h4>
<ul>
<li>기본적으로 센서로 파악한 데이터로 결정을 내리는 과정에서 사용됨</li>
<li>강화학습<ul>
<li>Agent가 State(상태)에 따라 특정 Action을 취하고, 이에 따라 보상을 받는 방식의 학습 방법</li>
<li>학습 과정에서 보상을 최대화하도록 최적화된다.</li>
</ul>
</li>
<li>ex. 자율 주행, 로봇 팔 등</li>
</ul>
<h4 id="computer-vision">Computer Vision</h4>
<ul>
<li>단순한 센서(빛, 열 등)를 통한 인지가 아니라, 카메라로 현재 상태를 인지하고, 이에 따른 행동을 할 수 있도록 함</li>
<li>물체 인식, 장애물 회피, 경로 계획 등이 가능함</li>
<li>SLAM(Simultaneous Localization and Mapping)
  <img src="https://velog.velcdn.com/images/zero-black/post/aa9240ba-79cf-4b6a-afb4-b8657d4cc45b/image.png" alt=""><ul>
<li>FE: 센서에 의존해서 데이터를 수집, Feature 추출</li>
<li>BE: Map estimation</li>
</ul>
</li>
<li>딥러닝과 결합하여 <strong>사람처럼</strong> 판단하고, 행동하는 것이 가능하도록 함</li>
</ul>
<h4 id="iortinternet-of-robotic-things">IoRT(Internet of Robotic things)</h4>
<p><img src="https://velog.velcdn.com/images/zero-black/post/49c3b84c-f521-4e71-a655-91b31451df4c/image.png" alt=""></p>
<ul>
<li>IoT: 사물 인터넷<ul>
<li>인터넷을 통해 연결된 물리적 장치들</li>
<li>각 장치가 센서를 통해 환경에 대한 정보를 습득하고, 인터넷을 통해 데이터를 주고 받고, 실제로 행동으로까지 이어짐<ul>
<li>ex. 온도 조절기, 스마트 잠금 장치</li>
</ul>
</li>
</ul>
</li>
<li>IoRT는 이를 로봇에 적용하여, 로봇들이 인터넷을 통해 연결되어 소통하고 의사결정을 내림<ul>
<li>결국 다른 로봇과 인터넷을 통해 협력하는 것.</li>
</ul>
</li>
<li>클라우드 컴퓨팅, 엣지 컴퓨팅과의 결합</li>
<li>ex. 스마트 물류 시스템, 스마트 팩토리</li>
</ul>
<h4 id="robot-as-a-service">Robot as a Service</h4>
<ul>
<li>로봇의 하드웨어와 소프트웨어를 구독 방식으로 제공 → 초기 비용 절감 &amp; 유연성 확장</li>
<li>cf. IaaS(Infrastructure as a Service)<ul>
<li>서버, 스토리지, 네트워크 등 인프라를 서비스로 제공하는 모델<ul>
<li>ex. AWS</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="cobot-collaborative-robot">Cobot (Collaborative Robot)</h4>
<ul>
<li>인간과 협력하여 Task를 수행하는 로봇</li>
<li>기존 산업 로봇과의 차이점<ul>
<li>여러가지 작업을 다양한 환경에서도 할 수 있음</li>
<li>인간과 상호작용함</li>
</ul>
</li>
<li>ex. 조립 라인, 수술 로봇</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ubuntu 파티션 마운트 (홈 파티션 새로 마운트하기)]]></title>
            <link>https://velog.io/@zero-black/Ubuntu-%ED%8C%8C%ED%8B%B0%EC%85%98-%EB%A7%88%EC%9A%B4%ED%8A%B8-%ED%99%88-%ED%8C%8C%ED%8B%B0%EC%85%98-%EC%83%88%EB%A1%9C-%EB%A7%88%EC%9A%B4%ED%8A%B8%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@zero-black/Ubuntu-%ED%8C%8C%ED%8B%B0%EC%85%98-%EB%A7%88%EC%9A%B4%ED%8A%B8-%ED%99%88-%ED%8C%8C%ED%8B%B0%EC%85%98-%EC%83%88%EB%A1%9C-%EB%A7%88%EC%9A%B4%ED%8A%B8%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 19 Jan 2025 10:07:11 GMT</pubDate>
            <description><![CDATA[<p>Ubuntu 20.04를 새로 설치하면서 파티션을 나눴는데, 문제가 생겼다.</p>
<p>설치 중 문제가 생겨서 파티션을 전부 밀고 다시 설정해주었다.
500GB SSD와 2TB HDD가 있는 상황이었고, 초기 파티션 설정은 아래와 같다.</p>
<ul>
<li>SSD<ul>
<li>EFI 파티션 (ESP) - 부팅<ul>
<li>크기: 500MB</li>
<li>파일 시스템: FAT32</li>
</ul>
</li>
<li>루트 파티션 (<code>/</code>) - 운영체제 기본 파일<ul>
<li>크기: 100GB</li>
<li>파일 시스템: EXT4</li>
</ul>
</li>
<li>스왑 파티션 - 메모리 부족시 대체<ul>
<li>크기: 16GB</li>
</ul>
</li>
<li>홈 파티션 (<code>/home</code>)<ul>
<li>크기: 350GB</li>
<li>파일 시스템: EXT4</li>
</ul>
</li>
<li>남은 공간은 freespace</li>
</ul>
</li>
<li>HDD<ul>
<li>데이터 저장용 파티션 (<code>/mnt/data</code>)<ul>
<li>크기: 1.8TB</li>
<li>파일: EXT4</li>
</ul>
</li>
<li>남은 공간 freespace</li>
</ul>
</li>
</ul>
<p>문제는... 한참 개발환경을 셋업하던 중 문제가 생겼다.
disk space 에러가 나서 봤더니, 홈 파티션(<code>/home</code>)과 데이터 저장용(<code>/mnt/data</code>) 파티션이 제대로 마운트 되지 않고 있어서 홈 이하에 저장한 데이터들이 실제로는 루트(<code>/</code>) 용량에 들어가고 있었다...</p>
<p>디스크 조회 명령어는 아래와 같다.</p>
<pre><code class="language-bash">df -h</code></pre>
<p>여기서 1차 실수를 하는데...</p>
<p>마운트가 안되어 있으니 홈에 마운트를 하면 되겠네? (빨리 집에 가고 싶었음)
이렇게 하면 /home이 애매하게 복제되다가 반절은 원래(루트 밑), 나머지는 새로 마운트 한 곳에 들어가는 식으로 꼬이게 될 수 있으니 주의하자...</p>
<p>아래 과정을 간단히 요약하면</p>
<ol>
<li>임시 폴더에 파티션 마운트</li>
<li>기존 /home 폴더 임시 폴더로 옮기기</li>
<li>기존 /home 폴더 이름 바꾸기(/home-tmp)</li>
<li>/home에 파티션 마운트</li>
<li>문제 없으면 /home-tmp 삭제</li>
</ol>
<p>이다. 어렵지 않으니 그렇게 쫄지는 않아도 된다.</p>
<h3 id="디스크-이름-확인">디스크 이름 확인</h3>
<pre><code class="language-bash">sudo fdisk -l</code></pre>
<p>제 경우에는 HDD는 <code>/dev/sdaX</code>, sdd는 파티션이 나눠져 있어서 그런지 <code>/dev/nvme0n1pX</code>의 이름으로 나오고 있었다.</p>
<h3 id="임시-폴더에-마운트">임시 폴더에 마운트</h3>
<pre><code class="language-bash">sudo mount &lt;디스크 이름&gt; &lt;임시 폴더&gt;</code></pre>
<p>예를 들어 <code>sudo mount /dev/sda1 /mnt/tmp</code></p>
<h3 id="home-폴더-옮기기">/home 폴더 옮기기</h3>
<p><code>/home</code>에 들어가서 실행하면 된다.</p>
<pre><code class="language-bash">sudo cp -rp * &lt;임시 폴더&gt;</code></pre>
<p>ex.<code>sudo cp -rp * /mnt/tmp</code></p>
<p><code>-r</code>: 하위 폴더까지 모조리 복사
<code>-p</code>: 파일 권한 유지 (안하면 전부 루트 소유로 복제된다)</p>
<h3 id="기존-home-폴더-이름-바꾸기">기존 Home 폴더 이름 바꾸기</h3>
<pre><code class="language-bash">mv /home /home-tmp</code></pre>
<h3 id="임시-마운트-해제-및-home에-마운트">임시 마운트 해제 및 Home에 마운트</h3>
<pre><code class="language-bash">sudo umount &lt;임시폴더&gt;                //마운트 해제
sudo mount &lt;디스크 이름&gt; /home            //홈에 마운트</code></pre>
<p>ex.<code>sudo umount /mnt/tmp</code>
unmount 아니고 <strong>umount</strong> 이다</p>
<p>이제 작업은 끝났다.
이상 없으면 아까 이름 바꿔둔 홈 홀더 삭제하면 된다.</p>
<pre><code class="language-bash">sudo rm -rf /home-tmp</code></pre>
<h3 id="자동-마운트-설정">자동 마운트 설정</h3>
<p>이 짓을 또 하지 않으려면 마운트가 항상 되어야 한다.</p>
<p><code>/etc/fstab</code> 열어보면 기존 마운트 정보(아마 부팅 디스크...)가 있다. 여기에 추가해주면 시스템이 켜질 때 자동으로 마운트 된다.</p>
<p>여기에 똑같이 덧붙여주면 되는데, 보면 UUID가 있을 것이다.
장치 이름만 써도 되긴 하는데, UUID가 혹시 장치 이름이 바뀌는 경우에도 안전하니 UUID로 써보자.</p>
<p>장치 UUID를 조회하는 방법은</p>
<pre><code class="language-bash">sudo blkid</code></pre>
<p>이제 UUID를 찾아서 vim, nano 등으로 <code>/etc/fstab</code> 열어서 마지막 줄에 추가해주면 된다.</p>
<pre><code class="language-bash">UUID=&lt;장치 UUID&gt;   /home   ext4   defaults   0   2</code></pre>
<p>이제 재부팅 하고도 마운트가 잘 되어 있는지 확인해주면 끝!!!</p>
<pre><code>df- h</code></pre><h3 id="reference">Reference</h3>
<p><a href="https://m.blog.naver.com/dohyu/222028942436">https://m.blog.naver.com/dohyu/222028942436</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unity + Photon Pun2 원격 멀티 플레이(1)]]></title>
            <link>https://velog.io/@zero-black/Unity-Photon-Pun2-%EC%9B%90%EA%B2%A9-%EB%A9%80%ED%8B%B0-%ED%94%8C%EB%A0%88%EC%9D%B4</link>
            <guid>https://velog.io/@zero-black/Unity-Photon-Pun2-%EC%9B%90%EA%B2%A9-%EB%A9%80%ED%8B%B0-%ED%94%8C%EB%A0%88%EC%9D%B4</guid>
            <pubDate>Sun, 05 Jan 2025 15:01:16 GMT</pubDate>
            <description><![CDATA[<p>해커톤 때 개발 했던 게임이 하나의 PC로 진행하는 2인 게임이었는데, 살짝 욕심이 생겨서 팀원들에게 허락을 구하고 원격 멀티가 가능하도록 수정하는 작업을 하고 있다.</p>
<p>우선 WebGL 빌드가 가능해야 하고,
이미 완성된 게임이기 때문에 간단하게(?) 붙일 수 있는 라이브러리를 찾았다.</p>
<p>크게 Mirror와 Photon이 있는데, 예제가 많고 호스팅 서비스를 제공하는 Photon을 사용했다.</p>
<p>Photon에서 무료로 제공하는 서버는 동시 접속자(CCU) 최대 20명까지를 허용하고, 그 이상은 유료 요금제를 사용해야 한다. 우선 설치에 앞서 프로젝트를 등록해두면 좋다. (어차피 세팅에 AppId가 필요하기 때문에...)</p>
<h3 id="프로젝트-만들기">프로젝트 만들기</h3>
<p><a href="https://dashboard.photonengine.com/app/create">Create New Application</a>에서 새로운 앱을 만들 수 있다.
<img src="https://velog.velcdn.com/images/zero-black/post/1aeeec3a-7e0a-4096-93b0-1a71b199c671/image.png" alt=""></p>
<blockquote>
<p>Type: MultiPlayer Game
SDK: Pun</p>
</blockquote>
<p>설정은 간단하다.
이제 대시보드에서 AppId를 찾을 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/zero-black/post/581b55c9-7b6e-4f5d-95bb-377c12434751/image.png" alt=""></p>
<p>우선 세팅은 Pun2 패키지를 설치해야 한다.
설치는 그냥 <a href="https://assetstore.unity.com/packages/tools/network/pun-2-free-119922?locale=ko-KR">Unity Asset Store</a>에서 &quot;내 에셋에 추가하기&quot;로 추가해주면 된다.</p>
<p><img src="https://velog.velcdn.com/images/zero-black/post/bfa661ca-3f0d-4257-a38a-0a347888d1b1/image.png" alt=""></p>
<p>설치하고 나면 세팅 UI가 뜨는데 AppId를 추가해주어야 한다.
<img src="https://velog.velcdn.com/images/zero-black/post/c07551a1-1aef-40ad-b912-18e543b1cbd4/image.png" alt=""></p>
<p>이후 셋업은 자동으로 완료된다!</p>
<h3 id="instantiate">Instantiate</h3>
<p>공유되는 요소는 Instantiate를 Photon에서 해주어야 한다.</p>
<pre><code class="language-cs">PhotonNetwork.Instantiate(&quot;Prefab경로&quot;, Vector3.zero, Quaternion.identity);</code></pre>
<p>특이한 점은 Resource 폴더에 배치해서 이름(경로)으로 불러온다는 점이다.
기본적으로 PhotonView 컴포넌트를 프리팹에 배치해주면 위치요소는 모두에게 공유된다.</p>
<h3 id="reference">reference</h3>
<p><a href="https://doc.photonengine.com/pun/current/getting-started/initial-setup">Photon 공식 문서</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unity + Firebase로 랭킹 시스템 구현하기 (Feat. REST API)]]></title>
            <link>https://velog.io/@zero-black/Unity-Firebase%EB%A1%9C-%EB%9E%AD%ED%82%B9-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-Feat.-REST-API</link>
            <guid>https://velog.io/@zero-black/Unity-Firebase%EB%A1%9C-%EB%9E%AD%ED%82%B9-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-Feat.-REST-API</guid>
            <pubDate>Fri, 27 Dec 2024 18:03:15 GMT</pubDate>
            <description><![CDATA[<p>학기 중 진행했던 게임 해커톤에 Firebase RealtimeDB를 사용해서 랭킹 시스템을 구현해보았다.
사실 해커톤 중에 시간이 남아서 구현을 했었고 완성을 했는데... 빌드 환경을 고려하지 못해서 실패하였다.</p>
<h3 id="환경">환경</h3>
<blockquote>
<p>Unity 2021.3.45f1
플랫폼: PC(WebGL)</p>
</blockquote>
<p>개발 관련 포스팅 할 때 항상 환경부터 써놓고 시작하는데, 이번에도 이게 가장 문제였다.
WebGL 빌드!!!!!
요즘 사람들은 다운로드를 잘 안한다. (특히 PC게임은...)
해커톤에서 만든 게임을 그래도 유저에게 노출시키려면 WebGL 빌드가 최선인 것 같다.</p>
<p>해커톤 당시, 피곤함과 촉박함 때문에 문서를 제대로 읽지 못한 것이 화를 불러왔다.</p>
<p><a href="https://firebase.google.com/docs/unity/setup?hl=ko">Unity firebase SDK 공식 문서</a>를 보자.
<img src="https://velog.velcdn.com/images/zero-black/post/35211e09-083d-46e8-a378-f9194c280a70/image.png" alt=""></p>
<p>iOS, tvOS, Android...
어디에도 web을 지원한다는 말이 없다.</p>
<p>당시 WebGL 환경에서 Firebase를 통합한 아주 자세한 블로그 글을 발견해서, 아무 생각 없이 그대로 진행한 것이다.
구현을 마치고 빌드를 하니 당연히 실패가 떴다. (애초에 Firebase SDK는 웹 빌드가 안되니 당연한 것...)</p>
<p>다시 제대로 읽어보니 참고한 블로그에서도 js코드를 직접 써서 우회 작업을 해주고 있었다.
<del>결국 해커톤은 랭킹 없이 끝나버렸다</del></p>
<h2 id="firebase-api">Firebase API</h2>
<p>해커톤이 끝나고 다시 생각해봤는데, 애시당초 SDK를 꼭 쓸 필요가 없다.
우리에겐 REST API가 있으니까...!</p>
<p>Unity에서 web request를 보내려면 여러가지 방법이 있는데, 나는 UnityWebRequest 패키지를 사용했다.
기본적으로 포함되어 있기 때문에 따로 설치할 필요는 없다.</p>
<p>기본적으로 코루틴 방식으로 되어 있는데, 우선 데이터를 받을 때도 그렇고 async/await 방식이 컨트롤이 쉬울 것 같아서 Async 방식으로 감싸주는 클래스를 새로 만들었다. (Thanks to GPT...)</p>
<h3 id="unitywebrequestasync">UnityWebRequestAsync</h3>
<pre><code class="language-cs">using UnityEngine.Networking;
using System.Threading.Tasks;

public static class UnityWebRequestAsync
{
    public static Task&lt;UnityWebRequest&gt; SendWebRequestAsync(UnityWebRequest request)
    {
        var tcs = new TaskCompletionSource&lt;UnityWebRequest&gt;();

        request.SendWebRequest().completed += operation =&gt;
        {
            if (request.result == UnityWebRequest.Result.Success)
            {
                tcs.SetResult(request);
            }
            else
            {
                tcs.SetException(new UnityWebRequestException(request));
            }
        };

        return tcs.Task;
    }

    public class UnityWebRequestException : System.Exception
    {
        public UnityWebRequest Request { get; }

        public UnityWebRequestException(UnityWebRequest request)
            : base($&quot;UnityWebRequest Error: {request.error}&quot;)
        {
            Request = request;
        }
    }
}</code></pre>
<h3 id="요청-보내기">요청 보내기</h3>
<p>사용은 아래와 같이 하면 된다.</p>
<ul>
<li><p><code>GET</code></p>
<pre><code class="language-cs">UnityWebRequest request = UnityWebRequest.Get(url);
UnityWebRequest response = await UnityWebRequestAsync.SendWebRequestAsync(request);</code></pre>
</li>
<li><p><code>POST</code></p>
<pre><code class="language-cs">UnityWebRequest request = new UnityWebRequest(url, &quot;POST&quot;);
byte[] bodyRaw = Encoding.UTF8.GetBytes(data.ToJson());
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();

request.SetRequestHeader(&quot;Content-Type&quot;, &quot;application/json&quot;);
UnityWebRequest response = await UnityWebRequestAsync.SendWebRequestAsync(request);</code></pre>
<p>POST는 GET보다 조금 복잡하게 짰는데, 이유는 <code>application/json</code> 타입을 보내기 위해서이다.
패키지 내부를 까보면 아는데, <code>UnityWebRequest.Post(url, json)</code> 방식을 사용하면 contentType이 <code>application/x-www-form-urlencoded</code> 로 되어 있다. 그래서 그냥 직접 작성해줬다.</p>
</li>
</ul>
<h3 id="응답-받기">응답 받기</h3>
<p>여기서도 은근히 시간을 많이 잡아먹었다.
Firebase RealtimeDB는 Key-Value 방식인데, 유니티에서 기본으로 사용되는 JsonUtility가 Dictionary를 처리하지 못하기 때문이다.
다음은 랭킹을 시간순으로 size만큼 가져오는 쿼리이다.</p>
<pre><code class="language-cs">UnityWebRequest request = UnityWebRequest.Get($&quot;{GetURI()}&amp;orderBy=\&quot;time\&quot;&amp;limitToFirst={size}&quot;);
UnityWebRequest response = await UnityWebRequestAsync.SendWebRequestAsync(request);</code></pre>
<p>응답을 <code>response.downloadHandler.text</code>에서 받아올 수 있는데, JsonUtility로는 아무리 용을 써도 안된다.
직접 파싱하는 로직을 구현해도 되지만, <code>JsonConvertor</code>를 사용하면 간단히 해결된다.</p>
<p>우선 샘플 데이터는 아래와 같다.</p>
<pre><code class="language-json">{
    &quot;ranking&quot;:{
        &quot;-OEt6lu9MkTkc35bEi6b&quot;:{
            &quot;name&quot;: &quot;XXX&quot;,
              &quot;time&quot;: 3000,
        }
    }
}</code></pre>
<p>이 랭킹에 해당하는 클래스를 짜주어야 한다.</p>
<pre><code class="language-cs">[System.Serializable]
public class Ranking
{
    public string name;
    public int time;

    public Ranking() { }

    public string ToJson() // POST 요청에서 Json으로 만들 때 사용
    {
        return JsonUtility.ToJson(this);
    }
}</code></pre>
<p>이후에 파싱은 간단하다.</p>
<pre><code>var result = JsonConvert.DeserializeObject&lt;Dictionary&lt;string, Ranking&gt;&gt;(response.downloadHandler.text);</code></pre><p>Json을 dictionary로 받은 것이다. 이후에는 result.Values로 접근하면 랭킹 목록을 얻을 수 있다.
이 때, result 내부에서는 정렬된 결과를 받지 못하므로 한번 더 정렬을 거쳐서 내보내야 한다.</p>
<p>만약 제대로 된 응답이 오지 않고 400 Bad Request가 뜬다면, 쿼리가 맞는지 점검해보자.
<a href="https://firebase.google.com/docs/database/rest/retrieve-data#section-rest-filtering">Firebase 공식문서 - Filtering Data</a>
우선, OrderBy는 다음 다섯개 중 하나와 반드시 같이 결합해서 사용해야 한다.</p>
<ul>
<li><code>limitToFirst</code></li>
<li><code>limitToLast</code></li>
<li><code>startAt</code></li>
<li><code>endAt</code></li>
<li><code>equalTo</code>
그냥 orderBy만 설정해서 보내면 400에러가 난다.</li>
</ul>
<p>다 설정했는데도 제대로 된 응답이 오지 않는다면, 인덱스를 설정했는지 봐야 한다.
rule에서 해당 데이터에 &quot;.indexOn&quot;을 설정해주었는지 확인하자!</p>
<h3 id="dispose">Dispose()</h3>
<p>이게 중요한데, 요청이 다 끝나고 나면, 요청 객체를 dispose() 해주어야 한다. 안하면 Memory leak 경고가 뜸...!</p>
<pre><code>request.dispose()</code></pre><p>사실 이게 전부이다.
랭킹이라고 해봤자 랭킹을 올리고(POST), 현재 목록을 받아오기(GET)이 전부이지 않은가...
그러나, 이대로 두면 우선 Firebase에서 위험한 규칙이 있다고 하루에 한번씩 메일이 온다.
DB 주소만 알면 누구나 읽고 쓰기가 가능하기 때문이다. 그러니 약간의 인증을 추가해보자.</p>
<h2 id="anonymous-login">Anonymous Login</h2>
<p>Firebase Authentication에서 지원하는 기능인데, 실제로 유저에게 로그인을 요구하진 않지만, 유저를 일시적으로 구분할 수 있는 익명 로그인이다.
콘솔에서 Authentication 추가해주고, Native provider &gt; Anonymous 선택해주면 설정은 끝이다.
<img src="https://velog.velcdn.com/images/zero-black/post/68c22981-833e-4dce-8796-9f5d5f267e33/image.png" alt=""></p>
<p>로그인도 간단하다.</p>
<pre><code class="language-cs">string AUTH_URL = &quot;https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=&quot;;

UnityWebRequest request = new UnityWebRequest($&quot;{AUTH_URL}{API_KEY}&quot;, &quot;POST&quot;);
var body = new { returnSecureToken = true };
byte[] bodyRaw = Encoding.UTF8.GetBytes(JsonUtility.ToJson(body));
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader(&quot;Content-Type&quot;, &quot;application/json&quot;);
UnityWebRequest response = await UnityWebRequestAsync.SendWebRequestAsync(request);

userToken = JsonConvert.DeserializeObject&lt;Dictionary&lt;string, string&gt;&gt;(response.downloadHandler.text)[&quot;idToken&quot;];
request.Dispose();</code></pre>
<p>이렇게 idToken을 발급 받아서, 이후 요청 Url에 넣어주면 된다.</p>
<pre><code class="language-cs">var url = &quot;{DB_URL}?auth={userToken}&quot;</code></pre>
<h3 id="firebase-realtimedb-rule">Firebase RealtimeDB rule</h3>
<p>이제 인증 관련해서 rule을 설정해주면 된다.
로그인 한 사용자는 전부 허용하려면, <code>auth != null</code>을 추가해주면 된다.</p>
<pre><code class="language-json">{
  &quot;rules&quot;: {
      &quot;DB&quot;: {
        &quot;$variable&quot;:{
            &quot;ranking&quot;:{
              &quot;.indexOn&quot;:[&quot;time&quot;, &quot;name&quot;],
              &quot;.read&quot;: &quot;auth != null&quot;,
              &quot;.write&quot;: &quot;auth != null&quot;,
            }
        }
      }
  }
}</code></pre>
<p><code>$variable</code>은 임의값을 의미한다. 사실상 <code>*</code>과 같은 뜻이라고 보면 된다.
즉, 위 규칙은 <code>DB &gt; * &gt; ranking</code>에 대한 조건을 세팅해준 것이다.
게임 버전이나 밸런스 패치 등 랭킹 판을 바꿔줘야 할 일이 있을 것 같아 이와 같이 세팅했다.
위 규칙은 완벽히 안전한 것이 아니고, 최소한의 조치이니 적당히 참고해서 사용하면 될 듯하다!</p>
<p>참고로, 전역에다가 <code>auth != null</code>을 설정해도 규칙이 안전하지 않다는 메일이 온다.
모든 유저가 읽기 쓰기 권한이 있다는 뜻이니 당연할지도...</p>
<pre><code class="language-json">{
    &quot;rules&quot;:{
      &quot;.read&quot;: &quot;auth != null&quot;,
      &quot;.write&quot;: &quot;auth != null&quot;,
    }
}</code></pre>
<h3 id="reference">Reference</h3>
<p><a href="https://mrbinggrae.tistory.com/category/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8%20%EA%B0%95%EC%A2%8C/%EC%9C%A0%EB%8B%88%ED%8B%B0%20%2B%20%ED%8C%8C%EC%9D%B4%EC%96%B4%EB%B2%A0%EC%9D%B4%EC%8A%A4?page=3">유니티 + 파이어베이스</a>
<a href="https://firebase.google.com/docs/unity/setup?hl=ko">Unity firebase SDK 공식 문서</a>
<a href="https://firebase.google.com/docs/reference/rest/database?hl=ko">Firebase 데이터베이스 REST API</a>
<a href="https://firebase.google.com/docs/database/rest/auth?hl=ko">Firebase REST 요청 인증 </a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSAFY 13기 전공자 지원 후기]]></title>
            <link>https://velog.io/@zero-black/SSAFY-13%EA%B8%B0-%EC%A0%84%EA%B3%B5%EC%9E%90-%EC%A7%80%EC%9B%90-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@zero-black/SSAFY-13%EA%B8%B0-%EC%A0%84%EA%B3%B5%EC%9E%90-%EC%A7%80%EC%9B%90-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sun, 22 Dec 2024 14:40:27 GMT</pubDate>
            <description><![CDATA[<h2 id="13기2025-상반기-시작-모집-일정">13기(2025 상반기 시작) 모집 일정</h2>
<h3 id="0-지원-자격">0. 지원 자격</h3>
<p>반 년마다 모집을 하는데, 기졸업자/졸업 예정자만 지원이 가능하다.
따라서 졸업 유예 신청을 하면 안되니 주의하자... (찾아보니 이것 때문에 탈락 된 사례도 있더라)</p>
<h3 id="1-지원서-작성">1. 지원서 작성</h3>
<blockquote>
<p>기간: 2024.10.21(월) ~ 11.4(월)</p>
</blockquote>
<p>지원서 작성은 별게 없다. 자소서 등 주관식 문항은 없고, 학력/경력/자격증 등 정보 입력과 1~3순위 캠퍼스만 지정해주면 완료 된다. (그닥 중요한 요소는 아닌 것 같지만 어학 성적이 있다면 입력할 수 있으니 미리 준비하는 사람이 있다면 미리 따 두면 좋을지도...?) 참고 이후 지원서 작성 기간이 종료되면 에세이 제출 기간이 따로 있다.</p>
<p>캠퍼스는 1~2순위는 필수 지정, 3순위는 지정하지 않아도 된다.
각 캠퍼스 별로 개설된 트랙이 다르니까 그걸 고려해서 신청하면 된다.</p>
<ul>
<li>코딩(JAVA/Python): 서울, 대전, 광주, 구미, 부울경</li>
<li>임베디드: 서울</li>
<li>임베디드 로봇: 광주</li>
<li>모바일: 구미</li>
<li>데이터: 대전</li>
</ul>
<p>거주 문제도 있고, 1학기 때도 취준에 힘을 쓸 예정이기 때문에 서울캠이 아니면 합격해도 고민이 길 것 같아서 1순위는 서울캠, 2순위는 구미캠을 신청하고 3순위는 비워두었다.</p>
<h3 id="2-에세이-작성">2. 에세이 작성</h3>
<blockquote>
<p>기간: 2024.11.5 (화) ~ 11.16 (토)
<img src="https://velog.velcdn.com/images/zero-black/post/74165ce9-6491-46ed-a02f-6fefa1cf5001/image.png" alt=""></p>
</blockquote>
<p>주제는 추후 공개라고 되어 있지만, 그냥 미리 써도 될 것 같다. 지원서 낼 때 안내된 사항인데, 별반 다를게 없음...
어차피 500자 내외(최대 600자)라서 대단한 내용을 쓰지도 못하니까</p>
<ul>
<li>SSAFY 지원동기</li>
<li>향후 목표</li>
</ul>
<p>두 가지 꼭지로 나누어서 적당히 작성하면 된다.
내가 작성했을 때 가장 유의했던 점은 SSAFY는 기업이 아니고 교육+취업 지원을 위하는 곳이라는 점이다. 많은 블로그에서 언급하고 있듯이 나 잘났다만 강조하면 안된다는 것이다.</p>
<p>개인적으로는 프로젝트 경험도 여러 번 있지만 굳이 구체적인 프로젝트를 설명하지는 않았다. 내가 프로젝트를 접하는 마음가짐, 팀에서 어떤 사람이 되려고 하는가 등에 대해서 통틀어서 설명했고, 솔직하게 취업 지원을 위해 SSAFY가 필요하다고 언급했다.</p>
<p>참고로, 복붙이 막혀있다. <del>(그렇지만 JS를 안다면 뚫을 수 있음)</del></p>
<h3 id="3-sw-적성진단">3. SW 적성진단</h3>
<blockquote>
<p>본 시험: 2024.11.17(일) 12:20</p>
</blockquote>
<p>지원서 제출 이후 메일로 관련 연락이 온다. 오리엔테이션이 있는데 그냥 접속 확인 정도라서 바쁘면 생략해도 될 것 같다. 나는 불안해서 확실히 하고 싶다하면 시험 환경과 동일한 환경에서 체크하면 좋을 듯!</p>
<p>권장 IDE는 Visual Studio/Pycharm/Eclipse 등이 있는데, 그냥 쓰던거 써도 된다. 다만 권장 IDE가 아니라면 시험 중 IDE에 관련해서 문제가 있다면 지원을 받을 수 없다. <del>(근데 솔직히 PS하면서 문제가 생길게 뭐가 있단 말인가)</del></p>
<p>나는 C++로 응시했고, 쓰던 CLion 사용했다. 당연히 문제 없었음!!</p>
<p>문제에 관해서는 자세히 언급하지 않았지만, 평소에 코테 준비했다면 걱정 안해도 된다는 점만 말해두겠다.</p>
<p>자세히 기억은 안나는데 자료형 이슈가 없었다면 깔끔하게 다 푼 것으로 추측된다.</p>
<h3 id="4-인터뷰">4. 인터뷰</h3>
<p><img src="https://velog.velcdn.com/images/zero-black/post/c31c742e-c5fe-4adc-a33b-ae412780333a/image.png" alt=""></p>
<blockquote>
<p>전공자: 2024.12.04(수) ~ 12.05(목)
비전공자: 2024.12.09(월) ~ 12.11(수)
마이스터고: 2024.12.11(수)</p>
</blockquote>
<p>결과는 합격.
이때부터 약간 정보 싸움이 시작되는 것 같다.
추천은 오픈카톡을 검색해서 들어가시라...
스터디를 오픈카톡에서 많이 구하던데, 나는 그냥 혼자 했다.</p>
<p>에세이에 뭐 썼는지 다시 읽어보고, 그간 했던 프로젝트 점검하고 그랬다.
다만 PT면접 준비가 관건이었는데 여러 사정으로 제대로 준비를 못했다.
미리 IT 트렌드 키워드 분석 또는 IT 기사 읽고 정리해보면 도움이 될 것 같다. (감이 안 온다면 스터디 추천)</p>
<p>만약 내일이 면접인데 PT 준비를 안했다면...
마음을 놓으십시오. 어차피 주제가 미리 알려지는 것도 아니고, 발표 시간이 대단히 긴 것도 아니다. 그리고 <em><strong>개인적으로</strong></em> 합불에 엄청 크리티컬 하다고도 생각하지 않는게, 이걸 인상적으로 대단히 잘 해낼 사람이 엄청 많을 것 같지는 않아서다. 그냥 적당히 논리있게, 말이 되는 소리를 하면 평타는 칠 것 같아서 그냥 부담을 내려놓고 차분하게 접근하는게 나을 것 같음(다시 한번 말하자면 개인적인 생각입니다)</p>
<p>인터뷰는 서울캠 멀티캠퍼스에서 진행했는데, 건물 안에서 벌어지는 일은 기밀유지 서약서 비스무리 한 것에 싸인을 한 관계로 생략하겠다.</p>
<p>음 그냥... 나에게 SSAFY가 왜 필요한지 제대로 생각하고 가면 될 듯!
(다시 한번 말하지만 SSAFY는 교육기관이자, 취업지원 프로그램이다.)</p>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/zero-black/post/3a30da98-3d7f-4059-863d-047b285a328f/image.png" alt=""></p>
<p>그렇게 됐다.
1월부터 열심히 살아야겠다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Web3.0이란?]]></title>
            <link>https://velog.io/@zero-black/Web3.0%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@zero-black/Web3.0%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Fri, 06 Dec 2024 09:51:15 GMT</pubDate>
            <description><![CDATA[<h2 id="web의-발전단계">Web의 발전단계</h2>
<h3 id="web10-초기-인터넷-90s--00s"><strong>Web1.0: 초기 인터넷 (90&#39;s ~ 00&#39;s)</strong></h3>
<h4 id="정적-웹web-of-information"><strong>정적 웹(Web of Information)</strong></h4>
<ul>
<li>주로 정적 HTML 페이지로 구성 → 업데이트는 거의 일어나지 않음</li>
<li>정보 제공 목적으로 사용</li>
<li>단순 키워드 검색</li>
<li>유저와의 상호 작용은 거의 없고, 유저는 정보를 소비(읽기)만 하는 방식<ul>
<li>일방적 데이터 흐름</li>
</ul>
</li>
<li>중앙화 된 서버</li>
<li>예시) 초기 포털 사이트, 브라우저 기반 뉴스 사이트</li>
</ul>
<p><img src="https://velog.velcdn.com/images/zero-black/post/b570cb78-8b85-46b9-8820-5a77217a73fd/image.png" alt=""></p>
<p><a href="https://info.cern.ch/hypertext/WWW/TheProject.html">복원된 최초의 웹사이트</a></p>
<h3 id="web20-소셜-웹-00s--현재"><strong>Web2.0: 소셜 웹 (00&#39;s ~ 현재)</strong></h3>
<h4 id="동적-웹web-of-interaction"><strong>동적 웹(Web of Interaction)</strong></h4>
<ul>
<li>유저의 역할 확장: 단순 소비 → 소비(읽기) + 생산(쓰기)<ul>
<li>양방향 플랫폼</li>
<li>사용자 생성 콘텐츠 (User Generated Content)</li>
</ul>
</li>
<li>동적 콘텐츠 → 실시간 업데이트</li>
<li>모바일 디바이스 사용의 급증 → 반응형 웹 기술</li>
<li>집중화된 플랫폼: 데이터와 권한이 대형 기업(구글, 페이스북 등)에 집중</li>
<li>발전된 검색<ul>
<li>실시간 데이터 처리</li>
<li>검색 기록 / 위치(IP) / 관심사 등에 따른 맞춤형 결과 (”알고리즘”)</li>
</ul>
</li>
<li>예시) 유튜브, 트위터, 페이스북, 위키</li>
</ul>
<h4 id="web20의-문제점">Web2.0의 문제점</h4>
<ul>
<li>플랫폼에 종속적 → 개방성 저해<ul>
<li>유저가 만든 정보가 플랫폼(구글, 네이버 등…)에 종속됨</li>
<li>플랫폼은 이를 토대로 수익을 창출 → 정보의 원 소유자에게 수익이 돌아가지 않음</li>
</ul>
</li>
<li>중앙화 된 데이터베이스 → 개인정보의 취약점</li>
</ul>
<h2 id="그래서-web30은">그래서… Web3.0은?</h2>
<p>현재 인터넷의 다음 단계 → 읽기 + 쓰기 + 개인의 데이터 소유까지!!</p>
<blockquote>
</blockquote>
<p><strong>인공지능 &amp; 블록 체인</strong> 기반으로
<strong>사용자 맞춤형 정보</strong>를 제공하고, <strong>데이터 소유를 개인화</strong></p>
<p>즉,
인공지능 → 맞춤형 정보 제공 
블록체인 → 탈중앙화 및 보안 강화</p>
<h3 id="블록체인">블록체인</h3>
<ul>
<li>각 블록들이 서로 연결되어 있는 방식</li>
<li>해킹(위/변조)를 하기 위해서는 전체 네트워크를 해킹해야 함 → 사실상 불가능</li>
<li>정보의 암호화 &amp; 분산화 → 보안성 높임</li>
<li>P2P(Peer to Peer) 방식<h3 id="인공지능">인공지능</h3>
</li>
<li>Semantic Web → 컴퓨터가 웹 페이지의 내용을 이해하고, 개인에 맞는 정보를 제공</li>
<li><em>Hyper-Personalized</em></li>
<li>개인의 영향력 강화 → 플랫폼을 거치지 않고 콘텐츠를 창작/발행/거래/소유</li>
</ul>
<h3 id="현재-운영-중인-서비스-예시">현재 운영 중인 서비스 예시</h3>
<ul>
<li><a href="https://steemit.com/">Steemit</a>: 약간 블로그 + SNS<ul>
<li>콘텐츠 작성자가 관련된 수익을 가져가는 방식</li>
</ul>
</li>
<li>Brave: 프라이버시 강화 &amp; 광고 차단 내장한 브라우저!<ul>
<li>보는 광고의 양을 유저가 설정할 수 있음 → 보상으로 암호화폐 지급</li>
<li>하지만 논란이 있음…<ul>
<li><a href="https://zdnet.co.kr/view/?no=20200609110637">https://zdnet.co.kr/view/?no=20200609110637</a><ul>
<li><a href="https://zdnet.co.kr/view/?no=20200611145909">https://zdnet.co.kr/view/?no=20200611145909</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="web30의-과제">Web3.0의 과제</h3>
<p>주로 현재 블록 체인 기술의 한계에서 발생</p>
<ol>
<li>기술적 과제<ul>
<li>블록체인 네트워크의 처리 속도 &amp; 확장성이 Web2.0에 비해 부족</li>
<li>대규모 사용자의 데이터 처리에서 성능 문제가 발생할 가능성이 높음</li>
</ul>
</li>
<li>규제 &amp; 법적 문제<ul>
<li>탈중앙화 &amp; 익명성 보장 → 불법 활동이 가능해짐</li>
<li>관련해서 법적인 규제가 필요함</li>
</ul>
</li>
<li>보안 문제<ul>
<li>블록체인 자체의 취약점보다는 블록체인을 활용하면서 생길 수 있는 보안적 위협<ul>
<li>스마트 계약의 취약점</li>
<li>블록체인 네트워크의 공격</li>
</ul>
</li>
</ul>
</li>
<li>접근성<ul>
<li>일반 사용자에게는 접근성이 낮음(사용이 어려움)</li>
<li>관련한 교육 및 직관적인 UX를 제공하는 앱 등이 필요</li>
</ul>
</li>
<li>에너지<ul>
<li>블록체인에서 새로운 블록을 받아들일지에 관련된 합의 알고리즘에 많은 에너지가 필요로 함<ul>
<li>ex) Proof of Work</li>
</ul>
</li>
</ul>
</li>
</ol>
<h3 id="reference">Reference</h3>
<ul>
<li>나의 친구 우리의 친구 ChatGPT</li>
<li><a href="https://www.codestates.com/blog/content/web3-0%EC%9D%B4%EB%9E%80-%EC%A0%95%EC%9D%98%EC%99%80-%ED%8A%B9%EC%A7%95-%EC%A0%84%EB%A7%9D">https://www.codestates.com/blog/content/web3-0%EC%9D%B4%EB%9E%80-%EC%A0%95%EC%9D%98%EC%99%80-%ED%8A%B9%EC%A7%95-%EC%A0%84%EB%A7%9D</a></li>
<li><a href="https://enterprise.kt.com/bt/dxstory/1083.do">https://enterprise.kt.com/bt/dxstory/1083.do</a></li>
<li><a href="https://wikidocs.net/250720">https://wikidocs.net/250720</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Flutter에서 악보 그리기 (feat. OSMD)]]></title>
            <link>https://velog.io/@zero-black/Flutter-Flutter%EC%97%90%EC%84%9C-%EC%95%85%EB%B3%B4-%EA%B7%B8%EB%A6%AC%EA%B8%B0-feat.-OSMD</link>
            <guid>https://velog.io/@zero-black/Flutter-Flutter%EC%97%90%EC%84%9C-%EC%95%85%EB%B3%B4-%EA%B7%B8%EB%A6%AC%EA%B8%B0-feat.-OSMD</guid>
            <pubDate>Tue, 21 May 2024 05:19:58 GMT</pubDate>
            <description><![CDATA[<h1 id="intro">intro</h1>
<p>진행하는 프로젝트에서 악보를 띄워야 하는 일이 있는데, 생각보다 악보를 그리는 일이 쉽지가 않았다. 악보를 다루는 포맷은 여러가지가 있는데, 나는 MusicXML을 입력으로 받아서 악보 이미지를 띄워야 했다. <a href="pub.dev">pub.dev</a>를 찾아본 결과 Flutter 패키지 중에는 따로 악보 및 MusicXML을 다루는 패키지가 없었기 때문에, 해결방법은...</p>
<ol>
<li>직접 구현하기</li>
<li>따로 서버를 두어 MusicXML -&gt; 이미지 변환을 받아서 사용하는 방식</li>
</ol>
<p>정도가 있을 듯 했다.</p>
<p>방법1은 정말 만만치가 않다. 생각보다 악보를 구성하는 요소들이 많기 때문에 이걸 다 구현하면 프로젝트 내내 이것만 하게 될 것 같았다. 그럼에도 직접 시도하려는 사람이 있다면 <a href="https://github.com/ghost23/music_notes">ghost23의 데모 앱</a>을 참고해보길 바란다.</p>
<p>만약 방법2로 진행한다면, python의 music21 라이브러리를 사용하면 비교적 간단하게 구현이 가능하다. 하지만 문제는 이 악보를 프롬프팅해야 한다는 것이다. 즉, 곡의 진행에 맞추어 악보를 넘겨주고, 음표에 하이라이트를 쳐서 표시를 해야 되므로 각 음표 별 위치를 파악하는 작업이 필요하다. 결론부터 말하자면, music21로 이게 가능한지는 찾아보지 못했다. 제 3의 방법을 찾았기 때문이다.</p>
<h1 id="opensheetmusicdisplay"><a href="https://github.com/opensheetmusicdisplay/opensheetmusicdisplay">OpenSheetMusicDisplay</a></h1>
<p><img src="https://velog.velcdn.com/images/zero-black/post/ec7c1c3e-4d6a-45e3-84c7-d75b0bb7be62/image.png" alt=""></p>
<blockquote>
<p>OpenSheetMusicDisplay renders MusicXML sheet music in the browser.</p>
</blockquote>
<p>데모 예시 사진에 커서가 있다는 점이 우선 감격적이다. 소개 문구를 보면 <strong>browser</strong>라는 단어를 제외하면 완벽하다.</p>
<p><img src="https://velog.velcdn.com/images/zero-black/post/7f9511b1-abe9-4976-bd40-f0563f38a6f6/image.png" alt="">
브라우저라는 설명에서 짐작할 수 있지만, 해당 라이브러리는 JS(TS)로 작성되었기 때문에 flutter와 상성이 그렇게 좋다고는 할 수 없다. 이쯤에서 아 차라리 React Native를 썼어야 했나 하는 후회가 살짝 들었으나<del>이미늦었어</del>, 뒤에 설명하겠지만 해결할 방법이 없는 것도 아니고, 개인적으로 UI를 구현하는데 React보다 Flutter가 훨씬 편했던 점을 고려하여 유턴은 하지 않기로 했다. 그렇다면 Flutter에서 어떻게 JS를 실행할까.</p>
<h2 id="webview">webview</h2>
<p>해결책은 webview를 띄우는 것이다. 웹뷰란 앱 내에 임베딩 된 브라우저이다. 카카오톡에서 웹페이지 링크를 눌렀을 때 뜨는 새 창 같은 것을 생각해볼 수 있겠다. 웹페이지 로딩에는 JS를 실행하는 브라우저가 필요하다. 보이기에는 그냥 새로운 창이 뜬 것 같지만, 사실은 간이 브라우저(=웹뷰)라고 생각할 수 있겠다. <del>(웹 개발자의 적...)</del></p>
<p>Flutter에서 webview 패키지는 굉장히 <a href="https://pub.dev/packages?q=webview">여러가지</a>가 있는데, 나의 요구조건은 다음과 같았다.</p>
<ol>
<li>localhost를 띄울 수 있을 것.</li>
<li>UX를 위해 headless로 동작이 가능할 것.</li>
</ol>
<p>1.에 대해 보충을 해보자면, OSMD는 그저 라이브러리일 뿐이므로 나는 실제 호스팅 되고 있는 사이트를 렌더링하는 것이 아니다. 웹뷰에 많이 나오는 예제인 유튜브 동영상 렌더링과는 다르다. 실제로 html과 js를 모바일 기기에서 localhost로 띄워서 접근해야 한다.</p>
<p>결론적으로, 공식 문서가 자세하고 점수가 높으며 활발하게 업데이트 되고 있는 <a href="https://pub.dev/packages/flutter_inappwebview">flutter_inappwebview</a>를 선택했다.</p>
<hr>
<h2 id="osmd-코드">OSMD 코드</h2>
<h3 id="1-파일-준비">1. 파일 준비</h3>
<p>이 부분은 OSMD 측에서 제공한 <a href="https://github.com/opensheetmusicdisplay/RawJavascript-usage-example">raw javascript usage example</a>을 참고하여 작성했다.
정확히 해당 레포에서 <a href="https://github.com/opensheetmusicdisplay/RawJavascript-usage-example/blob/master/index.html">index.html</a>과 <a href="https://github.com/opensheetmusicdisplay/RawJavascript-usage-example/blob/master/fileSelectAndLoadOSMD.js">fileSelectAndLoadOSMD.js</a> 두가지 파일을 받아서 시작했다. 추가로 OSMD 배포 파일인 <a href="https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/releases">opensheetmusicdisplay.min.js</a> 를 받아주면 준비는 끝난다.</p>
<p>추가로 테스트용으로 사용할 <a href="https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/blob/develop/demo/BrahWiMeSample.musicxml">MusicXML파일</a>도 받아주자.</p>
<h3 id="2-asset에-추가">2. asset에 추가</h3>
<p>3개의 파일을 assets 폴더에 넣어주면 된다.
개인적으로 나는 assets/web 폴더에 js와 html을 구분해서 넣었지만, 경로만 맞춰주면 어디든 무관하다.
<img src="https://velog.velcdn.com/images/zero-black/post/105c040d-dabb-49c8-bdc0-37b6729bd9a4/image.png" alt="">
index.html에서 script 태그를 사용할 때는 상대 경로로 맞춰주면 된다.</p>
<pre><code class="language-html">  &lt;script src=&quot;./js/opensheetmusicdisplay.min.js&quot;&gt;&lt;/script&gt;
  &lt;script src=&quot;./js/fileSelectAndLoadOSMD.js&quot;&gt;&lt;/script&gt;</code></pre>
<h3 id="3-pubspecyaml에-등록">3. pubspec.yaml에 등록</h3>
<p>flutter에서 이렇게 추가한 asset을 쓰고 싶으면 <code>pubspec.yaml</code> 파일에서 asset의 경로를 추가해주어야 한다.
<img src="https://velog.velcdn.com/images/zero-black/post/8485dc25-b006-4f2b-9179-bb7bef10603c/image.png" alt=""></p>
<p>처음에 폴더를 통째로 넣고 싶어서
assets/web/
assets/web/*
등등 시도해 보았으나 이렇게 하면 파일을 못 찾는거 같아서 무식하지만 그냥 하나씩 써주었다.</p>
<p>이제 준비는 끝났다.</p>
<h2 id="inappwebview">inappwebview</h2>
<p>코드를 부분적으로 설명해보자면,</p>
<h3 id="1-localhost-실행">1. localhost 실행</h3>
<p>index.html이 있는 루트 폴더를 지정해주면 localhost를 실행해준다.
포트를 지정하지 않는다면, 기본적으로 8080으로 배정된다.</p>
<pre><code class="language-dart">  InAppLocalhostServer localhostServer =
        InAppLocalhostServer(documentRoot: &#39;assets/web&#39;);</code></pre>
<h3 id="2-inappwebview-위젯">2. inappwebview 위젯</h3>
<p>이후는 나름 간단하다. url에 localhost:8080을 주면 asset의 index.html이 렌더링된다.</p>
<pre><code class="language-dart">  InAppWebView(
    initialUrlRequest = URLRequest(
      url: WebUri(&#39;http://localhost:8080&#39;),
    ),
  );</code></pre>
<h3 id="3-파일-입력-받기">3. 파일 입력 받기</h3>
<p>파일 입력의 경우, Flutter에서 열어서 webview에 byte 문자열로 넘겨주는 방식을 사용하였다.</p>
<h4 id="1-파일-읽기">1. 파일 읽기</h4>
<p>asset을 사용하는 경우 rootBundle을 사용해서 간단히 읽어오면 된다.</p>
<pre><code class="language-dart">  String fileString = file = await rootBundle.loadString(&#39;assets/music/demo.xml&#39;);</code></pre>
<p>   만약 기기에서 파일을 입력 받는다면, File 객체를 선언해서 경로를 사용해 읽어오면 된다. 이 경우 따로 디코딩을 해주어야 하는데, 파일 인코딩 형식에 따라 다르겠지만 일반적으로는 utf8로 디코드 하면 잘 읽어질 것이다.</p>
<pre><code class="language-dart">  file = File(filePath);
  utf8.decode(await file.readAsBytes());</code></pre>
<h4 id="2-웹뷰에-파일-넘기기">2. 웹뷰에 파일 넘기기</h4>
<p>이제 이 byte string을 웹뷰 쪽에 넘겨야 한다. webview와의 통신은 항상 webview측에서 시작한다고 생각하면 된다. 따라서 handler를 등록하고, 웹뷰의 자바스크립트 파일에서 해당 handler를 호출한다.</p>
<ol>
<li>handler 등록
handler는 InAppWebView 위젯에서 onWebViewCreated에서 등록할 수 있다. onWebViewCreated는 이름대로 웹뷰가 생성되었을 때 자동으로 호출되는 함수이다. 해당 함수는 controller 함께 호출되므로 우리는 이 controller에 handler를 등록한다.</li>
</ol>
<pre><code class="language-dart">InAppWebView(
  ...,
  onWebViewCreated = (controller) async { // 웹뷰 생성되었을 때 호출되는 부분
    controller.addJavaScriptHandler( // handler 등록
        handlerName: &#39;sendFileToOSMD&#39;, // javascript에서 호출할 때 사용되는 handler 이름
        callback: (args) async {
          return {
            &#39;bytes&#39;: fileString, // asset에서 불러오는 경우
            //&#39;bytes&#39;: utf8.decode(await file.readAsBytes()),// File 객체 사용하는 경우
            &#39;name&#39;: file.path
          };
        });
  },
);</code></pre>
<pre><code>위와 같이 &#39;sendFileToOSMD&#39;라는 이름의 handler를 등록하였다.</code></pre><ol start="2">
<li>handler 호출
여기까지 해도 아무 일도 안 일어났을 것이다. handler를 호출하지 않았으니까 당연하다. 이제 아까 asset 파일에 넣어준 파일을 변경해야 한다.</li>
</ol>
<p>2-1. index.html
기존 파일에는 파일을 올릴 수 있는 file input 태그가 있었지만, 이제는 flutter에서 파일을 읽어서 넘기기 때문에 필요가 없다.
따라서 전부 지우고, osmdCanvas를 id로 갖는 div 태그만 남기면 된다.</p>
<pre><code class="language-html">&lt;html&gt;
  &lt;script src=&quot;./js/opensheetmusicdisplay.min.js&quot;&gt;&lt;/script&gt;
  &lt;script src=&quot;./js/fileSelectAndLoadOSMD.js&quot;&gt;&lt;/script&gt;
  &lt;body&gt;
    &lt;div id=&quot;osmdCanvas&quot; style=&quot;width: 1024px&quot; /&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>2-2. fileSelectAndLoadOSMD.js
기존 코드는 input 태그를 통해 파일을 입력받고 해당 이벤트를 처리하는 방식으로 되어 있으므로 현재와 방식이 다르기 때문에 코드 수정이 필요하다.</p>
<p>우선, handler를 호출해서 파일 byte string을 받아와야 한다.</p>
<pre><code class="language-js">window.flutter_inappwebview.callHandler(&quot;sendFileToOSMD&quot;);</code></pre>
<p>이 때, script 상단에서 handler를 호출하면 아직 flutter_inappwebview가 생기기 전이므로 에러가 발생할 수 있다. 따라서, 이 부분이 제대로 설정될 때까지 기다려주어야 한다. inappwebview는 준비가 다 된 경우 <code>flutterInAppWebViewPlatformReady</code> 이벤트를 발생시키므로, 해당 이벤트 발생시 handler를 호출하도록 이벤트 리스너를 추가해준다.</p>
<pre><code class="language-js">  window.addEventListener(&quot;flutterInAppWebViewPlatformReady&quot;, async function (_) {
    // handler 호출
    const inputJson = await window.flutter_inappwebview.callHandler(&quot;sendFileToOSMD&quot;);
    // osmd 시작하기
    startOSMD(inputJson.bytes);
  });</code></pre>
<p>이제 거의 다 되었다! startOSMD 함수만 만들어주면 된다.
기존 코드에서는 파일을 읽고, 파일을 다 읽으면 osmd를 호출하는 방식인데 우리는 이미 파일을 읽은 byte string을 가지고 있으므로 그럴 필요가 없다.</p>
<p>참고로 OSMD의 다양한 옵션은 <a href="https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/blob/develop/src/OpenSheetMusicDisplay/OSMDOptions.ts">OSMDOptions.ts</a>에서 볼 수 있다.</p>
<pre><code class="language-js">  async function startOSMD(fileData) {
    var osmd = new opensheetmusicdisplay.OpenSheetMusicDisplay(&quot;osmdCanvas&quot;, {
      backend: &quot;canvas&quot;,
      resize: true,
      drawFromMeasureNumber: 1,
      drawUpToMeasureNumber: Number.MAX_SAFE_INTEGER,
      drawTitle: false, // 제목 그릴지 여부
      drawPartNames: false, // 악보의 각 파트를 그릴지
    });

    await osmd.load(fileData, &quot;&quot;);
    window.osmd = osmd;
    await osmd.render();
    return osmd;
  }</code></pre>
<p> 끝!</p>
<p><img src="https://velog.velcdn.com/images/zero-black/post/9b6e8b9a-3141-4d87-89d2-57f5f0fa8c6f/image.png" alt=""></p>
<hr>
<h2 id="전체코드">전체코드</h2>
<p>아래는 전체 코드이다.</p>
<ul>
<li><code>lib/main.dart</code><pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;
import &#39;package:flutter/services.dart&#39;;
import &#39;package:flutter_inappwebview/flutter_inappwebview.dart&#39;;
</code></pre>
</li>
</ul>
<p>void main() {
  runApp(const OSMDScreen());
}</p>
<p>class OSMDScreen extends StatefulWidget {
  const OSMDScreen({
    super.key,
  });</p>
<p>  @override
  State<OSMDScreen> createState() =&gt; _OSMDScreenState();
}</p>
<p>class _OSMDScreenState extends State<OSMDScreen> {
  InAppLocalhostServer localhostServer =
      InAppLocalhostServer(documentRoot: &#39;assets/web&#39;);</p>
<p>  late String fileString;
  bool isLoading = true;</p>
<p>  @override
  void initState() {
    super.initState();
    startLocalhost();
  }</p>
<p>  startLocalhost() async {
      // 파일 로드
    fileString = await rootBundle.loadString(&#39;assets/music/demo.xml&#39;);
    // 로컬호스트 시작
    await localhostServer.start();
    setState(() {});
  }</p>
<p>  @override
  void dispose() {
      // 위젯이 dispose되기 전에 localhost를 종료해야 한다.
    localhostServer.close();
    super.dispose();
  }</p>
<p>  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text(&quot;webview로 OSMD 사용하기 예제&quot;)),
        body: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            if (localhostServer.isRunning())
              Expanded(
                child: SizedBox.expand(
                  child: InAppWebView(
                    initialUrlRequest: URLRequest(
                      url: WebUri(&#39;<a href="http://localhost:8080&#39;">http://localhost:8080&#39;</a>),
                    ),
                    onWebViewCreated: (controller) async {
                      controller.addJavaScriptHandler(
                          handlerName: &#39;sendFileToOSMD&#39;,
                          callback: (args) async {
                            return {
                              &#39;bytes&#39;: fileString,
                            };
                          });
                    },
                  ),
                ),
              )
          ],
        ),
      ),
    );
  }
}</p>
<pre><code>
- `index.html`
```html
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;OSMD Raw Javascript Usage Example&lt;/title&gt;
  &lt;/head&gt;
  &lt;style&gt;
    body {
      padding: 0;
      margin: 0 auto;
      align-self: center;
      display: flex;
      justify-content: center;
    }
  &lt;/style&gt;
  &lt;script src=&quot;./js/opensheetmusicdisplay.min.js&quot;&gt;&lt;/script&gt;
  &lt;script src=&quot;./js/fileSelectAndLoadOSMD.js&quot;&gt;&lt;/script&gt;
  &lt;body&gt;
    &lt;div id=&quot;osmdCanvas&quot; style=&quot;width: 1024px&quot; /&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre><ul>
<li><code>fileSelectAndLoadOSMD.js</code></li>
</ul>
<pre><code class="language-js">// 준비가 끝나면
window.addEventListener(&quot;flutterInAppWebViewPlatformReady&quot;, async function (_) {
  const inputJson = await window.flutter_inappwebview.callHandler( // sendFileToOSMD 호출
    &quot;sendFileToOSMD&quot;
  );
  startOSMD(inputJson.bytes);
});

/*
    musicXML byte string을 읽어서 OSMD 시스템에 넘기는 함수
*/
async function startOSMD(fileData) {
  var osmd = new opensheetmusicdisplay.OpenSheetMusicDisplay(&quot;osmdCanvas&quot;, {
    backend: &quot;canvas&quot;,
    resize: true,
    drawFromMeasureNumber: 1,
    drawUpToMeasureNumber: Number.MAX_SAFE_INTEGER,
    drawTitle: false, // 제목 그릴지 여부
    drawPartNames: false, // 악보의 각 파트를 그릴지
  });

  await osmd.load(fileData, &quot;&quot;);
  window.osmd = osmd;
  // 이미지 렌더링
  await osmd.render();
  return osmd;
}</code></pre>
<h4 id="reference">reference</h4>
<p><a href="https://www.w3.org/2021/06/musicxml40/tutorial/notation-basics/">MusicXML</a>
<a href="https://github.com/opensheetmusicdisplay/opensheetmusicdisplay">OSMD - github</a>
<a href="https://github.com/opensheetmusicdisplay/RawJavascript-usage-example">OSMD - raw javascript usage example</a>
<a href="https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/blob/develop/src/OpenSheetMusicDisplay/OSMDOptions.ts">OSMD Options</a>
<a href="https://pub.dev/packages/flutter_inappwebview">flutter_inappwebview</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] 설치 및 개발환경 세팅 - macOS]]></title>
            <link>https://velog.io/@zero-black/Flutter-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85-macOS</link>
            <guid>https://velog.io/@zero-black/Flutter-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85-macOS</guid>
            <pubDate>Fri, 24 Nov 2023 10:30:44 GMT</pubDate>
            <description><![CDATA[<h3 id="why-flutter">why Flutter?</h3>
<p>웹으로 구현했으면 기반 지식이 있으니 훨씬 빨랐을 것 같긴 하지만... 현재 하고 있는 프로젝트 주제를 생각했을 때 사용성이 좋은 쪽은 모바일 어플리케이션이다. 그리고 native는 나중에 나에게 큰 도움이 되지 않을 것 같았다. 그리고 가능하면 cross-platform을 지원하는 프레임워크를 사용하고 싶었기 때문에 React-Native와 Flutter 중에 고민을 했다.</p>
<h3 id="react-native-vs-flutter">React Native vs Flutter</h3>
<p>RN과 flutter를 비교하는 글은 이미 많으니까 길게 적지는 않을 생각이다.
가장 큰 차이점이라고 하면 Flutter는 host OS에서 컴포넌트를 가져다가 쓰는 방식이 아니라, 엔진이 API를 제공하기 때문에 그래픽의 자유도가 높은 장점이 있다. 따라오는 단점으로는 host에서 제공하는 컴포넌트를 쓰는 것이 아니기 때문에 native 앱으로 보이지 않을 수 있다는 점이 있다.</p>
<p>리액트 개발 경험이 있기 때문에 RN을 시작하는게 학습 비용은 작았을 것이라고 생각하지만... 솔직히 구글에서 만든 dart, flutter를 배워보고 싶은 마음이 전부터 있었음 + flutter 개발 경험이 그렇게 좋다고 하길래 궁금한 마음 + 우리 팀 디자이너를 믿는 마음에서 flutter를 선택하게 되었다.</p>
<h3 id="설치">설치</h3>
<p>원래 뭐든 설치가 어려운 법이니까...</p>
<pre><code>OS: MacOS Monterey 12.3
Chip: Apple M1 Pro</code></pre><p>(스포하자면) 결국 OS를 업데이트 했다.</p>
<p><a href="https://docs.flutter.dev/get-started/install">공식 홈페이지</a>에 가면 OS별 설치 방법이 나와 있다.</p>
<p><img src="https://velog.velcdn.com/images/zero-black/post/f1b5a747-8246-4244-b304-14d9ea55030e/image.png" alt="">
안타깝게도 requirements에서는 10.14 이상의 MacOS 버전을 요구하고 있었다.
그러나 나는... flutter하나 배우자고 OS 업데이트를 하는 이 상황을 받아들일 수 없었고(사실 무서움) 일단 킵 고잉 해보기로 했다.</p>
<ol>
<li><p>Flutter SDK 설치
 zip 파일을 받으라는데, 귀찮으므로 이 과정은 homebrew를 통한 설치로 끝냈다.</p>
<pre><code class="language-bash">brew install --cask flutter</code></pre>
<p>환경변수도 따로 세팅할 필요가 없다!
web, 혹은 MacOS Desktop 앱을 만들기 위해서면 여기까지만 해도 된다.</p>
</li>
<li><p>iOS setup
문제는 여기서 발생했다.
Xcode를 깔아줘야 하는데, AppStore에서 설치하려고 하니까, OS version이 14이상 되어야 설치가 가능하다고 하더라...</p>
<p>따라서 web에서 직접 지난 버전을 설치하기로 했다.
<a href="https://developer.apple.com/download/all/?q=XCode">https://developer.apple.com/download/all/?q=XCode</a></p>
<p>어떤 버전을 깔아야 하는지는 <a href="https://xcodereleases.com/">여기</a>를 보면 알 수 있다.
<img src="https://velog.velcdn.com/images/zero-black/post/b40d7259-3627-4ade-aaac-f197157f8bea/image.png" alt=""></p>
<p>내 OS는 12.3 버전이므로 Xcode13.4.1 버전을 깔면 되겠다고 생각했으나,
설치 이후 flutter doctor 명령어를 통해 확인해보니 Xcode 14이상이 필요하다는게 아닌가...
<img src="https://velog.velcdn.com/images/zero-black/post/8153f225-e060-4b04-b47e-e5c68569689d/image.png" alt="">
그리고 Xcode 14 버전 부터는 macOS 12.5 이상에서 설치할 수 있다(...)</p>
<p>결국 OS를 업데이트 해줘야 했다.
그러나 major version 업데이트는 무서우니까, 같은 12버전에서 마이너 업데이트를 진행했다.
<img src="https://velog.velcdn.com/images/zero-black/post/ab439257-66e0-4bda-bf66-f62ecb830aef/image.png" alt="">
<img src="https://velog.velcdn.com/images/zero-black/post/2217b453-fdd5-4d3e-8e59-3dc7b54b9c8b/image.png" alt=""></p>
<p>[About This Mac] &gt; [Software Update...]에 들어가면, 운영체제를 업데이트 할 수 있다. 용감하신 분은 바로 Upgrade Now를 통해 업그레이드 하면 되고, 마이너 업데이트를 위해서는 [More Info...]를 눌러서 진행하면 된다.</p>
<p>이후 Xcode 14.2 버전을 설치했고, flutter doctor에게 통과를 받았다.
<img src="https://velog.velcdn.com/images/zero-black/post/46be0053-94d2-4b4c-93ee-2942b91b2c1c/image.png" alt="">
안드로이드는 당분간은 필요 없을 듯해서 따로 설정해주지는 않았다.</p>
</li>
<li><p>VSCode extension 설정
 VSCode가 설치되지 않았다면, 역시 homebrew를 통해 설치할 수 있다.</p>
<pre><code class="language-bash"> brew install --cask visual-studio-code</code></pre>
<p> 이미 깔려 있다면, 두 가지 extension을 설치하면 더욱 쾌적한 개발을 할 수 있다.</p>
<ol>
<li>Dart
<img src="https://velog.velcdn.com/images/zero-black/post/7d2c09b0-8aab-46a7-a305-7b026fbeae04/image.png" alt=""></li>
<li>Flutter
<img src="https://velog.velcdn.com/images/zero-black/post/535ae6ac-4f25-486d-8e4e-d241d2d3a105/image.png" alt=""></li>
</ol>
</li>
</ol>
<h3 id="프로젝트-생성">프로젝트 생성</h3>
<p>원하는 위치에서 터미널을 열어서,</p>
<pre><code class="language-bash">flutter create &lt;project name&gt;</code></pre>
<p>으로 간단히 flutter 프로젝트를 생성할 수 있다.
혹은 이미 폴더가 있는 상황이라면, 해당 폴더 내에 들어가서 project 이름 대신 <code>.</code>을 넣어서 실행하면 된다.</p>
<pre><code class="language-bash">flutter create .</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Express.js] 미들웨어로 로그 남기기]]></title>
            <link>https://velog.io/@zero-black/Express.js-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4%EB%A1%9C-%EB%A1%9C%EA%B7%B8-%EB%82%A8%EA%B8%B0%EA%B8%B0</link>
            <guid>https://velog.io/@zero-black/Express.js-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4%EB%A1%9C-%EB%A1%9C%EA%B7%B8-%EB%82%A8%EA%B8%B0%EA%B8%B0</guid>
            <pubDate>Tue, 31 Jan 2023 11:29:54 GMT</pubDate>
            <description><![CDATA[<p>express에서는 기본으로 로그를 남겨주지는 않는다. 그러나 미들웨어를 활용하면 간단하게 로그를 남길 수 있다!!</p>
<ol>
<li><p>로깅 미들웨어 작성</p>
<pre><code class="language-js">function loggerMiddleware(req, res, next){
 const log = `[${req.method}] ${req.url}`;
 console.log(log);
 next();
}</code></pre>
</li>
<li><p>app에 적용</p>
<pre><code class="language-js">const express = require(&quot;express&quot;);
const app = express();
// apis
...

// middleware
...
const { logger } = require(&quot;./utils/logger&quot;);

require(&quot;dotenv&quot;).config();
const port = process.env.EXPRESS_PORT;

// 백엔드 로그 남기기
app.use(logger);

...

// 서버 시작
app.listen(port, () =&gt; {
 console.log(`listening ${port}`);
});</code></pre>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Sequelize] transaction]]></title>
            <link>https://velog.io/@zero-black/Sequelize-transaction</link>
            <guid>https://velog.io/@zero-black/Sequelize-transaction</guid>
            <pubDate>Sat, 28 Jan 2023 12:51:32 GMT</pubDate>
            <description><![CDATA[<h3 id="transaction">transaction</h3>
<p>트랜잭션이란, 작업의 단위를 뜻한다. 하나의 트랜잭션이 하나의 작업이 되는 것이다. 일반적으로 하나의 작업은 여러개의 질의어로 이루어져 있다.
예를 들어, 은행에서 계좌이체를 하는 상황을 생각해보자. A가 B에게 1000원을 보내는 상황을 아주 간소화하면</p>
<ol>
<li>A의 잔액 1000원 감소</li>
<li>B의 잔액 1000원 증가</li>
</ol>
<p>위와 같이 두개로 나누어 볼 수 있다. 1과 2는 각각 하나의 SQL문으로 수행될 것이다.</p>
<p>그런데, 만약 서버에 문제가 생겨서 1이 완료된 상황에서 2가 수행되지 않았다면?
1000원은 그냥 증발해버리는 것이다. 보낸 사람은 있지만, 받는 사람은 없는 상황이다.</p>
<p>따라서 이런 경우, 2번을 수행하다가 오류가 생긴다면 1번도 이전 상태로 돌려놓아야 한다. 이것이 트랜잭션의 특징인 원자성이다. 이외에도 일관성, 독립성, 지속성이 있지만 트랜잭션에 대한 설명을 하려던 것은 아니므로... 일단 넘어가 보겠다.</p>
<p>Sequelize에서 트랜잭션은 다음과 같이 선언한다.</p>
<pre><code class="language-js">const t = await sequelize.transaction();</code></pre>
<p>이걸 사용하는 방법이 두 가지 있는데, 하나는 수동(unmanaged transaction)이고, 다른 하나는 자동(managed transaction)이다.</p>
<h3 id="1-unmanaged-transaction">1. unmanaged transaction</h3>
<p>수동도 그렇게 복잡하지는 않다.</p>
<p>수행이 완료된 후 데이터베이스에 반영 될 수 있는 지점에서 커밋을 하면 된다.</p>
<pre><code class="language-js">await t.commit();</code></pre>
<p>그리고 만일 에러가 났을 때 도달할 수 있는 지점에서 롤백을 해주면 된다.</p>
<pre><code class="language-js">await t.rollback();</code></pre>
<p>예시를 보면 더 어렵지 않다.</p>
<pre><code class="language-js">const t = await sequelize.transaction();

try {
  // a 계좌에서 1000원 출금
  await Account.decrement(&quot;balance&quot;,{ 
    transaction: t,
    where: {
      // id = a
    },
    by: 1000,
  });
  // b 계좌에 1000원 입금
  await Account.increment(&quot;balance&quot;,{ 
    transaction: t,
    where: {
      // id = b
    },
    by: 1000,
  });

  // 에러가 나지 않았으므로 커밋
  await t.commit();

} catch (error) {
  // 중간에 에러가 발생, 롤백
  await t.rollback();
}</code></pre>
<h3 id="2-managed-transaction">2. managed transaction</h3>
<p>자동은 위의 프로세스를 자동으로 해주는 것이다. 따라서 커밋과 롤백을 적어주지 않아도 된다. 대신, 작업을 콜백 함수로 sequelize.transaction에 전달해주어야 한다.</p>
<pre><code class="language-js">try {
  await sequelize.transaction(async t =&gt; {
    // a 계좌에서 1000원 출금
    await Account.decrement(&quot;balance&quot;,{ 
      transaction: t,
      where: {
        // id = a
      },
      by: 1000,
    });
    // b 계좌에 1000원 입금
    await Account.increment(&quot;balance&quot;,{ 
      transaction: t,
      where: {
        // id = b
      },
      by: 1000,
    });
  });
} catch (error) {
  console.log(error);
}</code></pre>
<p>그러나 이 경우에도 try-catch문은 써주어야 한다. 만약 catch문으로 에러가 전달된다면 롤백이 진행되고, 에러가 발생하지 않는다면 커밋이 자동으로 이루어진다.</p>
<p>두가지 모두, try-catch문의 위치는 크게 중요하지 않다. 에러가 발생했을 때 catch문에 전달되도록 짜면 된다.</p>
<h3 id="trouble-shooting">trouble shooting</h3>
<ol>
<li><p>트랜잭션을 선언할 때 <code>&quot;s&quot;equelize.transaction()</code>이 소문자임에 주의하자.
데이터베이스와 동기화(<code>sync()</code>) 된 인스턴스를 불러와야 한다!!</p>
<pre><code class="language-js">const Sequelize = require(&quot;sequelize&quot;); // 이게 아니라
const { sequelize } = require(&quot;../path&quot;); // 코드에 따라 위치는 다를 수 있음</code></pre>
</li>
<li><p>만약 catch에 에러가 잡히는데도 롤백이 제대로 이루어지 않는 다면, 트랜잭션 인스턴스를 제대로 전달하고 있는지 확인할 것!! 각 함수의 Option 객체가 어떻게 이루어졌는지 확인해야 한다.</p>
<p>예를 들어, create의 경우 두번째 인자 옵션에 트랜잭션을 넣어서 전달해야 하지만</p>
<pre><code class="language-js">await User.create({user_id: ... },{ transaction: t });</code></pre>
<p>destroy문의 경우 첫번째 인자가 Option이므로 첫번째 인자에 트랜잭션을 넣어서 전달해야 한다.</p>
<pre><code class="language-js">await models.userCalculetLike.destroy({
 where: { ... },
 transaction: t,
});</code></pre>
<p><del>이걸 못찾아서 한참 삽질함</del></p>
</li>
</ol>
<hr>
<h3 id="reference">reference</h3>
<p><a href="https://sequelize.org/docs/v6/other-topics/transactions/">Sequelize - Transactions</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Sequelize] UPDATE id = id + 1 (increment, decrement)]]></title>
            <link>https://velog.io/@zero-black/Sequelize-UPDATE-id-id-1-increment-decrement</link>
            <guid>https://velog.io/@zero-black/Sequelize-UPDATE-id-id-1-increment-decrement</guid>
            <pubDate>Fri, 27 Jan 2023 06:07:50 GMT</pubDate>
            <description><![CDATA[<p><code>sequalize</code>를 이용해서 값을 증가 혹은 감소시키는 방법에 대해 정리해보려고 한다. 하려고 하는 동작을 SQL 문으로 작성해보자면 아래와 같다.</p>
<pre><code class="language-sql">UPDATE 테이블 명
    SET COL = COL + 1
    WHERE 조건</code></pre>
<p>특정 컬럼의 값을 1 증가시키는 것이다.</p>
<h3 id="update">update</h3>
<p>값을 변경할 때 사용하는 update 문을 사용할 수 있다.
update의 기본적인 활용은 다음과 같다.</p>
<pre><code class="language-js">// Change everyone without a last name to &quot;Doe&quot;
await User.update({ lastName: &quot;Doe&quot; }, {
  where: {
    lastName: null
  }
});</code></pre>
<p>그러나, 특정 데이터의 값을 1 늘리기 위해서는 해당 값의 기존 정보를 알고 있어야 한다. 따라서 이 경우에는</p>
<pre><code class="language-js">const user = await models.User.findByPk(id);
await user.update({ age: user.age + 1});</code></pre>
<p>이렇게 update문을 사용할 수 있다.</p>
<h3 id="increment--decrement">increment / decrement</h3>
<p>혹은 sequalize가 제공하는 increment, decrement을 사용할 수도 있다. 말 그대로 값을 늘리거나 줄이는 함수이고, 사용법은 다음과 같다.</p>
<p>만약 이미 검색한 인스턴스가 있는 경우,</p>
<pre><code class="language-js">await user.increment(&quot;age&quot;, {by: 1});</code></pre>
<p>위와 같이 쓰면 기존 값에 1을 더하게 된다. 만약 더하는 값이 1이라면, by 값은 생략해도 좋다.</p>
<p>혹은 조건을 걸어서 모델에 대해 사용할 수도 있다.</p>
<pre><code class="language-js">await models.User.increment(&quot;age&quot;, { where: {
    id: {
        [Op.eq]: &quot;id&quot;,
    },
}});</code></pre>
<p>decrement도 똑같이 사용할 수 있다. increment의 by 값에 음수를 주는 것과 같은 동작을 하는듯.</p>
<h3 id="update-vs-increment">update vs increment</h3>
<p>둘의 차이는 실행 전후에 콘솔을 찍어보면 알 수 있다.</p>
<pre><code class="language-js">const user = await models.User.findByPk(id);
console.log(user.age); // 1

await user.update({ age: user.age + 1});
console.log(user.age); // 2

await user.increment(&quot;age&quot;);
console.log(user.age); // 2</code></pre>
<p>DB에 저장된 age 값이 1이었다고 하면, 두번째 출력은 2, 세번째 출력은 3이라고 예상할 수 있다. 하지만 세번째 출력에서도 값은 2로 찍히는 것을 볼 수 있다.</p>
<p>DB에 반영되지 않아서가 아니라, update와 increment는 작동 방식에 차이가 있기 때문이다.</p>
<p>우선 update에 대한 공식 문서를 보면,
<img src="https://velog.velcdn.com/images/zero-black/post/3c91ef4e-0077-4333-b696-19f6cfda7c2e/image.png" alt="">
set을 호출한 후에 save를 수행한다고 되어 있다.
set의 설명을 보자.
instance를 update하기 위해 쓰이며, save 호출 전에는 결과가 저장되지 않을 것이라고 나와 있다.
<img src="https://velog.velcdn.com/images/zero-black/post/cf6579e3-c84a-411e-bf00-b49513a935cb/image.png" alt=""></p>
<p>즉, update를 실행할 경우, 우선 set을 수행하여 인스턴스 값을 수정한 후, save를 통해 해당 인스턴스 값을 DB에 저장하게 된다. 반면 increment의 설명을 보자.
<img src="https://velog.velcdn.com/images/zero-black/post/acb86739-65cc-4e8f-9f40-95e9e034e6f3/image.png" alt="">
데이터베이스단에서 수행된다고 되어있다. 즉, 인스턴스를 수정하는 과정이 생략된 것이다. 따라서 마지막 <code>user.age</code>의 값은 업데이트 되지 못한 채 2로 남아있는 것이다.</p>
<p>사실 블로그 안썼으면 대충 되는대로 넘어갔을 것 같은데, 쓰다보니 찝찝한 부분을 해결하려고 공식문서를 찾게 되었다. 그리고 공식 문서에는 모든게 있다 그냥 내가 처음에 못본 것일 뿐...</p>
<hr>
<h3 id="reference">reference</h3>
<p><a href="https://sequelize.org/api/v6/class/src/model.js~model#instance-method-increment">Sequelize - increment</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity] 애니메이션 만들기 (2)]]></title>
            <link>https://velog.io/@zero-black/Unity-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</link>
            <guid>https://velog.io/@zero-black/Unity-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</guid>
            <pubDate>Thu, 26 Jan 2023 14:31:21 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@zero-black/Unity-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%A7%8C%EB%93%A4%EA%B8%B0-1">지난 글</a>에서 애니메이터 컨트롤러에 대해 조금 다뤄보았는데, 각 state에 애니메이션을 지정하는 방법에 대해 마저 써보겠다...
애니메이션은 sprite의 모음으로 만들 수 있다.</p>
<h3 id="애니메이션-만들기">애니메이션 만들기</h3>
<p>이미 sprite 모음이 있는 경우, [Create] &gt; [Animation] 클릭하면 애니메이션을 만들 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/zero-black/post/b1ed1abf-da3c-48eb-8caa-fd039e8a7b62/image.png" alt=""></p>
<p>만약 애니메이션이 단순히 gif형태로 되어있다면, 우선 그걸 sprite로 바꿔야 한다.
<a href="https://ezgif.com/gif-to-sprite">gif to sprite sheet</a>
위의 컨버터로 바꾸면 gif가 한 장의 이미지에 펼쳐지게 된다. 이걸 multiple sprite로 자르면 애니메이션으로 만들어 쓸 수 있다.</p>
<h3 id="애니메이션-적용하기">애니메이션 적용하기</h3>
<p>애니메이터 컨트롤러의 State의 Inspector에서 Motion에 애니메이션을 추가하면 된다.
<img src="https://velog.velcdn.com/images/zero-black/post/9d6913c3-af46-453c-8d03-f301549c0cd2/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity] 애니메이션 만들기 (1)]]></title>
            <link>https://velog.io/@zero-black/Unity-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</link>
            <guid>https://velog.io/@zero-black/Unity-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</guid>
            <pubDate>Wed, 25 Jan 2023 13:29:37 GMT</pubDate>
            <description><![CDATA[<p>유니티에서 애니메이션을 표현하기 위해서는 애니메이터 컨트롤러와 애니메이터가 필요하다.</p>
<p>컨트롤러는 말 그대로 애니메이션을 제어하는 컨트롤러이며, 상태머신으로 애니메이션를 제어한다.</p>
<h3 id="1-애니메이터-컨트롤러-만들기">1. 애니메이터 컨트롤러 만들기</h3>
<p>[Create] &gt; [Animator Controller]
<img src="https://velog.velcdn.com/images/zero-black/post/46277442-55ed-4667-a1bf-f43fbf744f9f/image.png" alt=""></p>
<p>애니메이터를 열만, 아래와 같은 상태머신이 보인다.
<img src="https://velog.velcdn.com/images/zero-black/post/9886ba8f-a078-4f90-afa4-b913de2d8955/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/zero-black/post/b8e2ef57-e308-4635-9d23-729e6c2a1358/image.png" alt=""></p>
<p>우클릭 &gt; [Create State] &gt; [Empty]를 통해 state를 만들 수 있는데, 처음 만든 state는 자동으로 Entry에서 연결된다.</p>
<p>Entry에서 시작된 상태는 자동으로 다음 state로 넘어가기 때문에, 항상 작동해야 하는 애니메이션은 Entry 다음의 state에 배치하면 된다.
만약 특정 트리거 하에 작동해야 하는 애니메이션이 있다면 새로운 state를 만들어야 한다.</p>
<h3 id="transition">transition</h3>
<p>transition은 한 state로부터 다음 state로 전환되는 것을 말한다. state를 우클릭해서 [new transition]을 만들면 화살표가 생긴다.
<img src="https://velog.velcdn.com/images/zero-black/post/d4d6cf40-1fc7-499d-81fc-3c4308de217a/image.png" alt=""></p>
<p>이제 해당 transition을 발생시킬 조건이 필요한데, Animator 설정에서 파라미터를 만들어서 조건에 추가하면 된다!</p>
<p><img src="https://velog.velcdn.com/images/zero-black/post/08127698-2298-48c1-9695-d943beb7b85e/image.png" alt=""></p>
<p>화살표를 클릭하면 Inspector 창에서 Conditions을 추가하면 된다!
<img src="https://velog.velcdn.com/images/zero-black/post/82c1b05d-8b89-4ac6-801f-482740e3f63b/image.png" alt=""></p>
<h3 id="code">code</h3>
<p>이제 프로그램 실행 중에 해당 트리거를 실행할 수 있다.</p>
<pre><code class="language-cs">anim = GetComponent&lt;Animator&gt;();
anim.SetTrigger(&quot;변수 이름&quot;);</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Firebase] IdToken 유효성 검사하기]]></title>
            <link>https://velog.io/@zero-black/Firebase-IdToken-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@zero-black/Firebase-IdToken-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 19 Jan 2023 14:59:00 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@zero-black/Firebase-firebase-admin-sdk-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0">지난 글</a>에서 세팅한 firebase sdk를 사용해서 클라이언트에서 받은 idToken을 검증해보려고 한다.</p>
<p>우선 클라이언트에서 백엔드로 토큰을 보내는 방법은 여러가지가 있겠지만, HTTP 헤더에 토큰을 포함시키는 Bearer Authentication 방법을 사용하였다.</p>
<h3 id="토큰-정보-읽어오기">토큰 정보 읽어오기</h3>
<pre><code class="language-js">let idToken = null;
if (
  req.headers.authorization &amp;&amp;
  req.headers.authorization.startsWith(&quot;Bearer &quot;)
) {
  // get token from header
  idToken = req.headers.authorization.split(&quot;Bearer &quot;)[1];
}</code></pre>
<h3 id="토큰-검증하기">토큰 검증하기</h3>
<pre><code class="language-js">const { admin } = require(&quot;../config/firebase&quot;);

admin
  .auth()
  .verifyIdToken(idToken)
  .then((decodedIdToken) =&gt; {
  console.log(decodedIdToken);
})</code></pre>
<p>검증 결과는 아래와 같이 나타난다. 
<img src="https://velog.velcdn.com/images/zero-black/post/08652be5-250d-48a8-9e7a-f437e64517c6/image.png" alt=""></p>
<p>따라서 firebase의 uid가 필요한 경우, <code>decodedIdToken.user_id</code>로 조회할 수 있다.</p>
<p>토큰이 유효하지 않으면 error로 이어지고, <code>error.code</code>로 조회하면 결과를 볼 수 있다.</p>
<ul>
<li><code>auth/id-token-expired</code>: 유효시간이 지난 토큰</li>
<li><code>auth/argument-error</code>: 유효하지 않은 토큰</li>
</ul>
<h3 id="전체코드">전체코드</h3>
<p>전체 코드는 아래와 같다. 편의를 위해 미들웨어로 선언하였고, 필요한 api에서 가져다 쓰면 된다.</p>
<pre><code class="language-js">const { admin } = require(&quot;../config/firebase&quot;);

/**
 * 미들웨어 - 인증이 필요한 api 앞단에서 클라이언트의 토큰 유효성 검사 (firebase)
 */
async function authFirebase(req, res, next) {
  let idToken = null;
  if (
    req.headers.authorization &amp;&amp;
    req.headers.authorization.startsWith(&quot;Bearer &quot;)
  ) {
    // get token from header
    idToken = req.headers.authorization.split(&quot;Bearer &quot;)[1];
  } else {
    // token not found
    res.status(401).send({
      code: -1,
      message: &quot;can&#39;t find token&quot;,
    });
    return;
  }

  // verify token
  admin
    .auth()
    .verifyIdToken(idToken)
    .then((decodedIdToken) =&gt; {
      res.locals.userId = decodedIdToken.user_id;
      res.locals.email = decodedIdToken.email;
      next(); // 다음 미들웨어로
    })
    .catch((error) =&gt; {
      console.log(error.code);

      res.status(401);

      switch (error.code) {
        // expired token
        case &quot;auth/id-token-expired&quot;:
          // error handling
          break;
        // invalid token
        case &quot;auth/argument-error&quot;:
          // error handling
          break;
      }
    });
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[S3] aws-sdk로 put 요청보내기]]></title>
            <link>https://velog.io/@zero-black/S3-aws-sdk%EB%A1%9C-put-%EC%9A%94%EC%B2%AD%EB%B3%B4%EB%82%B4%EA%B8%B0</link>
            <guid>https://velog.io/@zero-black/S3-aws-sdk%EB%A1%9C-put-%EC%9A%94%EC%B2%AD%EB%B3%B4%EB%82%B4%EA%B8%B0</guid>
            <pubDate>Wed, 18 Jan 2023 14:58:34 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@zero-black/AWS-aws-sdk%EB%A1%9C-S3-bucket-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0">지난 글</a>에 이어서 aws-sdk for javascript을 사용해 S3 버킷에 객체를 업로드하려고 한다.</p>
<p>aws-sdk v3에서 버킷에 요청을 보낼 때, 두가지 단계가 있다.</p>
<ol>
<li>command 객체 생성</li>
<li>생성한 command s3에 보내기</li>
</ol>
<p>putObject의 경우,
1에 해당하는 함수가 <code>PutObjectCommand</code>이다. 여기에 객체 정보를 담은 Object를 넣으면 s3Client에 보낼 command 객체를 만들어준다.</p>
<pre><code class="language-js">const command = new PutObjectCommand({
    Bucket: process.env.AWS_S3_BUCKET_NAME,
    Key: &quot;key-for-object&quot;,
    Body: &quot;object&quot;,    
});</code></pre>
<p>이렇게 만들어진 객체를 send()를 통해 s3 버킷에 보내면 된다.</p>
<pre><code class="language-js">const response = await s3Client.send(command);</code></pre>
<p>전체 소스코드는 다음과 같다.</p>
<pre><code class="language-js">const { S3Client, PutObjectCommand } = require(&quot;@aws-sdk/client-s3&quot;);
const s3Client = new S3Client({ region: &quot;ap-northeast-2&quot; });

async function putObject(){
  const params = {
    Bucket: process.env.AWS_S3_BUCKET_NAME,
    Key: &quot;key-for-object&quot;,
    Body: &quot;object&quot;,
  };
  const command = new PutObjectCommand(params);
  const response = await s3Client.send(command);
  return response;
}

putObject();</code></pre>
<hr>
<h3 id="reference">reference</h3>
<p><a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/index.html">aws-sdk/client-s3</a>
<a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/classes/putobjectcommand.html">aws-sdk/putobjectcommand</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Firebase] HTTP 요청으로 유저 만들기 / 로그인 요청 보내기]]></title>
            <link>https://velog.io/@zero-black/Firebase-HTTP-%EC%9A%94%EC%B2%AD%EC%9C%BC%EB%A1%9C-%EC%9C%A0%EC%A0%80-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9A%94%EC%B2%AD-%EB%B3%B4%EB%82%B4%EA%B8%B0</link>
            <guid>https://velog.io/@zero-black/Firebase-HTTP-%EC%9A%94%EC%B2%AD%EC%9C%BC%EB%A1%9C-%EC%9C%A0%EC%A0%80-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9A%94%EC%B2%AD-%EB%B3%B4%EB%82%B4%EA%B8%B0</guid>
            <pubDate>Mon, 16 Jan 2023 18:05:13 GMT</pubDate>
            <description><![CDATA[<p>Firebase를 사용해서 개발할 때 로그인 관련 테스트를 정말 많이 하게 된다. 백번쯤 로그인을 했다가 풀었다가 난리를 치는 것 같은데... 항상 코드를 통해 요청을 보내는 것이 상당히 번거롭다.</p>
<p>따라서 curl 명령어를 통해 (그나마) 간단하게 유저 정보를 등록하고 토큰을 발급 받는 방식을 사용했었다.</p>
<h3 id="유저-등록-회원가입">유저 등록 (회원가입)</h3>
<pre><code class="language-bash"># Create a test user
curl -X POST -H &quot;Content-Type: application/json&quot; -d &#39;{&quot;email&quot;:&quot;&lt;USER_EMAIL&gt;&quot;,&quot;password&quot;:&quot;&lt;USER_PASSWORD&gt;&quot;,&quot;returnSecureToken&quot;:true}&#39; &quot;https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key=&lt;API_KEY&gt;&quot;</code></pre>
<p>POST 요청시 아래와 같은 응답을 받을 수 있는데, <code>idToken</code>이 인증에 사용되는 토큰, <code>localId</code>가 <code>user uid</code>라고 생각하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/zero-black/post/ff865f67-f2e8-4913-994a-57df202e857f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/zero-black/post/00133a9c-d0d1-463b-84eb-adf4a6115a3e/image.png" alt=""></p>
<h3 id="로그인-idtoken-받아오기">로그인 (idToken 받아오기)</h3>
<pre><code class="language-bash"># Get an ID token for the test user
curl -X POST -H &quot;Content-Type: application/json&quot; -d &#39;{&quot;email&quot;:&quot;&lt;USER_EMAIL&gt;&quot;,&quot;password&quot;:&quot;&lt;USER_PASSWORD&gt;&quot;,&quot;returnSecureToken&quot;:true}&#39; &quot;https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=&lt;API_KEY&gt;&quot;</code></pre>
<p>이미 등록된 유저의 idToken이 필요할 때, 주로 api 작성을 할 때 인증 관련 테스트를 할 때 필요하다.</p>
<p>아까와 비슷하지만 약간 다른 응답이 온다! 약 60분간 유효한 토큰이므로 복사해서 쓰면 된다!
<img src="https://velog.velcdn.com/images/zero-black/post/17a8b359-2fde-42b3-a30f-2794d18bff6e/image.png" alt=""></p>
<p>&lt;비밀번호가 틀린 경우&gt;
<img src="https://velog.velcdn.com/images/zero-black/post/ebed506d-ce96-467f-b054-5f06033c10c2/image.png" alt=""></p>
<p>&lt;이메일이 틀린 경우 (미가입 계정 / 계정 삭제 후)&gt;
<img src="https://velog.velcdn.com/images/zero-black/post/29d57a88-6f3c-434c-82f1-a52e56faa9d8/image.png" alt=""></p>
<p>&lt;사용 중지된 계정의 경우&gt;
<img src="https://velog.velcdn.com/images/zero-black/post/189198a5-8d45-4ae9-8fdd-7af6c94d11ab/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] EC2 - S3 access denied 해결하기]]></title>
            <link>https://velog.io/@zero-black/AWS-EC2-S3-access-denied-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@zero-black/AWS-EC2-S3-access-denied-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 16 Jan 2023 07:16:04 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@zero-black/AWS-aws-sdk%EB%A1%9C-S3-bucket-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0">지난 글</a>에서 aws-sdk for javascript를 통해 s3 버킷에 접근에 성공했었다. 그러나, 로컬에서는 잘 동작하는 것이 이상하게 EC2 인스턴스에서는 동작을 하지 않았다.</p>
<blockquote>
<p><em>사용한 코드 전문</em></p>
<pre><code class="language-js">const express = require(&quot;express&quot;);
const app = express();
require(&quot;dotenv&quot;).config();
const port = process.env.EXPRESS_PORT;

const { S3Client, ListBucketsCommand } = require(&quot;@aws-sdk/client-s3&quot;);

const s3Client = new S3Client({ region: &quot;ap-northeast-2&quot;});

// s3 버킷 정보를 불러와서 출력하는 예제
async function testConnection() {
 try {
   const data = await s3Client.send(new &gt;ListBucketsCommand({}));
   console.log(&quot;Success&quot;, data);
   return data; // For unit tests.
 } catch (err) {
   console.log(&quot;Error&quot;, err);
 }
}

testConnection();
/**
* 서버 시작
*/
app.listen(port, () =&gt; {
 console.log(`listening ${port}`);
});</code></pre>
</blockquote>
<p>물론 env 파일도 잘 설정되어 있었고, 하다 하다 안되어서 key 값을 직접 credential 객체에 넣어서 전달도 해보았지만 계속 403 AccessDenied 가 떴다.</p>
<p><img src="https://velog.velcdn.com/images/zero-black/post/d1284c1f-194d-4401-88fb-c991d386ead1/image.png" alt=""></p>
<p>알고보니... 과거의 내가 쌓아둔 업보였다.
처음 S3 버킷을 파고 EC2에서 연결을 하기 위해서 엔드포인트 설정을 해두었는데, (<a href="https://velog.io/@zero-black/AWS-S3-Bucket-2">과거의 글...</a>) 알고보니 이 권한이 문제였다.</p>
<p>이 때는 이 엔드포인트로는 get요청만 하고, 나머지 요청은 aws-sdk에 키 값을 넣어서 할 것이라고 생각하고 엔드포인트 정책에 GetObject 권한만을 주었었다.
그러나 여기서 이미 권한이 좁혀져서, IAM으로 생성한 키나 역할에 FullAccess를 주어도 listBucket, 혹은 put 요청은 AccessDenied가 되는 것이다.
따라서 해결방법은 다음과 같다.</p>
<p>[VPC 콘솔] &gt; [Endpoints]
지난번 생성한 Endpoint의 Policy의 정책을 수정해주면 된다.
<img src="https://velog.velcdn.com/images/zero-black/post/a23d41d9-1685-4c9d-bd6c-e8c8e146b81a/image.png" alt=""></p>
<p>이제 S3로의 접근은 전부 endpoint를 거칠 것이므로, 기존 로컬에서 사용하던 것처럼 IAM user 키를 사용해서 접근을 할 필요가 없다. 따라서 사용했던 IAM 유저는 삭제해도 된다.(물론 로컬에서 접속하려면 필요하다.) EC2 인스턴스에서 S3 엔드포인트로 연결이 가능하므로, 별도의 credential을 입력하지 않고 그냥 하면 된다.</p>
<pre><code class="language-js">const s3Client = new S3Client({ region: &quot;ap-northeast-2&quot;});</code></pre>
<p>애초에 VPC 구성을 하지 않았다면 그냥 IAM 키로 바로 해결 되었을 문제지만, 내가 EC2를 S3의 프록시로 쓰려고 이전해 해둔 설정이 문제가 되었던거 같다. 그러나 공식문서를 살펴보니 이 방식을 더 권장한다고 한다. ENV 키 파일을 애초에 인스턴스에 저장하지 않아도 되어 보안적으로 더 나은 선택이라고 함!!!</p>
<hr>
<h3 id="reference">reference</h3>
<p><a href="https://aws.amazon.com/premiumsupport/knowledge-center/connect-s3-vpc-endpoint/">Why can’t I connect to an S3 bucket using a gateway VPC endpoint?</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Express.js] error middleware]]></title>
            <link>https://velog.io/@zero-black/Express.js-error-middleware</link>
            <guid>https://velog.io/@zero-black/Express.js-error-middleware</guid>
            <pubDate>Sun, 15 Jan 2023 09:48:10 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@zero-black/Express.js-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0">지난 글</a>에서 미들웨어를 만들어봤는데, 이번엔 오류 처리 미들웨어를 활용해서 코드를 조금 더 리팩토링 해볼 예정이다.</p>
<p>물론 백엔드에서 가능한 모든 오류처리를 세세하게 해주면 좋지만, 가끔 예상하지 못한 오류가 날 때도 있다. 오류 발생시 서버가 멈춰버리기 때문에 일단 모든 로직을 try-catch 블록으로 감싸고 있는데, 중복 코드가 너무 많다. 따라서 오류처리 미들웨어를 하나 만들어서 리팩토링을 해보려고 한다.</p>
<p>다른 미들웨어와는 다르게, 오류처리 미들웨어는 파라미터를 하나 더 받는다.</p>
<pre><code class="language-js">function defaultErrorHandler(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send(&quot;something broke...&quot;);
}</code></pre>
<p>이제 router의 가장 상단 코드(<code>main.js</code>, <code>index.js</code>, <code>router.js</code> 등)에서 오류 처리 미들웨어를 적용해주면 된다.
주의할 점은, express는 미들웨어를 적용할 때 코드 순서대로 적용하기 때문에 가장 마지막에 작성해주어야 한다.</p>
<pre><code class="language-js">...

app.use(errorHandler.default);

// 서버 시작
app.listen(port, () =&gt; {
  console.log(`listening ${port}`);
});</code></pre>
<p>이렇게 되면, 오류 발생시 errorHandler()가 호출되고 서버는 멈추지 않고 500번 에러를 클라이언트로 전송할 것이다.</p>
<h3 id="비동기-에러처리">비동기 에러처리</h3>
<p>그렇지만, async 함수의 경우는 위의 방법으로 해결할 수 없다.
말 그대로 비동기로 처리되기 때문인데... 따라서 wrapper가 필요하다.</p>
<pre><code class="language-js">const asyncErrorHandlerWrapper = (asyncFunc) =&gt; {
  return (req, res, next) =&gt; {
    asyncFunc(req, res, next).catch(next);
  };
};</code></pre>
<p>이제 비동기 처리를 할 때 위 함수로 감싸서 보내면 된다.</p>
<pre><code class="language-js">router.post(&#39;/&#39;, asyncErrorHandlerWrapper(postSomething));</code></pre>
<hr>
<h3 id="reference">reference</h3>
<p><a href="https://expressjs.com/en/guide/using-middleware.html#middleware.error-handling">Error-handling middleware
</a></p>
<p><a href="https://blog.pumpkin-raccoon.com/111"></a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Express.js] 미들웨어 사용하기]]></title>
            <link>https://velog.io/@zero-black/Express.js-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@zero-black/Express.js-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 13 Jan 2023 17:47:24 GMT</pubDate>
            <description><![CDATA[<h3 id="미들웨어">미들웨어</h3>
<p>middleware는 이름에서 알 수 있듯이 중간에서 양쪽을 매개하는 소프트웨어다.
즉, A와 B 사이의 미들웨어를 두면 A에서 B로 요청을 보내면 미들웨어가 그 요청을 먼저 받아서 사전에 어떠한 기능을 수행하고 B로 요청을 넘겨주도록 할 수 있다.
프록시가 네트워크 요청을 중개한다면 미들웨어는 어플리케이션, 소프트웨어 간의 중개를 한다.</p>
<h3 id="express---middleware-function">express - middleware function</h3>
<p>Express에서는 미들웨어 함수를 선언해서 데이터의 유효성 검증, 사전처리 등을 담당하게 할 수 있다.</p>
<p>다른 라우터 함수가 파라미터로 <code>req</code>, <code>res</code> 두 가지를 받는 반면, 미들웨어 함수는 <code>next</code> 인자를 받는다. 말 그대로 다음 함수이다.</p>
<pre><code class="language-js">function func1(req, res, next){
      console.log(&quot;verify data&quot;);
      next(); // 다음 (미들웨어)함수 호출
}

app.post(&#39;/&#39;, func1, (req, res) =&gt; {
    console.log(&quot;process data&quot;);
      res.status(200).send(&quot;success&quot;);
})</code></pre>
<p>이렇게 하면 이제 루트로 POST요청이 들어올 경우, 미들웨어가 먼저 실행된 다음 라우터 함수가 작동할 것이다.</p>
<pre><code>&gt; verify data
&gt; process data</code></pre><h3 id="여러-미들웨어-사용하기">여러 미들웨어 사용하기</h3>
<p>물론 필요에 의해 여러개의 미들웨어가 순차적으로 실행되야 할 수도 있다.
이럴 때는 함수 배열을 전달해주면 된다.
예를 들어 아래와 같은 코드가 있다면,</p>
<pre><code class="language-js">app.post(&#39;/&#39;, [func1, func2, func3], postSomething)</code></pre>
<p>func1 -&gt; func2 -&gt; func3 -&gt; postSomething 순으로 함수가 호출될 것이다.</p>
<p>이 방식의 장점은 클라이언트에서 들어오는 요청에 대한 대응을 비교적 쉽게 할 수 있다는 것이다.</p>
<pre><code class="language-js">function func1(req, res, next){
    if (not authorized){
        res.status(401).send(&quot;Unauthorized&quot;);
          return;
    } else {
        next();
    }
}</code></pre>
<p>만약 첫번째 함수가 유저의 권한을 검사하는 함수라면, 함수2에 넘어오는 요청은 권한 검사를 통과하는 요청 뿐이다. 따라서 미들웨어를 잘 사용하면 중복 코드를 많이 줄이고 편하게 오류 처리를 할 수 있다.</p>
<h3 id="미들웨어간-값-전달">미들웨어간 값 전달</h3>
<p>func1에서 가공한 값을 func2, func3에서 써야 한다면?
이럴 때는 res.locals를 활용한다.</p>
<pre><code class="language-js">function func1(req, res, next){
    res.locals.var1 = data;
}

function func2(req, res, next){
    if (res.locals.var1 == ~~){
        ...    
    }
}</code></pre>
<p>이런식으로 뒷 단의 미들웨어에 값을 넘겨줄 수 있고, 이 변수는 request의 라이프타임 동안에만 유효하기 때문에 클라이언트로 값이 넘어가지는 않는다.</p>
<hr>
<h3 id="reference">reference</h3>
<p><a href="https://expressjs.com/en/guide/using-middleware.html">using middleware</a>
<a href="https://stackoverflow.com/questions/18875292/passing-variables-to-the-next-middleware-using-next-in-express-js">passing variables to the next middleware using next in expree.js</a></p>
]]></description>
        </item>
    </channel>
</rss>