<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>kong-e.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sat, 11 Jan 2025 15:32:17 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>kong-e.log</title>
            <url>https://velog.velcdn.com/images/kong-e/profile/b0ad3f6b-9700-4c3e-8790-af12c182a754/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. kong-e.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kong-e" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Next.js standalone 빌드로 도커 이미지 최적화하기]]></title>
            <link>https://velog.io/@kong-e/Next.js-standalone-%EB%B9%8C%EB%93%9C%EB%A1%9C-%EB%8F%84%EC%BB%A4-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kong-e/Next.js-standalone-%EB%B9%8C%EB%93%9C%EB%A1%9C-%EB%8F%84%EC%BB%A4-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 11 Jan 2025 15:32:17 GMT</pubDate>
            <description><![CDATA[<p>미니 프로젝트를 진행하며 AWS를 지원받게 되었고, Next.js 프로젝트를 Vercel이 아닌 EC2에 배포해보기로 했다.</p>
<p>EC2에 그냥 Next.js 서버를 백그라운드로 켜놓는 식으로 배포할 수도 있었지만, 같은 EC2에 스프링부트 애플리케이션도 띄울 예정이었기 때문에 Docker를 사용하지 않으면 EC2에 jre와 node.js 등 필요한 개발 환경을 일일이 구축해야 했다. </p>
<p>그래서 Docker만 설치해서 간편하게 구동시킬 수 있도록 하기로 했고, Github Actions 기반으로 배포 CI/CD를 구축해 보기로 했다.</p>
<h2 id="초기-작성-dockerfile">초기 작성 Dockerfile</h2>
<p>처음에 작성했던 Next.js Dockerfile이다.</p>
<pre><code class="language-python"># 1단계: 환경설정 및 dependency 설치
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .

# 2단계: 빌드 단계
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package*.json ./
RUN npm install --production
ENV NEXT_PUBLIC_BASE_URL=http://action-be:8080

# 3단계: 실행 단계
CMD [&quot;npm&quot;, &quot;run&quot;, &quot;start&quot;]</code></pre>
<p>멀티스테이지 방식과 alpine 이미지를 사용해서 나름 최적화된 Dockerfile이었다.</p>
<p><img src="https://velog.velcdn.com/images/kong-e/post/d437bcef-ff13-4a16-8bd5-b6c052d73eb0/image.png" alt=""></p>
<p>아래는 초기 작성했던 workflow yml 파일이다.</p>
<pre><code class="language-yml">name: Deploy Frontend

on:
  push:
    paths:
      - &quot;frontend-3rd-loan/**&quot;
    branches:
      - &quot;fe&quot;
      # TODO: 최종에서는 fe -&gt; main으로 바꾸기

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Set up Docker
        run: |
          sudo apt-get update
          curl -fsSL https://get.docker.com -o get-docker.sh
          sudo sh get-docker.sh
      - name: Log in to DockerHub
        run: echo &quot;${{ secrets.DOCKER_PASSWORD }}&quot; | docker login -u &quot;${{ secrets.DOCKER_USERNAME }}&quot; --password-stdin

      # docker hub push
      - name: Push frontend Docker image
        run: |
          cd frontend-3rd-loan
          docker build -t ${{ secrets.DOCKER_USERNAME }}/action-fe:1.0 .
          docker push ${{ secrets.DOCKER_USERNAME }}/action-fe:1.0
      # EC2 Server connection &amp; docker deploy
      - name: EC2 Docker deploy
        uses: appleboy/ssh-action@v0.1.6
        with:
          host: ${{ secrets.EC2_IP }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_SSH_KEY }}

          # Stop and remove only the relevant container
          # Remove old image
          # Pull new image and run
          script: |
            sudo docker stop action-fe || true
            sudo docker rm action-fe || true
            sudo docker rmi -f ${{ secrets.DOCKER_USERNAME }}/action-fe:1.0 || true
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/action-fe:1.0
            sudo docker run -d -p 80:3000 --name action-fe ${{ secrets.DOCKER_USERNAME }}/action-fe:1.0</code></pre>
<p>그렇지만 github workflow 과정에 걸리는 시간이 3분 정도로 너무 길었기 때문에 <code>도커이미지를 더 최적화시키면 되지 않을까?</code> 하는 생각으로 Next.js 이미지를 더 최적화 해보고자 했다. <strong><em>(그런데 오래 걸렸던 원인 중에서 위의 워크플로우 중 Set up Docker 단계의 비중이 컸던 것 같다.. 저 단계는 생략해도 된다고 한다. 그래서 저 단계도 제거했다.)</em></strong></p>
<h2 id="수정한-dockerfile">수정한 Dockerfile</h2>
<p>Next.js standalone 빌드 방식을 사용했다.</p>
<h3 id="nextjs-standalone-빌드란">Next.js standalone 빌드란?</h3>
<p>Next.js의 standalone output은 Next.js 12부터 도입된 기능으로, next build 실행 시 .next/standalone 디렉토리에 완전히 독립적으로 실행 가능한 서버를 생성한다. <strong>최소한의 node_modules만 포함</strong>하여 node server.js로 직접 실행시킬 수 있다.</p>
<p>이 방식을 사용하기 위해서는 next.config.js도 수정해야한다.</p>
<pre><code class="language-js">const nextConfig = { output: &quot;standalone&quot; };

export default nextConfig;</code></pre>
<p>그렇게 해서 열심히 서치해가며 수정된 Dockerfile!</p>
<pre><code class="language-python"># 기본 이미지로 node:18-alpine을 사용하여 base 스테이지 생성
FROM node:18-alpine AS base

# deps 스테이지: 의존성 설치를 위한 단계
FROM base AS deps
# libc6-compat 패키지 설치 (알파인 리눅스 호환성을 위해)
RUN apk add --no-cache libc6-compat
# 작업 디렉토리 설정
WORKDIR /usr/src/app
# package.json과 package-lock.json 파일 복사
COPY package.json package-lock.json ./
# npm 의존성 설치
RUN npm ci
# Next.js 캐시 삭제
RUN rm -rf ./.next/cache

# builder 스테이지: 소스 코드 빌드를 위한 단계
FROM base AS builder
WORKDIR /usr/src/app
# deps 스테이지에서 설치한 node_modules 복사
COPY --from=deps /usr/src/app/node_modules ./node_modules
# 현재 디렉토리의 모든 파일 복사
COPY . ./
# Next.js 애플리케이션 빌드
RUN npm run build

# runner 스테이지: 최종 프로덕션 이미지
FROM base AS runner
WORKDIR /usr/src/app
# 프로덕션 환경 설정
ENV NODE_ENV=production
# 보안을 위한 시스템 그룹과 사용자 생성
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 빌드된 파일들을 복사
COPY --from=builder /usr/src/app/public ./public
COPY --from=builder --chown=nextjs:nodejs /usr/src/app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /usr/src/app/.next/static ./.next/static
# nextjs 사용자로 전환
USER nextjs
# 3000번 포트 노출
EXPOSE 3000
# 포트 환경변수 설정
ENV PORT=3000
# 백엔드 API URL 환경변수 설정
ENV NEXT_PUBLIC_SERVER_API_URL=http://action-be:8080
# 서버 실행 명령
CMD [&quot;node&quot;, &quot;server.js&quot;]</code></pre>
<p>이 도커파일이 개선한 점은 다음과 같다.</p>
<hr>
<ol>
<li>보안 강화</li>
</ol>
<ul>
<li>전용 시스템 사용자(nextjs)와 그룹(nodejs) 생성</li>
<li>root 대신 nextjs 사용자로 애플리케이션 실행</li>
<li>보안 취약점 감소 및 권한 제한</li>
</ul>
<ol start="2">
<li>더 효율적인 의존성 관리</li>
</ol>
<ul>
<li><code>npm install</code> 대신 <code>npm ci</code> 사용으로 더 신뢰성 있는 설치</li>
<li>deps 스테이지를 별도로 분리하여 캐싱 최적화</li>
<li>불필요한 캐시 제거 (.next/cache)</li>
</ul>
<ol start="3">
<li>더 체계적인 멀티 스테이지 빌드</li>
</ol>
<ul>
<li>base, deps, builder, runner 4단계로 명확히 구분</li>
<li>각 단계의 역할이 더 명확하게 분리됨</li>
<li>중간 빌드 결과물을 효율적으로 재사용</li>
</ul>
<ol start="4">
<li>더 최적화된 결과물 복사</li>
</ol>
<ul>
<li>standalone 출력물 사용으로 더 최적화된 프로덕션 번들</li>
<li>정확한 파일 권한 설정 (chown 사용)</li>
<li>필요한 파일만 선택적으로 복사</li>
</ul>
<ol start="5">
<li>명확한 포트 설정</li>
</ol>
<ul>
<li>EXPOSE 명령으로 컨테이너 포트 명시</li>
<li>PORT 환경변수 별도 설정</li>
</ul>
<hr>
<p>이렇게 만들어진 도커 컨테이너의 크기는?
<img src="https://velog.velcdn.com/images/kong-e/post/81815e3c-01cf-4a2c-9900-23bc99605e4e/image.png" alt=""></p>
<p>654.83MB -&gt; 147.18MB로 크기가 <strong>약 77.52% 감소</strong>했다.</p>
<p>Github Actions Workflow 시간도 2m 53s에서 1m 26s 정도로 줄어들었다. (위에서 말했듯 Set up Docker 단계를 삭제해서 줄어든 영향도 있을 것이다.) </p>
<p>도커 이미지 크기가 줄어든다고 Github Actions workflow 시간도 줄어드는걸까? 상관관계가 있을까? 궁금해서 지선생에게 물어봤다.</p>
<blockquote>
<p>도커 이미지 크기와 GitHub Actions 워크플로우 시간 간에는 상관관계가 있을 수 있습니다. 주요 요인은 다음과 같습니다.</p>
</blockquote>
<ol>
<li><strong>빌드 시간</strong>: 도커 이미지가 작아지면 빌드 시간이 줄어들 수 있습니다. 이미지에 포함된 레이어가 적어지고, 필요한 패키지나 라이브러리의 크기가 줄어들면 빌드 프로세스가 더 빨라질 수 있습니다.</li>
<li><strong>캐시 활용</strong>: 도커 이미지 크기가 작으면, GitHub Actions가 이전 빌드의 캐시를 더 효율적으로 사용할 수 있습니다. 캐시를 사용하면 빌드 시간이 단축될 수 있습니다.</li>
<li><strong>푸시/풀 시간</strong>: 워크플로우 중 도커 이미지를 레지스트리에 푸시하거나 풀링하는 과정이 있을 경우, 이미지 크기가 작아지면 이 과정도 더 빠르게 진행됩니다. 네트워크 전송 시간도 줄어들게 됩니다.</li>
<li><strong>테스트 및 배포 시간</strong>: 이미지 크기가 줄어들면 컨테이너의 초기화 시간도 줄어들 수 있어, 테스트 및 배포 단계에서도 시간이 절약될 수 있습니다.</li>
</ol>
<p>결론적으로, 도커 이미지 크기를 줄이면 GitHub Actions 워크플로우의 전체 시간이 단축될 가능성이 있다. 그렇지만 워크플로우의 전체 시간은 네트워크 속도, GitHub Actions의 실행 환경 등 다양한 요인에 의해 영향을 받을 수 있으므로 이미지 크기만이 유일한 요인은 아니다.</p>
<hr>
<h2 id="참고">참고</h2>
<p><a href="https://velog.io/@jadenkim5179/Next.js-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-docker-%EB%B0%B0%ED%8F%AC-%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%81%AC%EA%B8%B0-%EC%A4%84%EC%9D%B4%EA%B8%B0">Next.js 프로젝트 docker 배포 + 이미지 크기 줄이기</a>
<a href="https://kyoung-jnn.com/posts/nextjs-build-optimization-in-ci">캐시(Cache)를 이용한 Next.js 빌드 최적화, 근데 이제 도커를 곁들인</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MERN 스택 프로젝트 API 통신 트러블 슈팅]]></title>
            <link>https://velog.io/@kong-e/MERN-%EC%8A%A4%ED%83%9D-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-API-%ED%86%B5%EC%8B%A0-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@kong-e/MERN-%EC%8A%A4%ED%83%9D-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-API-%ED%86%B5%EC%8B%A0-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Tue, 06 Feb 2024 19:26:48 GMT</pubDate>
            <description><![CDATA[<p>작년 여름에 MERN 스택 프로젝트를 진행했다. 개발 모드에서는 문제가 없었으나, 배포 후 CORS 문제가 생겨 제대로 작동하지 않아 시연 영상으로 대체하게 되었던 기억이 난다.</p>
<p>프로젝트 일정을 마치고나서 위와 같이 해결하지 못했던 문제가 아른거려 
최근에 해당 프로젝트를 다시 열어 문제점들을 해결했다. (아직 CORS 문제 말고도 해결해야할 것들이 많다...)</p>
<p>이 글에서는 문제점을 해결하면서 새로 알게 되었던 점들을 정리해보려 한다.</p>
<h2 id="1-proxy-설정은-개발-모드에서만-작동한다">1. proxy 설정은 개발 모드에서만 작동한다.</h2>
<p>개발 모드에서 API 서버와 통신하는 과정에서 아래와 같이 프론트엔드 package.json의 proxy 설정에 서버 URL을 설정해두었었다.</p>
<pre><code>// ... 생략
    &quot;@types/dompurify&quot;: &quot;^3.0.2&quot;,
    &quot;@types/styled-components&quot;: &quot;5.1.26&quot;
  },
  &quot;proxy&quot;: &quot;http://localhost:5001&quot;
}</code></pre><p>이렇게 설정함으로써 예를 들어 axios.get(&#39;/api/answers&#39;)로 요청을 보내면 &#39;<a href="http://localhost:5001/api/answers&#39;">http://localhost:5001/api/answers&#39;</a> 로 요청을 보낼 수 있었다.</p>
<p>그런데 배포 이후 proxy를 배포된 서버 URL로 변경하여도 요청이 제대로 보내지지 않는 문제가 발생했다.</p>
<p>그 이유는 package.json의 proxy 설정이 개발 모드에서만 통하는 설정이기 때문이었다.</p>
<blockquote>
<p>Keep in mind that proxy only has effect in development (with npm start)</p>
</blockquote>
<p> 이러한 사항은 CRA 공식 문서에도 위와 같이 명시되어 있었다.</p>
<p><img src="https://velog.velcdn.com/images/kong-e/post/a467da61-221b-447a-b66d-f65b20151683/image.png" alt=""></p>
<p> 이러한 프록시 설정은 클라이언트에서 보낸 요청을 개발 서버에서 프록시 서버로 전달하고, 프록시 서버가 해당 요청을 다시 실제 API 서버로 전달하는 방식으로 동작한다. 
 즉 요청을 대신 보내주는 포워드 프록시의 역할을 한다.</p>
<p> 그런데 이 proxy 설정은 개발 모드에서만 작동한다! 개발 모드에서 CORS 에러를 방지하기 위해 우회하는 방법으로 사용한다고 한다.</p>
<p>그래서 배포에서도 서버 URL로 요청이 정상적으로 보내지도록 하기 위해
axios.create를 통해 만들어진 axios 인스턴스를 적용하여 요청을 보냈다.</p>
<pre><code class="language-typescript">const instance = axios.create({
    baseURL: process.env.REACT_APP_SERVER_URL,
    withCredentials: true,
    headers: {
        &#39;Content-Type&#39;: &#39;application/json&#39;,
    },
});</code></pre>
<p>axios.create로 기본 instance를 설정하여 기존 axios.get(&#39;/api/answers&#39;) 형식에서 instance.get(&#39;/api/answers&#39;) 형식으로 api 요청 코드를 변경했다.</p>
<p>그런데 나중에 이러한 설정도 문제를 일으키는데...</p>
<h2 id="2-put-요청에-발생하는-preflight-400-에러">2. PUT 요청에 발생하는 Preflight 400 에러</h2>
<blockquote>
<p>No &#39;Access-Control-Allow-Origin&#39; header is present on the requested resource.</p>
</blockquote>
<p>그 후 대부분의 요청은 잘 이루어졌으나, <strong>일부</strong> PUT 요청(북마크, 좋아요 기능)에서 Preflight 에러가 발생했다.</p>
<pre><code class="language-typescript">const instance = axios.create({
    baseURL: process.env.REACT_APP_SERVER_URL,
      // withCredentials: true,
    // headers: {
    //     &#39;Content-Type&#39;: &#39;application/json&#39;,
    // },
});</code></pre>
<p>결과적으로는 axios instance에서 credentials 설정과 Content-Type에 대한 설정 부분을 지우니 정상적으로 PUT 요청이 이루어졌다.</p>
<p>우선 우리 프로젝트에서는 토큰 관리에 쿠키를 이용하지 않았으므로 withCredentials를 true로 설정해줄 필요는 없었다. 그래서 해당 부분을 지웠다.</p>
<p>문제는 headers의 Content-Type을 application/json으로 지정해준 부분인데, 이 부분 때문에 문제가 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/kong-e/post/f5df0402-ace6-46df-aae7-18ab313ba22c/image.png" alt="">저 headers 부분을 주석 처리하고 정상적으로 PUT 요청이 이루어졌을 때의 Content-Type은 <code>application/x-www-form-urlencoded</code>였다.
<img src="https://velog.velcdn.com/images/kong-e/post/d5446a0b-9503-4ac0-82b5-2c6c1521c7ed/image.png" alt="">Content-Type이 <code>application/json</code>으로 설정된 axios 인스턴스로 bookmark PUT 요청을 보내면 에러가 났다. 
왜 해당 PUT 요청에서만 Content-Type이 <code>application/json</code>으로 설정되어있을 때 400 에러가 나는지는 아직 이유를 정확히 파악하지는 못했다. 
알게 되면 다시 글을 추가하여 써 보겠다.</p>
<pre><code class="language-typescript">app.use(
  cors({
    origin: process.env.CLIENT_URL,
    // credentials: true 삭제
  })
);</code></pre>
<p>또한 우리 프로젝트는 쿠키를 사용하지 않기 때문에 express 서버의 cors에 설정되어있던 credentials: true도 지워줬다.</p>
<h2 id="3-envdevelopment와-envproduction">3. .env.development와 .env.production</h2>
<p>기존에 .env 파일 하나로 관리되던 환경변수를 .env.development와 .env.production으로 나누어줬다. 
하나의 .env 파일만 사용할 때는 프로덕션에서 테스트를 하다가 다시 개발 모드로 돌아갈 때마다 .env 파일을 수정해주어야 했다.</p>
<p>이러한 점이 불편해서 환경변수를 프론트엔드와 백엔드 모두 두 개의 파일로 나누어주었다.</p>
<h3 id="프론트엔드">프론트엔드</h3>
<p><em>.env.production</em></p>
<pre><code class="language-javascript">REACT_APP_SERVER_URL=배포된_서버_주소
</code></pre>
<p><em>.env.development</em></p>
<pre><code class="language-javascript">REACT_APP_SERVER_URL=http://localhost:5001</code></pre>
<p>프론트엔드의 경우, 리액트가 npm start에서는 자동으로 .env.development의 환경변수를 적용해주고 빌드 시에는 .env.production을 적용해주는 것 같아서 어렵지 않았다.</p>
<h3 id="백엔드">백엔드</h3>
<p><em>.env.development</em></p>
<pre><code class="language-javascript">MONGO_URL=mongodb+srv://~
CLIENT_URL=http://localhost:3000
JWT_SECRET_KEY=시크릿키</code></pre>
<p><em>.env.production</em></p>
<pre><code class="language-javascript">MONGO_URL=mongodb+srv://~
CLIENT_URL=배포된_클라이언트_주소
JWT_SECRET_KEY=시크릿키</code></pre>
<p>백엔드는 이와 같이 설정해주었는데 프론트엔드와 달리 아래와 같이 NODE_ENV에 따라 적용될 환경변수파일을 지정해주어야 했다.</p>
<p>먼저 dotenv 패키지와 cross-env 패키지를 설치해야 한다.</p>
<p><code>npm install dotenv</code>
<code>npm install cross-env --save-dev</code></p>
<p>dotenv는 환경 변수를 설정하고 관리할 때 편리한 도구이고, 
cross-env는 프로젝트를 다양한 환경에서 실행할 때 환경 변수를 쉽게 설정할 수 있도록 돕는 도구다. 예를 들어, Windows와 Unix 기반 시스템에서는 환경 변수 설정이 서로 다른데, cross-env를 사용하면 이런 차이를 신경쓰지 않고 환경 변수를 설정할 수 있어 개발할 때 유용하다.</p>
<p><em>package.json</em></p>
<pre><code class="language-js">// ... 생략
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;cross-env NODE_ENV=development nodemon index.js&quot;,
    &quot;start&quot;: &quot;cross-env NODE_ENV=production node index.js&quot;
  },
// ... 생략</code></pre>
<p>npm run dev를 실행할 때 NODE_ENV는 development로, 
npm run start를 실행할 때 NODE_ENV는 production으로 이름 짓는다.</p>
<p><em>index.js</em></p>
<pre><code class="language-javascript">// NODE_ENV에 따라 적절한 .env 파일을 로드한다.
if (process.env.NODE_ENV === &quot;development&quot;) {
  dotenv.config({ path: &quot;./.env.development&quot; });
} else if (process.env.NODE_ENV === &quot;production&quot;) {
  dotenv.config({ path: &quot;./.env.production&quot; });
}</code></pre>
<p>NODE_ENV가 development일 때는 .env.development 환경 변수를,
NODE_ENV가 production일 때는 .env.production 환경 변수를 적용하도록 한다.</p>
<p>그런데 index.js에만 설정할 게 아니라 환경변수를 사용하는 파일 모두에 위 코드를 넣어줘야 에러가 발생하지 않았다. 나는 환경 변수에 이미 설정되어 있는 JWT Secret Key가 없다는 에러가 자꾸 떠서 화가 났는데 이 환경변수를 사용하는 (index.js 외의) 다른 파일에도 위의 dotenv 코드를 넣어주니 에러가 사라졌다.</p>
<hr>
<h3 id="참고자료">참고자료</h3>
<p><a href="https://create-react-app.dev/docs/proxying-api-requests-in-development/">https://create-react-app.dev/docs/proxying-api-requests-in-development/</a>
<a href="https://falsy.me/nodejs-express-%ED%86%B5%EC%8B%A0-cors-cors-pre-flight-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0/">https://falsy.me/nodejs-express-%ED%86%B5%EC%8B%A0-cors-cors-pre-flight-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0/</a>
<a href="https://velog.io/@public_danuel/process-env-on-node-js">https://velog.io/@public_danuel/process-env-on-node-js</a>
<a href="https://soohey.tistory.com/4">https://soohey.tistory.com/4</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[애자일에 대한 정리]]></title>
            <link>https://velog.io/@kong-e/%EC%95%A0%EC%9E%90%EC%9D%BC%EC%97%90-%EB%8C%80%ED%95%9C-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@kong-e/%EC%95%A0%EC%9E%90%EC%9D%BC%EC%97%90-%EB%8C%80%ED%95%9C-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 06 Feb 2024 18:26:30 GMT</pubDate>
            <description><![CDATA[<p>수업 과제로 애자일에 대해 한 페이지 정도로 요약했던 것이다.
요즘 애자일 용어에 대해 헷갈려서 리마인드 하는 겸 블로그에도 기록한다.</p>
<h2 id="애자일">애자일</h2>
<ul>
<li>신속한 개발 방법론, 지속 가능한 개발 장려</li>
<li>공정과 도구 &lt; 개인과 상호작용</li>
<li>문서 &lt; 소프트웨어</li>
<li>계약 협상 &lt; 고객과의 협력</li>
<li>계획 &lt; 변화에 대응<h2 id="애자일-우산">애자일 우산</h2>
애자일 원칙을 실천하기 위해 제안되는 방식들<h3 id="스크럼">스크럼</h3>
</li>
<li>작은 팀이 짧은 주기로 일을 나누어 협업하고, 변화에 유연하게 대응하는 프로세스</li>
<li>팀이 강한 조직력을 가질 수 있도록 해줌<ul>
<li><strong>백로그</strong>: 프로젝트를 구성하는 주요 아이디어, 피드백, 요구사항을 모아 프로젝트의 목표를 구성하는 각 단계로 나눈 각각의 요소<ul>
<li><strong>Sprint</strong>: 1~4주 정도로 계획/구현/리뷰/회고를 반복하는 하나의 단위, Backlog에서 이번 스프린트에 진행할 백로그 선정</li>
<li><strong>Daily Scrum</strong>: 매일 동안 진행되는 15분 정도의 짧은 회의, 어제 뭐했는지 / 오늘 뭐할건지 / 방해요소 없는지, Sprint를 잘하기위해</li>
</ul>
</li>
<li><strong>Retrospective(회고)</strong>: 다음 스프린트는 어떻게 하면 잘할 수 있을지 개선점을 찾는 과정, Action Item(구체적 행동 제안)</li>
<li><strong>가치</strong>: 매 스프린트가 끝나면 작동하는 소프트웨어가 만들어짐, 잘 하고 있는지 고민해볼 수 있음, 명확한 목표가 주어짐<h3 id="칸반">칸반</h3>
생산량을 조절하고 과잉생산을 줄이기 위한 기법
ex. Jira, GitHub issues<h3 id="린">린</h3>
</li>
</ul>
</li>
<li>자원 낭비를 최소화하고 가치를 최대화하기 위해 지속적으로 개선하는 제조업에서 시작된 개념</li>
<li>소프트웨어 개발에서는 린 소프트웨어 개발이라고 하며, 고객 가치 중심으로 작은 배포 가능한 제품을 빠르게 개발하여 피드백을 받고 이를 통해 지속적으로 개선<h3 id="xp">XP</h3>
</li>
<li>개발 과정에서 테스트 주도 개발(TDD), 짝 프로그래밍, 계획과 반복, 간단한 설계 등의 가치를 강조</li>
<li>TDD : 테스트코드 작성, 테스트코드가 수시로 내 프로그램 검증</li>
<li>Pair Programming : Driver(코드 작성하는 사람)와 Navigator(제안하는 사람), 실력차이가 적을 때 효과적</li>
</ul>
<h2 id="애자일-도구--애자일-선언--애자일-essence">애자일 도구 &lt; 애자일 선언 &lt; 애자일 Essence</h2>
<ul>
<li>애자일 Essence는 애자일 방법론의 본질적인 가치와 원리를 포괄하는 개념</li>
<li>애자일 방법론을 적용할 때 사용되는 도구나 프레임워크보다는 애자일 선언에 명시된 가치와 원칙을 이해하고 따르는 것이 더 중요</li>
<li>애자일을 단순히 도구나 프로세스로만 보지 않고, 그 철학과 가치에 중점을 두어야 함</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Node.js Express 서버 도커라이즈하기(multi-stage build)]]></title>
            <link>https://velog.io/@kong-e/Node.js-Express-%EC%84%9C%EB%B2%84-%EB%8F%84%EC%BB%A4%EB%9D%BC%EC%9D%B4%EC%A6%88%ED%95%98%EA%B8%B0multi-stage-build</link>
            <guid>https://velog.io/@kong-e/Node.js-Express-%EC%84%9C%EB%B2%84-%EB%8F%84%EC%BB%A4%EB%9D%BC%EC%9D%B4%EC%A6%88%ED%95%98%EA%B8%B0multi-stage-build</guid>
            <pubDate>Tue, 23 Jan 2024 08:14:47 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에서 줄곧 프론트엔드를 담당 해오다가 최근 프로젝트에서는 백엔드를 맡게 되었다. 비교적 익숙한 Node.js의 Express를 사용하여 백엔드를 구축하기로 하였다.</p>
<p>서버는 EC2에 Docker 컨테이너를 띄우는 식으로 하게 되었는데, 이 과정에서 Dockerfile을 만들어야했다.</p>
<p>아래는 처음 작성한 Dockerfile이다.</p>
<pre><code class="language-shell"># 기본 이미지로 node의 최신 LTS 버전 사용
FROM node:lts

# 작업 디렉토리 설정
WORKDIR /usr/src/app

# package.json과 package-lock.json(있을 경우) 복사
COPY package*.json ./

# 프로젝트 의존성 설치
RUN npm install

# 앱 소스 추가
COPY . .

# 빌드 스크립트 실행 (TypeScript 프로젝트인 경우)
RUN npm run build

# 앱 시작
CMD [&quot;node&quot;, &quot;dist/index.js&quot;]</code></pre>
<p>이 이미지의 크기가 상당히 커서 빌드한 뒤에 Docker Desktop이 멈추고 느려지는 현상이 발생했다.
<img src="https://velog.velcdn.com/images/kong-e/post/ee08578e-dbcd-4f5e-a018-033b43ed1b7b/image.png" alt=""></p>
<p>그래서 이를 해결할 수 있는 방법을 알아보니 아래의 3가지 방법이 있었다.</p>
<blockquote>
<ol>
<li><strong>보다 가벼운 베이스 이미지 사용</strong>: node:alpine 같은 더 가벼운 버전을 사용해볼 수 있다. Alpine Linux는 작고 가볍다는 특징이 있다.</li>
<li><strong>멀티-스테이지 빌드</strong>: 멀티-스테이지 빌드를 사용하여 빌드 단계에 필요한 도구들을 최종 이미지에 포함시키지 않을 수 있다.</li>
<li><strong>불필요한 파일 제거</strong>: .dockerignore 파일을 사용해 불필요한 파일이나 디렉토리를 Docker 이미지에 포함시키지 않도록 할 수 있다.</li>
</ol>
</blockquote>
<p>가벼운 베이스 이미지를 이용한 <strong>Multi-stage build</strong> 방법을 써보기로 했다.</p>
<h2 id="multi-stage-build">Multi-stage Build</h2>
<pre><code class="language-shell"># 빌드 스테이지
FROM node:lts AS builder # AS builder는 이 빌드 스테이지의 이름을 builder로 지정
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# 실행 스테이지
FROM node:alpine
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app/dist ./dist # builder 스테이지에서 빌드한 결과물을 현재 스테이지로 복사
COPY package*.json ./
RUN npm install --only=production # --only=production 옵션으로 npm install을 실행하여 개발 의존성을 제외한 프로덕션 의존성만 설치
CMD [&quot;node&quot;, &quot;dist/index.js&quot;]</code></pre>
<p>아래와 같이 이미지 크기가 크게 줄어든 것을 볼 수 있다.
<img src="https://velog.velcdn.com/images/kong-e/post/15920b71-9fdb-4f95-afd2-8d244ff373fe/image.png" alt=""></p>
<p>.dockerignore 파일도 추가해보았으나 아직 가벼운 프로젝트 크기라 그런지 이미지 크기에는 영향을 주지 않았다.</p>
<p>추가적으로 환경변수가 node 서버를 실행시키기 위해 환경변수가 필요했는데, 
이럴 경우 이미지를 빌드한 다음 컨테이너를 실행시킬 때 
<code>docker run -p 5005:5005 -e PORT=5005 -e MONGO_URL=mongodb://yourMongoDBUri nft-backend</code></p>
<p>이런 긴 명령어를 또 넣어줘야하기 때문에 docker-compose up 명령어로 한 번에 이미지 빌드와 컨테이너 실행을 시키기 위해 도커 컴포즈 파일을 작성했다.</p>
<pre><code class="language-shell">version: &#39;3.8&#39;
services:
  dgu-nft-backend:
    build: .
    ports:
      - &#39;5005:5005&#39;
    environment:
      MONGO_URL: ${MONGO_URL}
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules</code></pre>
<p>이렇게 할 경우 현재 디렉토리 .env 파일의 환경 변수가 적용된다.</p>
<hr>
<h3 id="참고자료">참고자료</h3>
<p><a href="https://sachithsiriwardana.medium.com/dockerizing-nodejs-application-with-multi-stage-build-e30477ca572">https://sachithsiriwardana.medium.com/dockerizing-nodejs-application-with-multi-stage-build-e30477ca572</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[알고리즘] 그래프와 그래프 탐색]]></title>
            <link>https://velog.io/@kong-e/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B7%B8%EB%9E%98%ED%94%84</link>
            <guid>https://velog.io/@kong-e/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B7%B8%EB%9E%98%ED%94%84</guid>
            <pubDate>Mon, 01 Jan 2024 02:56:58 GMT</pubDate>
            <description><![CDATA[<h3 id="개념">개념</h3>
<ul>
<li>데이터들 간 관계를 표현</li>
<li>데이터가 있고 데이터들 간의 커넥션이 있는 경우 그래프로 나타냄</li>
<li>정점 = 데이터</li>
<li>간선 = 정점과 정점을 이어줌, 방향이 있을 수도 있고 없을 수도 있음</li>
<li>가중치 = 간선 위의 숫자</li>
</ul>
<h3 id="무방향-그래프">무방향 그래프</h3>
<ul>
<li>간선의 방향이 없는 그래프</li>
<li>가중치와는 무관
<img src="https://velog.velcdn.com/images/kong-e/post/fa97ce46-e671-4caf-bd76-88b3dd68a3d9/image.png" alt=""></li>
</ul>
<h3 id="방향-그래프">방향 그래프</h3>
<ul>
<li>간선의 방향이 있는 그래프</li>
<li>가중치와는 무관
<img src="https://velog.velcdn.com/images/kong-e/post/e9fa00d3-4676-45c0-82f6-4c7401362eeb/image.png" alt=""></li>
</ul>
<h3 id="가중치-그래프">가중치 그래프</h3>
<ul>
<li>간선에 가중치가 있는지 여부가 중요</li>
<li>간선의 방향과 무관</li>
</ul>
<h3 id="표현">표현</h3>
<h4 id="인접행렬을-이용한-방식">인접행렬을 이용한 방식</h4>
<ul>
<li>2차원 배열 활용</li>
<li>2차원 배열의 인덱스 = 각 정점</li>
<li>배열의 값 = 정점의 가중치</li>
<li>random access 가능 -&gt; 성능 면에서 우수</li>
<li>정점의 개수 1000개 미만일 때 활용
<img src="https://velog.velcdn.com/images/kong-e/post/6fffba5e-b3e6-45bf-86c9-03267babc236/image.png" alt=""></li>
</ul>
<h4 id="인접리스트를-이용한-방식">인접리스트를 이용한 방식</h4>
<ul>
<li>링크드리스트 활용</li>
<li>메모리 측면에서 우수</li>
<li>정점의 개수 1000개 이상일 때 활용
<img src="https://velog.velcdn.com/images/kong-e/post/57342034-f219-4222-aec9-285d3a6931ba/image.png" alt=""></li>
</ul>
<h3 id="탐색">탐색</h3>
<h4 id="깊이우선탐색dfs---lifo">깊이우선탐색(DFS) - LIFO</h4>
<ul>
<li>뒤로 가는 동작이 필요함 = 백트랙이 있음</li>
<li>최근에 push 된 정점에 방문함</li>
</ul>
<ol>
<li>시작 정점을 방문한다.</li>
<li>현재 방문한 정점과 연결된 정점 중, 아직 방문하지 않은 정점을 스택에 push 한다. 이때 방문한 정점의 visited를 true로 설정한다</li>
<li>stack에서 pop하면서 방문한다</li>
</ol>
<p>2~3 과정을 모든 정점을 방문할 때까지 반복한다. 
<img src="https://velog.velcdn.com/images/kong-e/post/07644fe8-7620-46d5-9b16-c6fe0730dc3d/image.png" alt=""></p>
<p><em><strong>dfs_stack</strong></em></p>
<pre><code class="language-python">def dfs(graph, start_node):
    visited = [False] * (len(graph) + 1)
    stack = [start_node]

    while stack:
        node = stack.pop()

        if not visited[node] :
            visited[node] = True;
            print(node)
            for adj_node in graph[node]: # 인접한 노드 방문
                    if not visited[adj_node]:
                        stack.append(adj_node)


# 그래프를 인접 리스트로 표현
graph = {
    1: [2, 3],
    2: [4, 5],
    3: [],
    4: [],
    5: []
}

# DFS 알고리즘 실행
dfs(graph, 1) # 1 3 2 5 4</code></pre>
<p><strong><em>dfs_recursion</em></strong></p>
<pre><code class="language-python">def dfs(graph, start_node,visited):
    visited[start_node] = True
    print(start_node)

    for adj_node in graph[start_node]: # 인접한 노드 방문
       if not visited[adj_node]:
          dfs(graph,adj_node,visited)


# 그래프를 인접 리스트로 표현
graph = {
    1: [2, 3],
    2: [4, 5],
    3: [],
    4: [],
    5: []
}

visited = [False] * (len(graph) + 1)
# DFS 알고리즘 실행
dfs(graph, 1,visited) # 1 2 4 5 3</code></pre>
<h4 id="너비우선탐색bfs---fifo">너비우선탐색(BFS) - FIFO</h4>
<ul>
<li>레벨: 몇 번 거쳐서 가느냐</li>
<li>같은 레벨끼리는 순서가 바뀌어도 상관없음</li>
<li>연결된 모든 것을 담아야 하므로 메모리 공간이 많이 필요</li>
<li>정점을 일단 큐에 넣어놓고 나중에 방문함</li>
</ul>
<ol>
<li>시작 노드(1)를 큐에 넣고, 방문 처리를 한다</li>
<li>큐에서 노드를 하나 꺼낸다(노드 1)</li>
<li>해당 노드의 인접 노드 중 방문하지 않은 노드들(노드 2와 3)을 큐에 넣고 방문 처리를 한다.</li>
<li>큐에서 노드를 하나 꺼낸다(노드 2)</li>
<li>노드 2의 인접 노드 중 방문하지 않은 노드들(노드 4와 5)을 큐에 넣고 방문 처리를 한다.</li>
</ol>
<p>위 과정을 큐가 빌 때까지 반복한다.</p>
<p><img src="https://velog.velcdn.com/images/kong-e/post/a9630525-626d-4675-9b4f-822137e4103e/image.png" alt=""></p>
<pre><code class="language-python">from collections import deque

def bfs(graph, start_node):
    visited = [False] * (len(graph) + 1)
    queue = deque([start_node])
    visited[start_node] = True # 시작노드 방문 처리


    while queue:
        node = queue.popleft() # 이어붙인 노드를 꺼냄
        print(node) # 방문 노드 출력
        for adj_node in graph[node]: # 인접한 노드 방문
            if not visited[adj_node]:
                queue.append(adj_node) # 큐에 이어붙임
                visited[adj_node] = True

# 그래프를 인접 리스트로 표현
graph = {
    1: [2, 3],
    2: [4, 5],
    3: [],
    4: [],
    5: []
}

# BFS 알고리즘 실행
bfs(graph, 1) # 1, 2, 3, 4, 5</code></pre>
<hr>
<h3 id="참고자료">참고자료</h3>
<p><a href="https://cafe.naver.com/dremdeveloper/418">https://cafe.naver.com/dremdeveloper/418</a>
<a href="https://cafe.naver.com/dremdeveloper/30">https://cafe.naver.com/dremdeveloper/30</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 소셜로그인 구현 방법 정리]]></title>
            <link>https://velog.io/@kong-e/React-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95-%EC%A0%95%EB%A6%AC-l4zkl3pe</link>
            <guid>https://velog.io/@kong-e/React-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95-%EC%A0%95%EB%A6%AC-l4zkl3pe</guid>
            <pubDate>Fri, 24 Nov 2023 14:03:08 GMT</pubDate>
            <description><![CDATA[<p>최근 소셜로그인 기능을 백엔드(Spring boot 이용)와 협업하여 구현해야했는데,
조사하면서 사람들마다 다양한 방식으로 구현하고 있어서 혼란스러웠다.
그래서 알아낸 구현방법들을 정리해보고자 한다. 
참고로 구글 소셜로그인을 기준으로 작성된 글인데, 다른 플랫폼의 소셜로그인 구현 방식도 유사하다고 함!</p>
<h2 id="✨fe-라이브러리를-이용한-토큰-발급-방식">✨FE 라이브러리를 이용한 토큰 발급 방식</h2>
<p><img src="https://velog.velcdn.com/images/kong-e/post/b851f377-8aa9-42c9-a735-914af6bd0d1a/image.png" alt="">gapi (Google API Client Library)와 react-google-login나 @react-oauth/google과 같은 자바스크립트 혹은 리액트 라이브러리를 사용하는 방식이다.
이 방식을 사용하면 클라이언트 사이드에서 사용자 인증을 직접 처리하고 토큰을 관리할 수 있다. 
구글 클라우드 플랫폼에서 OAuth 클라이언트 ID와 클라이언트 secret을 발급받은 뒤, 이 정보를 코드에 해당 라이브러리에서 요구하는 방식대로 입력하면 소셜로그인을 구현할 수 있다.
이 방식은 리액트에서 구글로 로그인 요청을 하면 구글에서 유저데이터와 토큰을 리액트에 다이렉트로 보내주는 방식이다. 서버 사이드 코드 없이 프론트엔드에서 모든 인증 흐름을 처리할 수 있지만, 이 경우 보안에 더 많은 주의가 필요하다고 한다.<img src="https://velog.velcdn.com/images/kong-e/post/0ec2bfd8-aac3-44b9-a1bd-ec2af460d627/image.png" alt="">이후 위의 그림과 같이 유저데이터를 백엔드에 보내 유저 정보를 관리할 수 있다.</p>
<h2 id="✨fe와-be-간-핑퐁을-통한-토큰-발급-방식">✨FE와 BE 간 핑퐁을 통한 토큰 발급 방식</h2>
<p>이 방식은 구글클라우드플랫폼에서 OAuth 클라이언트 ID를 만들 때 리디렉션 URI를 클라이언트 URI로 설정하느냐, 서버 URI로 설정하느냐에 따라 다시 두 가지로 나눌 수 있다. 참고로 response_type을 code로 설정했을 경우를 전제하였따.
<img src="https://velog.velcdn.com/images/kong-e/post/12ee0cb1-17ad-4ae9-b9ac-18ba9a607541/image.png" alt="">여기서 설정할 수 있다.</p>
<h3 id="1-리디렉션-uri를-클라이언트-uri로-설정할-경우">1) 리디렉션 URI를 클라이언트 URI로 설정할 경우</h3>
<p>리디렉션 URI를 클라이언트 URI(ex. localhost:3000/example)로 설정할 경우다.
<img src="https://velog.velcdn.com/images/kong-e/post/332757e1-1834-4371-89bf-28fff279814a/image.png" alt=""><img src="https://velog.velcdn.com/images/kong-e/post/bcba8c56-20b6-4361-8e60-a3f40fd9cfe3/image.png" alt=""><img src="https://velog.velcdn.com/images/kong-e/post/0ca3ab08-a72b-4ac1-8819-1fd9c51d3127/image.png" alt=""></p>
<p>즉 정리해보자면,</p>
<p><strong>1. 클라이언트가 로그인 버튼을 누르면 구글 소셜 로그인 링크로 이동한다.
2. 클라이언트가 로그인 할 계정을 클릭한다.
3. 설정한 리디렉션 클라이언트 URI로 구글이 인가코드(code)를 쿼리파라미터로 설정하여 보낸다. 이 인가코드는 재사용이 불가능하다.
4. FE는 쿼리파라미터에서 인가코드를 추출하여 BE에 보낸다.
5. 백엔드는 획득한 인가코드로 구글 측에 액세스토큰을 요청한다.
6. 백엔드는 획득한 액세스토큰으로 구글 측에 사용자 정보를 요청한다.
7. 백엔드는 획득한 유저 정보를 데이터베이스에 저장하고 JWT 토큰을 발급한다.
8. 백엔드는 발급한 JWT 토큰과 함께 클라이언트 URI로 리다이렉트를 한다.
9. 클라이언트는 백엔드로부터 JWT 토큰을 획득한다.</strong></p>
<p><strong>코드 예시(4단계~9단계)</strong>(<a href="https://github.com/g2Min/Prog-rangers/blob/main/frontend/Prog-rangers-main/src/components/SignUp/GoogleRedirect.jsx">출처</a>)
인가코드가 쿼리파라미터를 통해 클라이언트 측으로 전달되어 이를 추출한 뒤, 다시 BE로 토큰을 요청하는 코드다.</p>
<pre><code class="language-javascript">import axios from &quot;axios&quot;;
import { useEffect, useContext } from &quot;react&quot;;
import { Navigate, useNavigate } from &quot;react-router-dom&quot;;
import { IsLoginContext } from &quot;../../context/AuthContext&quot;;

export const GoogleRedirect = () =&gt; {
  const navigate = useNavigate();
  const href = window.location.href;
  let params = new URL(document.location).searchParams;
  let GOOGLE_CODE = params.get(&quot;code&quot;);
  const { setIsLogin } = useContext(IsLoginContext);

  useEffect(() =&gt; {
    fetch(`http://13.124.131.171:8080/api/v1/login/google?code=${GOOGLE_CODE}`,{
        method: &quot;POST&quot;,
        headers: {
          &quot;Content-Type&quot;: &quot;application/json;&quot;,
        },
      })
      .then(res =&gt;{
          return res.json();
        })
      .then(data =&gt; {
        console.log(data);
        localStorage.setItem(&#39;token&#39;, data.accessToken);
        localStorage.setItem(&#39;nickname&#39;, data.nickname);
        setIsLogin(true);
        navigate(&quot;/&quot;);
      })
      .catch(error =&gt;{
        console.log(&#39;Error:&#39;, error);
      })

  }, []);

  return(
    &lt;&gt;
    &lt;/&gt;
  );
};</code></pre>
<h3 id="2-리디렉션-uri를-서버-uri로-설정할-경우">2) 리디렉션 URI를 서버 URI로 설정할 경우</h3>
<p>리디렉션 URI를 서버 URI(ex. localhost:8080/example)로 설정할 경우다.
<img src="https://velog.velcdn.com/images/kong-e/post/3d49c18d-8d75-4994-a941-0f4d6dce6add/image.png" alt=""><img src="https://velog.velcdn.com/images/kong-e/post/c950761b-2668-4fcf-82dd-39cd3c56e8c3/image.png" alt=""><img src="https://velog.velcdn.com/images/kong-e/post/bd829c39-8b51-4414-b842-aa4fcf5aefa0/image.png" alt="">위의 그림들을 정리하자면 소셜로그인을 위해 아래와 같은 단계를 거친다.</p>
<p><strong>1. 클라이언트가 로그인 버튼을 누르면 구글 소셜 로그인 링크로 이동한다.
2. 클라이언트가 로그인 할 계정을 클릭한다.
3. 설정한 리디렉션 서버 URI로 구글이 인가코드(code)를 쿼리파라미터로 설정하여 보낸다. 이 인가코드는 재사용이 불가능하다.
4. 백엔드는 획득한 인가코드로 다시 구글에 계정에 대한 액세스토큰을 요청한다.
5. 백엔드는 획득한 액세스토큰으로 구글 측에 사용자 정보를 요청한다.
6. 백엔드는 획득한 유저 정보를 데이터베이스에 저장하고 JWT 토큰을 발급한다.
7. 백엔드는 발급한 JWT 토큰과 함께 클라이언트 URI로 리다이렉트를 한다.
8. 클라이언트는 백엔드로부터 JWT 토큰을 획득한다.</strong></p>
<p>내가 이번에 진행한 프로젝트는 리다이렉트 URI가 서버 URI로 설정되어 있어 이 방법으로 구현했다. 다음은 내가 작성한 코드다.</p>
<p><strong>로그인 링크로 이동(1단계)</strong></p>
<pre><code class="language-javascript">import React, { useState } from &#39;react&#39;;
import { AiOutlineClose } from &#39;react-icons/ai&#39;;
import styles from &#39;./LoginModal.module.scss&#39;;
import GoogleLogo from &#39;../../assets/images/GoogleLogo.png&#39;;

const LoginModal = ({ onModalClose }) =&gt; {
  const handleClose = () =&gt; {
    onModalClose(false);
  };
  const handleLogin = () =&gt; {
    window.location.href = &#39;http://localhost:8080/oauth2/authorization/google&#39;;
  };

  return (
    &lt;div className={styles.modalBackgound}&gt;
      &lt;div className={styles.modalContainer}&gt;
        &lt;div className={styles.modalHeader}&gt;
          &lt;div className={styles.item}&gt;로그인&lt;/div&gt;
          &lt;AiOutlineClose className={styles.button} onClick={handleClose} /&gt;
        &lt;/div&gt;
        &lt;div className={styles.modalButtonContainer}&gt;
          &lt;button className={styles.button} onClick={handleLogin}&gt;
            &lt;img src={GoogleLogo} alt=&quot;구글&quot; /&gt;
            Sign in using Google
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default LoginModal;</code></pre>
<p>이 경우는 백엔드 서버의 특정 엔드포인트로 리디렉션하고, 그 서버가 사용자를 OAuth 서비스 제공자의 인증 페이지로 리디렉션되도록 구현되어있는 경우다.
그런데 클라이언트 측에서 직접 OAuth 클라이언트 ID와 window.location.href를 아래와 같이 설정하여 이동할 수도 있다. 즉 클라이언트에서 직접 OAuth 서비스 제공자의 인증 페이지로 리디렉션할 수 있다.</p>
<pre><code>window.location.href =`https://accounts.google.com/o/oauth2/v2/auth?client_id=${GOOGLE_CLIENT_ID}&amp;redirect_uri=${GOOGLE_REDIRECT_URI}&amp;response_type=code&amp;scope=${GOOGLE_SCOPE}`</code></pre><p><strong>jwt 토큰 획득(8단계)</strong>
해당 리다이렉트 컴포넌트의 route path는 /login이고, 로그인이 성공되면 메인화면(/)으로 이동한다.
이 경우에는 토큰을 쿼리파라미터로 받아왔고, 이를 로컬스토리지에 저장했다.
그러나 이 방식이 보안상 안전한 방식은 아니다.</p>
<pre><code class="language-javascript">import React, { useEffect } from &#39;react&#39;;
import { useNavigate } from &#39;react-router&#39;;
import { useSetRecoilState } from &#39;recoil&#39;;
import { loginState } from &#39;../store/loginStore&#39;;

const LoginRedirect = () =&gt; {
  const setIsLoggedin = useSetRecoilState(loginState);
  const navigate = useNavigate();

  useEffect(() =&gt; {
    // URL에서 쿼리 파라미터 추출
    const queryParams = new URLSearchParams(window.location.search);
    const jwtToken = queryParams.get(&#39;jwt&#39;);

    if (jwtToken) {
      // 토큰을 로컬 스토리지에 저장
      localStorage.setItem(&#39;ziio-token&#39;, jwtToken);
      setIsLoggedin(true);
      navigate(&#39;/&#39;); // 홈으로 리다이렉트
    } else {
      // 토큰이 없으면 로그인 실패 처리
      console.error(&#39;로그인 실패&#39;);
    }
  }, []);

  return &lt;&gt;&lt;/&gt;;
};

export default LoginRedirect;</code></pre>
<p>이처럼 프론트엔드는 처음 로그인요청과 마지막 토큰을 받아오는 로직만 짜면 된다.</p>
<h2 id="➕response_type-설정-token-vs-code">➕response_type 설정: token vs code</h2>
<pre><code class="language-javascript">window.location.href =&quot;https://accounts.google.com/o/oauth2/auth?&quot; +
  &quot;client_id={클라이언트 ID}&amp;&quot;+
  &quot;redirect_uri={리디렉션 URI}&amp;&quot;+
  &quot;response_type=token&amp;&quot;+
  &quot;scope=https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile&quot;;</code></pre>
<p>구글 로그인 페이지로 이동할 때 response_type을 token으로 지정하는 방법과 code로 지정하는 방법이 있다.
이 파라미터는 OAuth 서버에게 클라이언트가 인증 과정을 통해 어떤 종류의 응답을 기대하는지를 알려준다. </p>
<h3 id="response_typetoken">response_type=token</h3>
<p>response_type=token은 Implicit Grant 플로우를 사용하는 것을 의미한다고 한다.
사용자가 인증을 성공하면, OAuth 서버는 액세스 토큰을 직접 반환한다. 이 토큰은 URL의 쿼리파라미터 부분에 포함되어 리디렉션 URI로 전달된다.
여기서 리디렉션 URI를 클라이언트 측으로 설정하면, FE는 액세스토큰을 받아 BE에 전송하고, BE는 이 액세스 토큰으로 OAuth 서버에 유저 정보를 요청한다.
반면, 리디렉션 URI를 서버 측으로 설정하면 BE로 액세스토큰이 바로 전송될 것이다.
response_type=token은 간단하고 클라이언트 측에서 처리하기 쉽지만 액세스 토큰이 브라우저를 통해 직접 전송되기 때문에 보안상의 위험이 더 크다. 특히 XSS(크로스 사이트 스크립팅) 공격에 취약할 수 있다.</p>
<h3 id="response_typecode">response_type=code</h3>
<p>response_type=code는 Authorization Code Grant 플로우를 사용하는 것을 의미한다고 한다.
사용자가 인증을 성공하면, OAuth 서버는 인증 코드만을 클라이언트혹은 서버에게 반환한다. 이 코드는 서버 측에서 액세스 토큰과 교환되어야 한다.
클라이언트는 받은 인증 코드를 백엔드 서버로 전송하고, 서버는 이 코드를 사용하여 OAuth 서버로부터 액세스 토큰을 요청한다.
액세스 토큰이 클라이언트 측에 직접 노출되지 않으므로, 보안성이 더 강화된다. 특히, client secret을 사용하여 토큰을 요청하므로, 보안성이 더욱 향상된다.</p>
<hr>
<h2 id="참고글">참고글</h2>
<ul>
<li><a href="https://velog.io/@nuri00/Google-OAuth-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84">https://velog.io/@nuri00/Google-OAuth-로그인-구현</a></li>
<li><a href="https://iltae.tistory.com/11">https://iltae.tistory.com/11</a></li>
<li><a href="https://www.hacksoft.io/blog/google-oauth2-with-django-react-part-2#overview">https://www.hacksoft.io/blog/google-oauth2-with-django-react-part-2#overview</a></li>
<li><a href="https://velog.io/@mannmae/%EB%A6%AC%EC%95%A1%ED%8A%B8%EB%A1%9C-%EA%B5%AC%EA%B8%80-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9D%BC%EB%8B%A8-%EA%B0%80%EC%A0%B8%EB%8B%A4-%EC%93%B0%EC%84%B8%EC%9A%94">https://velog.io/@mannmae/리액트로-구글-소셜로그인-일단-가져다-쓰세요</a></li>
<li><a href="https://www.yeti.co/blog/client-vs-server-oauth-flows-with-rest-apis">https://www.yeti.co/blog/client-vs-server-oauth-flows-with-rest-apis</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 리액트에서 타입스크립트 데코레이터의 사용]]></title>
            <link>https://velog.io/@kong-e/%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%97%90%EC%84%9C-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0%EC%9D%98-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@kong-e/%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%97%90%EC%84%9C-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0%EC%9D%98-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Wed, 08 Nov 2023 01:58:02 GMT</pubDate>
            <description><![CDATA[<p>최근에 타입스크립트를 공부하면서 데코레이터에 대해 배웠는데, 요즘 리액트에서 데코레이터를 사용하는 것을 거의 못본 것 같아 리액트에서 어떻게 사용되었는지 알아보았다.</p>
<p>알아보니 리액트가 함수형 컴포넌트 방식으로 전환되면서 데코레이터는 거의 사용하지 않게 되었다고 한다.</p>
<hr>
<p>최신 웹 개발 트렌드는 리액트에서 함수형 컴포넌트의 사용을 적극 권장하고 있다. 이에 따라, 과거 클래스 컴포넌트에서 널리 사용되던 데코레이터의 사용 빈도가 점점 감소하고 있다. 함수형 컴포넌트는 훅(Hook)을 통해 상태 관리, 사이드 이펙트 처리 등을 할 수 있으므로, 데코레이터를 사용할 필요성이 크게 줄어들었다.</p>
<h3 id="함수형-컴포넌트와-훅의-부상">함수형 컴포넌트와 훅의 부상</h3>
<p>함수형 컴포넌트는 클래스 기반의 컴포넌트보다 간결하고, 훅을 사용하여 다양한 리액트 기능을 손쉽게 통합할 수 있다. 특히 리액트 16.8의 훅 도입으로 함수형 컴포넌트에서도 상태 관리, 생명주기 메서드, 컨텍스트 사용이 가능해졌다.</p>
<h3 id="클래스-컴포넌트의-데코레이터-한계">클래스 컴포넌트의 데코레이터 한계</h3>
<p>클래스 컴포넌트에서 <code>this</code> 바인딩은 오랜 기간 개발자들에게 혼란과 불편함을 야기했다. 예를 들어, 리액트 컴포넌트의 메서드를 이벤트 핸들러로 사용할 때 <code>this</code>가 예상대로 바인딩되지 않으면, 상태나 props에 접근할 수 없는 문제가 발생한다.</p>
<pre><code class="language-jsx">class App extends React.Component {
  state = {
    message: &#39;Hello!&#39;
  };

  // 이 메서드는 기본적으로 인스턴스에 바인딩되지 않는다.
  showMessage() {
    alert(this.state.message);
  }

  render() {
    // showMessage가 호출될 때 this는 undefined를 가리키게 된다.
    return &lt;button onClick={this.showMessage}&gt;Show Message&lt;/button&gt;;
  }
}</code></pre>
<p>이 문제를 해결하기 위해 <code>AutoBind</code>와 같은 데코레이터가 등장했다. <code>AutoBind</code>는 메서드에 적용되어 자동으로 <code>this</code>를 인스턴스에 바인딩해 준다.</p>
<pre><code class="language-typescript">// AutoBind 데코레이터 정의
function AutoBind(_: any, _2: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  const adjDescriptor: PropertyDescriptor = {
    configurable: true,
    get() {
      return originalMethod.bind(this);
    }
  };
  return adjDescriptor;
}

class App extends React.Component {
  state = {
    message: &#39;Hello!&#39;
  };

  @AutoBind
  showMessage() {
    alert(this.state.message);
  }

  render() {
    // showMessage가 호출될 때 this는 항상 App 인스턴스를 가리킨다.
    return &lt;button onClick={this.showMessage}&gt;Show Message&lt;/button&gt;;
  }
}</code></pre>
<p>그러나 함수형 컴포넌트와 훅의 등장으로, 이러한 복잡성을 해결할 새로운 패턴이 나타났다. 예를 들어, <code>useState</code>와 <code>useCallback</code> 훅을 사용하여 클래스 컴포넌트의 상태 관리와 이벤트 핸들러를 다음과 같이 간단히 표현할 수 있다.</p>
<pre><code class="language-jsx">const App = () =&gt; {
  const [message, setMessage] = useState(&#39;Hello!&#39;);

  // useCallback 훅을 사용하여 showMessage 함수를 메모이제이션한다.
  const showMessage = useCallback(() =&gt; {
    alert(message);
  }, [message]);

  return &lt;button onClick={showMessage}&gt;Show Message&lt;/button&gt;;
};</code></pre>
<p>이처럼 현대 리액트 개발에서는 함수형 컴포넌트와 훅을 통해 데코레이터가 제공했던 기능을 대체하고 있다. 이로 인해 데코레이터의 사용이 감소하고 있으며, 리액트 개발에서 함수형 컴포넌트와 훅이 주류를 이루고 있다. 개발자들은 이제 데코레이터의 복잡성을 피하고, 훅을 통해 더욱 간결하고 직관적인 코드를 작성하고 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] useState와 const (리액트 라이프사이클)]]></title>
            <link>https://velog.io/@kong-e/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%9D%BC%EC%9D%B4%ED%94%84%EC%82%AC%EC%9D%B4%ED%81%B4-useState%EC%99%80-const</link>
            <guid>https://velog.io/@kong-e/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%9D%BC%EC%9D%B4%ED%94%84%EC%82%AC%EC%9D%B4%ED%81%B4-useState%EC%99%80-const</guid>
            <pubDate>Sat, 04 Nov 2023 13:20:17 GMT</pubDate>
            <description><![CDATA[<p>프로필 편집 기능을 구현하다가 컴포넌트 내에 선언된 <code>useState</code>와 일반 <code>const</code> 변수의 행동 차이를 유의미하게 관찰할 수 있게 되었다😶‍</p>
<p><code>useState</code>로 선언된 상태 변수 <code>user</code>와 이를 참조하는 <code>const</code>로 선언된 변수 <code>preUserIntroduction</code> 간의 상호작용에서 예상치 못한 결과를 마주했다. </p>
<p>나는 이것이 깊은 복사(deep copy)/얕은 복사(shallow copy) 개념과 관련이 있는 것인줄 알았다. 마치 <code>state</code> 변수는 깊은 복사를 하는 것이고, <code>const</code> 변수는 얕은 복사를 하는 것인가 싶었다. </p>
<p>그러나 나의 리액트 컴포넌트 라이프사이클에 대한 이해 부족이었다.
결론적으로는 둘 다 얕은 복사를 하는 것이고, <code>const</code> 변수는 컴포넌트가 마운트 될 때마다 참조되는 값이 새로 갱신되는 것일 뿐이었다.</p>
<pre><code class="language-javascript">export const EditIntroduction = ({ navigation }) =&gt; {
  const [user, setUser] = useRecoilState(userState);
  const preUserIntroduction = user.introduction;

  const onPressConfirm = () =&gt; {
    navigation.navigate(&#39;EditProfile&#39;);
  };

  const onPressCancel = () =&gt; {
    setUser({ ...user, introduction: preUserIntroduction });
    navigation.navigate(&#39;EditProfile&#39;);
  };

  // 생략된 UI 렌더링 부분
};</code></pre>
<p>위의 코드에서 나는 <code>preUserIntroduction</code>이 <code>user.introduction</code>의 초기값을 그대로 유지하기를 바랐다.
그런데 사용자가 <code>TextInput</code>의 onChange를 통해 <code>user.introduction</code>을 변경하면 <code>const preUserIntroduction</code>도 업데이트되는 현상이 있었다. 그래서 취소 버튼을 통해 <code>onPressCancel</code> 함수가 호출되더라도 초기의 user 값이 아닌 onChange로 인해 변경된 user 값이 user 상태 변수에 반영되었다.</p>
<pre><code class="language-javascript">export const EditIntroduction = ({ navigation }) =&gt; {
  const [user, setUser] = useRecoilState(userState);
  const [preUserIntroduction, setPreUserIntroduction] = useState[user.introduction];

  const onPressConfirm = () =&gt; {
    navigation.navigate(&#39;EditProfile&#39;);
  };

  const onPressCancel = () =&gt; {
    setUser({ ...user, introduction: preUserIntroduction });
    navigation.navigate(&#39;EditProfile&#39;);
  };

  // 생략된 UI 렌더링 부분
};</code></pre>
<p>반면 <code>preUserIntroduction</code>을 <code>useState</code>로 관리했을 때는 <code>TextInput</code>의 onChange를 통해 <code>user.introduction</code>을 변경하더라도 <code>preUserIntroduction</code>이 초기값을 잘 유지했다. 그래서 <code>onPressCancel</code> 함수가 호출되면 user 상태 변수에 초기의 user 값이 잘 반영되었다.</p>
<p>리액트의 렌더링 메커니즘에 따라<code>const</code> 변수는 컴포넌트의 렌더링마다 새로운 값으로 초기화된다. 컴포넌트의 상태가 변경되어 재렌더링이 발생하면, <code>const</code> 변수는 새로운 상태 값으로 갱신된다.
반면, <code>useState</code>로 선언된 상태는 컴포넌트가 재렌더링 되어도 이전 상태를 유지하며, 오직 설정된 업데이트 함수를 통해서만 변경될 수 있다. 그래서 마치 깊은 복사를 하는 것처럼 보였던 것이다.</p>
<p>const와 state 변수의 차이를 이번에 제대로 알게 되었다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[URI? URL? URN?]]></title>
            <link>https://velog.io/@kong-e/URI-URL-URN</link>
            <guid>https://velog.io/@kong-e/URI-URL-URN</guid>
            <pubDate>Sun, 29 Oct 2023 14:56:04 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kong-e/post/e94669e9-7f2d-490b-bbbd-62ab9f58c187/image.jpeg" alt=""></p>
<h1 id="uri">URI</h1>
<ul>
<li>정보나 데이터 같은 리소스를 식별하기 위한 기술 방법</li>
<li>컴퓨터가 다루는 리소스뿐만 아니라 사람이나 회사, 서적 등 다양한 리소를 나타낼 수 있음<h2 id="url">URL</h2>
</li>
<li>URI 중 리소스가 존재하는 위치를 나타내는 것</li>
<li>URL에는 리소스의 위치를 나타내는 정보 외에, 리소스를 얻는 방법이 기술되어 있음</li>
<li>웹사이트의 위치를 나타낼 때 사용<h2 id="urn">URN</h2>
</li>
<li>URI 중 위치에 관계없이 리소스의 이름을 나타내는 것</li>
<li>간행된 서적을 고유하게 특정해 식별하기 위한 ISBN 코드 등을 사용해 기술 가능<h2 id="스킴별-표현-형식">스킴별 표현 형식</h2>
</li>
<li><code>http:</code> : //example.com/news/index.html</li>
<li><code>ftp:</code> : //example.com/docs/news01.doc</li>
<li><code>urn:</code> : isbn:0-123-45678-9</li>
<li><code>urn:</code> : ietf:rfc2648<h2 id="요청-uri">요청 URI</h2>
</li>
<li>HTTP에서도 리소스를 특정하기 위해 URI 사용</li>
<li>요청 행의 메서드에 이어서 기술<h3 id="절대-uri">절대 URI</h3>
<code>GET http://example.com/news/index.html HTTP/1.1</code>
HTTP 요청이 프락시 서버를 경유할 때는 절대 URI 사용<h3 id="상대-uri">상대 URI</h3>
<code>GET /news/index.html HTTP/1.1</code>
일반적으로는 상대 URI를 사용해 HTTP 요청 송신<h2 id="퍼센트-인코딩">퍼센트 인코딩</h2>
</li>
<li>예약 문자(!, #, &amp; 등의 기호)도 비예약 문자(숫자, 알파벳 등)도 아닌 문자를 URI에서 사용할 때는 퍼센트 인코딩이라는 방법 사용해 해당 문자를 변환해야 함</li>
<li>&#39;%&#39;에 이어서 표기할 수 없는 문자의 문자 코드를 16진수로 표시</li>
<li>사용하는 문자 코드(EUC-KR, URF-8)에 따라 퍼센트 인코딩 변환 결과가 다름</li>
</ul>
<hr>
<p><strong>참고자료</strong>
웹의 기초, 위키북스
모던 자바스크립트 딥다이브 p. 662</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] NFC 감지 기능 만들기]]></title>
            <link>https://velog.io/@kong-e/React%EB%A1%9C-NFC-%ED%83%AD-%EA%B8%B0%EB%8A%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@kong-e/React%EB%A1%9C-NFC-%ED%83%AD-%EA%B8%B0%EB%8A%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Fri, 27 Oct 2023 14:57:16 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@kong-e/TIL-NFC%EB%A1%9C-%EB%B8%94%EB%A1%9D%EC%B2%B4%EC%9D%B8-%EC%A7%80%EA%B0%91-%EC%97%B0%EA%B2%B0%ED%95%98%EB%8A%94-%EA%B8%B0%EB%8A%A5">이 글</a>에서 NFC를 탭하면 블록체인 지갑을 만들어주는 기능에 대해 글을 썼었다.</p>
<p>그런데 useWalletAuth라는 지갑 생성 hook에 지갑 생성 기능 외에도 NFC를 감지하는 기능도 들어있어 단일책임원칙(SRP)을 위반하는 것 같아 리팩토링을 진행했다.</p>
<p>NFC 기능은 useNfc라는 hook으로 빼고
매개변수에 nfc가 감지된 후에 실행할 함수를 넣도록 했다.</p>
<pre><code class="language-javascript">import { useState } from &quot;react&quot;;

export const useNfc = (nextFunction: () =&gt; Promise&lt;void&gt;) =&gt; {
  const [nfc, setNfc] = useState&lt;any&gt;(null);
  const [nfcSerialNumber, setNfcSerialNumber] = useState&lt;string | null&gt;(null);
  const [isNfcConnecting, setIsNfcConnecting] = useState(false);

  async function handleNfcReading() {
    if (typeof NDEFReader === &quot;undefined&quot;) {
      console.log(&quot;NFC is not supported in this browser.&quot;);
      return;
    }

    try {
      console.log(&quot;NFC Reading Start&quot;);
      const ndef = new NDEFReader();
      setNfc(ndef);

      await ndef.scan();

      setIsNfcConnecting(true);

      ndef.addEventListener(&quot;readingerror&quot;, () =&gt; {
        console.log(&quot;Argh! Cannot read data from the NFC tag. Try another one?&quot;);
        setIsNfcConnecting(false);
      });

      ndef.addEventListener(&quot;reading&quot;, (event: any) =&gt; {
        const { message, serialNumber } = event;

        // setNfcMessage(message);
        setNfcSerialNumber(serialNumber);
        setIsNfcConnecting(false);
        nextFunction();
      });
    } catch (e) {
      console.log((e as Error).message);
    }
  }

  return {
    nfc,
    nfcSerialNumber,
    isNfcConnecting,
    handleNfcReading,
  };
};</code></pre>
<p>아래처럼 useNfc hook을 이용한다.</p>
<pre><code class="language-javascript">&quot;use client&quot;;

import {
  ComethProvider,
  ComethWallet,
  ConnectAdaptor,
  SupportedNetworks,
} from &quot;@cometh/connect-sdk&quot;;
import { useState } from &quot;react&quot;;
import { useWalletContext } from &quot;./useWalletContext&quot;;
import { useNfc } from &quot;./useNfc&quot;;
import { ethers } from &quot;ethers&quot;;
import { COUNTER_ABI } from &quot;@/abi/counter&quot;;
import { TOKEN_ABI } from &quot;@/abi/sample-token&quot;;

export function useWalletAuth() {
  const { isNfcConnecting, nfcSerialNumber, handleNfcReading } = useNfc(connect);
  const { wallet, setWallet, setProvider, contract, setContract } = useWalletContext();
  const [isConnecting, setIsConnecting] = useState(false);
  const [isConnected, setIsConnected] = useState(false);
  const [connectionError, setConnectionError] = useState&lt;string | null&gt;(null);

  const apiKey = process.env.NEXT_PUBLIC_COMETH_API_KEY;
  const COUNTER_CONTRACT_ADDRESS = &quot;0x3633A1bE570fBD902D10aC6ADd65BB11FC914624&quot;;
  const MATIC_ADDRESS = &quot;0x0000000000000000000000000000000000001010&quot;;

  function displayError(message: string) {
    setConnectionError(message);
  }

  async function connect() {
    if (!apiKey) throw new Error(&quot;no apiKey provided&quot;);

    try {
      const walletAdaptor = new ConnectAdaptor({
        chainId: SupportedNetworks.MUMBAI,
        apiKey,
      });

      const instance = new ComethWallet({
        authAdapter: walletAdaptor,
        apiKey,
      });

      const localStorageAddress = window.localStorage.getItem(&quot;walletAddress&quot;);

      if (localStorageAddress) {
        const parsedLocalStorageAddress = JSON.parse(localStorageAddress);
        if (parsedLocalStorageAddress[nfcSerialNumber!]) {
          // if it is in localStorage, connect to it
          await instance.connect(parsedLocalStorageAddress[nfcSerialNumber!]);
        } else {
          // if it is not in localStorage, connect to it and save it to localStorage
          await instance.connect();
          const walletAddress = await instance.getAddress();
          parsedLocalStorageAddress[nfcSerialNumber!] = walletAddress;
          window.localStorage.setItem(&quot;walletAddress&quot;, JSON.stringify(parsedLocalStorageAddress));
        }
      } else {
        // if there is no localStorage, connect to it and save it to localStorage
        await instance.connect();
        const walletAddress = await instance.getAddress();
        const addressObject = { [nfcSerialNumber!]: walletAddress };
        window.localStorage.setItem(&quot;walletAddress&quot;, JSON.stringify(addressObject));
      }

      const instanceProvider = new ComethProvider(instance);

      // const contract = new ethers.Contract(MATIC_ADDRESS, TOKEN_ABI, instanceProvider.getSigner());

      const contract = new ethers.Contract(
        COUNTER_CONTRACT_ADDRESS,
        COUNTER_ABI,
        instanceProvider.getSigner()
      );

      setContract(contract);

      setIsConnected(true);
      setWallet(instance as any);
      setProvider(instanceProvider as any);
    } catch (e) {
      displayError((e as Error).message);
    } finally {
      setIsConnecting(false);
    }
  }

  async function disconnect() {
    if (wallet) {
      try {
        await wallet!.logout();
        setIsConnected(false);
        setWallet(null);
        setProvider(null);
        // setContract(null);
      } catch (e) {
        displayError((e as Error).message);
      }
    }
  }

  return {
    wallet,
    connect,
    disconnect,
    isConnected,
    isConnecting,
    isNfcConnecting,
    nfcSerialNumber,
    handleNfcReading,
    connectionError,
    setConnectionError,
    contract,
    setContract,
  };
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠키, 스토리지, 세션]]></title>
            <link>https://velog.io/@kong-e/%EC%BF%A0%ED%82%A4-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80-%EC%84%B8%EC%85%98</link>
            <guid>https://velog.io/@kong-e/%EC%BF%A0%ED%82%A4-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80-%EC%84%B8%EC%85%98</guid>
            <pubDate>Thu, 26 Oct 2023 14:45:12 GMT</pubDate>
            <description><![CDATA[<h1 id="쿠키">쿠키</h1>
<p>서버에서 유저의 웹 브라우저에 보낼 수 있는 작은 데이터 (4kb 정도)</p>
<h2 id="목적">목적</h2>
<ul>
<li>세션 관리 : 로그인 하고 있는 유저 정보, 장바구니 등 기억해야 하는 정보</li>
<li>개인화 : 유저 설정, 테마 정보 등</li>
<li>추적 : 유저 행동 추적<h1 id="세션">세션</h1>
쿠키를 이용해 구현되나 서버 안에 별도의 세션 스토어를 가지고 있음<h1 id="세션쿠키">세션쿠키</h1>
</li>
<li>보관 기간이 정해지지 않은 쿠키 (max-age, expires 지정X)</li>
<li>브라우저를 닫을 때 쿠키도 항상 같이 삭제<h1 id="쿠키와-세션의-차이">쿠키와 세션의 차이</h1>
</li>
<li>모범답안: 쿠키는 로컬 컴퓨터에 저장되어 보안에 취약하고, 세션은 사용자의 정보를 서버에 저장하기 때문에 쿠키에 비해서 안전한 편이다</li>
<li>보충답안: 세션도 쿠키를 이용해 구현되나, 세션은 서버에 저장되는 세션 스토어가 있어야 하고 서버가 임의의 시점에 해당 세션을 무효화 시킬 수 있는 특징이 있다. 쿠키는 브라우저 헤더에 담겨서 클라이언트와 지속적으로 주고 받는 과정에서 서버가 임의로 무효화 하기 힘들다.</li>
<li>세션도 쿠키를 통해 구현하는 것이기 때문에 세션과 쿠키를 딱 잘라 구별하는 것은 옳지 않음</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP 캐시]]></title>
            <link>https://velog.io/@kong-e/TIL-HTTP-%EC%BA%90%EC%8B%9C</link>
            <guid>https://velog.io/@kong-e/TIL-HTTP-%EC%BA%90%EC%8B%9C</guid>
            <pubDate>Wed, 25 Oct 2023 14:52:31 GMT</pubDate>
            <description><![CDATA[<p>캐시에는 항상 위계가 있다.</p>
<ul>
<li>CDN은 캐시 서버를 구성하는 물리/논리적 방법에 대한 이야기</li>
<li>HTTP Cache-Control은 개별 HTTP 요청의 캐싱 정책을 다룸</li>
</ul>
<h1 id="http-cache">HTTP Cache</h1>
<ul>
<li>HTTP 캐시 위계 중 서버와 브라우저 사이에 위치한다.</li>
<li>CDN이 주든, Origin 서버가 주든 관계 없이 HTTP 캐시가 어떻게 적재되고 어떻게 관리할 것인지에 관한 문제 </li>
<li>CDN은 인프라의 영역이라면, HTTP 캐시는 개발의 영역이다.<h2 id="max-age">max-age</h2>
</li>
<li>캐시의 유효 기간을 설정한다.</li>
<li><strong>&#39;서버가 데이터를 발행한 시점을 기준으로 몇 초동안 캐시가 유효할 것인가?&#39;</strong>를 정함</li>
<li>유저 입장에서의 max-age라고 볼 수 있으나 s-max-age 헤더 키가 없다면 공유 프록시도 max-age 사용<h2 id="s-max-age">s-max-age</h2>
</li>
<li>CDN을 위한 max-age</li>
<li>공유 proxy에서 저장되는 최대 시간</li>
<li>프록시 서버(중계 서버)에서의 max-age<h2 id="max-stale">max-stale</h2>
</li>
<li>만료된 캐시의 사용 최대 시간이 얼마까지 인지</li>
<li>stale한 캐시를 쓰는 데에 대한 타임아웃</li>
<li>stale한 캐시를 보여주고 그 사이에 revalidate를 시킨 다음 revalidate를 마치면 stale한 캐시는 날아감<h2 id="expires">Expires</h2>
</li>
<li>HTTP 1.0에서 제시된 캐시 유효기간 헤더</li>
<li>날짜 파싱 버그로 지금은 사용 추천하지 않음</li>
<li>아주 예전의 브라우저를 지원하기 위해 사용하는 경우는 있음<h2 id="age">Age</h2>
</li>
<li>Proxy cache에서 넘어온 HTTP 응답에 대해 보관한 시간을 설명</li>
<li>보통 서비스는 직접 발행하기 때문에 0이 일반적이다.<h2 id="access-control-max-age">Access-Control-Max-Age</h2>
</li>
<li>Preflight 요청(OPTIONS 메소드로 이뤄지는 요청)에 대한 응답인 Access-Control-Allow-Methods에 대한 유효기간<h2 id="no-cache">no-cache</h2>
</li>
<li>간단하게 캐시하지 않음을 나타낼 수 있음</li>
<li>age=0, must-revalidate와 동일함</li>
<li>리소스를 요청할 때마다 origin 서버에 검증을 요청함</li>
<li>브라우저에 저장은 하고 매번 원 서버에 검증을 요청함<h2 id="no-store">no-store</h2>
</li>
<li>아무도 캐시하지 마라</li>
<li>리소스를 하드디스크에도 저장하지 않고 항상 서버에서 가져옴</li>
<li>프록시 포함 저장하지 않음(CDN도 캐시하지 않도록 함)<h2 id="stale">stale</h2>
</li>
<li>캐시가 시간이 지나서 다시 갱신이 필요할 때</li>
<li>max-age가 지나고 나서부터 stale 상태로 들어감</li>
<li>stale-while-revalidate(SWR) : stale 상태 동안에도 캐시 정보는 다시 쓴다.</li>
<li>stale-if-error : 서버에서 에러가 발생하면 stale 상태에서도 캐시를 재활용한다.<h2 id="etag">Etag</h2>
</li>
<li>브라우저에서 캐시 정보를 갱신할 때 버전 리소스를 확인하는 코드</li>
<li>클라이언트는 서버에 현재 내가 가지고 있는 캐시된 리소스 Etag 전송<h3 id="if-match">If-Match</h3>
</li>
<li>If-Match 헤더를 포함하여 요청을 전송할 경우, 서버는 헤더에 포함된 ETag 값과 서버의 리소스 ETag 값이 일치하는지 확인한다. 일치하면 요청이 정상적으로 처리되고, 일치하지 않으면 서버는 일반적으로 412 Precondition Failed 응답을 반환한다.<h3 id="if-none-match">If-None-Match</h3>
</li>
<li>요청 헤더에 포함된 ETag 값과 서버의 리소스 ETag 값이 일치하지 않으면 갱신된 ETag 값을 보낸다.</li>
<li>리소스가 바뀌었으나 실제로는 그렇게 중요하지 않은 변경 사항이어서 옛날 리소스를 그대로 써도 되는 경우 = 유의미한 변경 내용에만 캐시를 변경하고 싶을 때<h3 id="if-modified-since">If-Modified-Since</h3>
</li>
<li>특정 날짜 기준으로 캐시가 바뀌었는지 안바뀌었는지 판별<h3 id="if-modified-since-vs-if-none-match">If-Modified-Since vs If-None-Match</h3>
</li>
<li>If-Modified-Since 기준으로는 캐시를 갈아야 하는데 If-None-Match 기준으로는 Etag가 일치하는 경우?<ul>
<li>보통 304 Not Modified</li>
<li>실제 RFC를 확인하여 권장사항을 확인해보긴 해야함<h3 id="조건부-헤더의-경쟁-조건-해지">조건부 헤더의 경쟁 조건 해지</h3>
<img src="https://velog.velcdn.com/images/kong-e/post/7520eb67-157a-46f3-ad9c-c6ccc392f3dc/image.png" alt=""><h2 id="min-fresh">min-fresh</h2>
</li>
</ul>
</li>
<li>클라이언트가 캐시가 필요한 시간을 지정할 수 있음</li>
<li>예를 들어 1일 이상 살아남을 수 있는 캐시를 요청할 수 있음</li>
<li>서버의 판단에 따라 응답이 달라짐<h2 id="must-revalidate">must-revalidate</h2>
</li>
<li>항상 캐시를 해선 안되고 만료된 리소스를 사용하면 안됨<h2 id="no-transform">no-transform</h2>
</li>
<li>프록시 서버 등이 임의로 HTTP 헤더 내용을 변경해서는 안됨<h2 id="public-private">public, private</h2>
</li>
<li>public : 공개 캐시에 저장될 수 있는 캐시</li>
<li>private : 특정 사용자/그룹만 사용해야 하는 캐시<ul>
<li>그러나 실무에서는 private 캐시는 사용하지 않을 가능성이 높다. 데이터의 주소를 적당히 숨김으로써 주소를 확보하는 편이다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP 1.1 & HTTP 2.0 & HTTP 3.0 & CDN]]></title>
            <link>https://velog.io/@kong-e/TIL-HTTP-CDN</link>
            <guid>https://velog.io/@kong-e/TIL-HTTP-CDN</guid>
            <pubDate>Sun, 22 Oct 2023 14:38:07 GMT</pubDate>
            <description><![CDATA[<h1 id="http-11">HTTP 1.1</h1>
<ul>
<li>인터넷 연결을 위한 TCP application protocol</li>
<li>request-response 모델 : 하나의 request이 발생하면 response가 나가고 연결이 끝남</li>
<li>Text 기반 Protocol : 어떤 프로토콜은 바이너리 형식으로 묶여 있어 내용을 이해하기 어려운 경우도 있지만 HTTP는 텍스트 형식으로 주어져 읽기 편하다.</li>
<li>Chunk 단위의 Encoding</li>
<li>Compression(압축) 지원</li>
<li>인증 쪽에서는 Cookie, Session 지원</li>
<li>Virtual Host 지원</li>
</ul>
<h2 id="http11-header-분석">HTTP/1.1 Header 분석</h2>
<p><img src="https://velog.velcdn.com/images/kong-e/post/82313eaa-8dbb-4c29-b2e6-f9a574a504fc/image.png" alt=""><img src="https://velog.velcdn.com/images/kong-e/post/6a78aca4-8a7d-42a2-a9af-1a6fb6850df0/image.png" alt=""></p>
<h3 id="status-line">Status Line</h3>
<ul>
<li>요청<ul>
<li>{METHOD} {TARGET} {HTTP/VERSION}</li>
<li>ex. POST / HTTP/1.1</li>
</ul>
</li>
<li>응답<ul>
<li>{HTTP/VERSION} {STATUS_CODE} {REASON}</li>
<li>ex. HTTP/1.1 403 Forbidden</li>
</ul>
</li>
</ul>
<h3 id="http-status-code">HTTP STATUS CODE</h3>
<ul>
<li>200대 : 정상적으로 성공했을 때</li>
<li>300대 : 리다이렉션이 필요할 때(다른 데에 자료가 있을 때)</li>
<li>400대 : request의 정보가 오류가 있을 때, request를 잘못 보냈을 때</li>
<li>500대 : 서버가 죽었을 때</li>
</ul>
<h3 id="http-method">HTTP Method</h3>
<ul>
<li>GET : 리소스 조회</li>
<li>POST : 리소스 추가</li>
<li>PUT : 리소스 교체</li>
<li>DELETE : 리소스 삭제</li>
<li>PATCH : 리소스 변경</li>
<li>OPTIONS : 메소드 조회, 해당 타겟에 어떤 메소드들이 가능한지 조회</li>
<li>HEAD : GET 상태 조회, BODY는 관심 없고 HEAD만 가져오겠다 할 때(서비스가 정상적으로 살아있는지만 가져오기 위해)</li>
<li>HTTP Method는 CRUD에 대응 가능하다<ul>
<li>Create = POST</li>
<li>Read = GET</li>
<li>Update = PUT</li>
<li>Delete = DELETE</li>
</ul>
</li>
<li>PUT vs POST<ul>
<li>원칙 : 멱등성(Idempotent)</li>
<li>POST는 idempotent 하지 않다.</li>
<li>PUT은 idepotent 하다.</li>
<li>사례 : 주민등록 시스템에 출생신고 하기<ul>
<li>공무원이 김민수라는 사람에 대해 출생신고 버튼을 연속 3번 눌렀을 경우</li>
<li>POST : 김민수라는 동명이인이 3명 등록됨</li>
<li>PUT : 두번째, 세번째 요청은 주민등록번호가 같기 때문에 버려짐</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="http-header">HTTP HEADER</h3>
<h3 id="표준-헤더">표준 헤더</h3>
<p><strong>User Agent</strong></p>
<ul>
<li>서버나 네트워크가 어플리케이션이나 운영체제 장비나 버전을 감지하기 위한 문자열</li>
<li>운영체제, 브라우저 프로그램, 디바이스 등 분리 가능</li>
<li>User-Agent : <product> / <product-version> <comment></li>
<li>User Agent 읽는 법
<code>Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)</code><ul>
<li>Mozilla = Application Name</li>
<li>/4.0 = Application Version</li>
<li>compatible = Compatiblity Flag</li>
<li>MSIE 7.0 = Version Token</li>
<li>Windows NT 6.0 = Platform Token</li>
</ul>
</li>
<li>최근 User Agent의 중요성이나 가치가 크게 떨어진 이유는?<ul>
<li>웹 개발자<ul>
<li>과거에 User-Agent를 체크해서 개발을 했다.</li>
<li>브라우저마다 동작이 조금씩 상이하니 User Agent 기반으로 페이지를 개발(User Agent가 모질라면 이렇게 만들고, 익스플로러면 이렇게 만들고...)</li>
</ul>
</li>
<li>브라우저 개발자<ul>
<li>우리는 모질라 것도 잘나오고, 익스플로러 것도 잘 나와. 모질라도 넣고 인터넷 익스플로러도 넣자.</li>
<li>역으로 User Agent를 꼬아서 호환성을 만드려 함</li>
</ul>
</li>
</ul>
</li>
<li></li>
</ul>
<p><strong>Accept</strong></p>
<ul>
<li>응답이 어떻게 넘어와야 하는지 설명해주는 헤더</li>
<li>Aceept : 컨텐츠 타입이 무엇인지 (이미지, 텍스트, 오디오...)<ul>
<li>Accept: text/html, application/xhtml+xml, application/xml;q=0.9, <em>/</em>;q=0.8<ul>
<li>text/html은 가중치가 1, application/xhtml+xml도 가중치가 1, application/xml은 0.9,  <em>/</em>(전체)은 가중치가 0.8</li>
<li>서버가 가중치 순서대로 보낼 수도 있고, 이미지만 받는 장치라면 가중치가 0.1인 이미지를 보낼 수 도음</li>
</ul>
</li>
<li>Accept-Encoding: 인코딩이 무엇인지 (gzip 압축인지, 풀어서 오는지…)<ul>
<li>Accept-Encoding: deflate, gzip;q=1.0, *;q=0.5</li>
</ul>
</li>
<li>Accept-Language: 이해할 수 있는 언어가 무엇인지<ul>
<li>Accept-Language: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><strong>Content</strong></p>
<ul>
<li>서버가 응답한 컨텐츠를 설명해주는 헤더<ul>
<li>Content-Type: 컨텐츠가 이미지인지, 텍스트인지 등<ul>
<li>Content-Type: text/html; charset=utf-8</li>
</ul>
</li>
<li>Content-Length: 컨텐츠의 크기를 반환<ul>
<li>Content-Length: <length></li>
</ul>
</li>
<li>Content-Encoding: 컨텐츠의 인코딩 형식을 설명함<ul>
<li>Content-Encoding: deflate, gzip</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><strong>Access-Control-Allow-Origin</strong></p>
<ul>
<li>CORS</li>
</ul>
<p><strong>Access-Control-Request-Headers</strong></p>
<ul>
<li>사용 가능한 헤더</li>
</ul>
<p><strong>Access-Control-Request-Method</strong></p>
<ul>
<li>사용 가능한 메소드</li>
</ul>
<p><strong>Access-Control-Max-Age</strong></p>
<ul>
<li>preflight Request 보존 연한</li>
</ul>
<p><strong>Authorization</strong></p>
<ul>
<li>인증에 주로 사용되는 헤더</li>
</ul>
<h3 id="비표준-헤더x-api">비표준 헤더(X-API)</h3>
<ul>
<li>헤더의 키 값은 콜론으로 구분되기 때문에 개발자가 임의로 만든 키에 임의의 밸류를 넣을 수 있음</li>
<li>X-API의 경우 키가 &#39;x-&#39;로 시작하는 비표준 헤더</li>
</ul>
<h1 id="http-over-tcp">HTTP over TCP</h1>
<ul>
<li><p>Keep-Alive 옵션</p>
<ul>
<li><p>HTTP Request 이후에 TCP Connection이 닫히지 않게 해준다.</p>
</li>
<li><p>TCP Connection 맺는데 비용이 발생</p>
</li>
<li><p>HTTP는 보통 여러개 요청 보내기 마련, 커넥션을 살려두면 재활용이 가능하다. = Persistant HTTP</p>
<p><img src="https://velog.velcdn.com/images/kong-e/post/69b305a2-c201-4852-b27e-81c2c204331f/image.png" alt=""></p>
</li>
</ul>
</li>
</ul>
<ul>
<li><p>Pipelining</p>
<ul>
<li><p>여러 개의 HTTP 요청을 하나의 TCP Connection에 보내준다</p>
</li>
<li><p>각각의 HTTP 요청을 처리하려면 대기가 길다</p>
</li>
<li><p>여러개 필요한 상황이면 한 번에 받아오자</p>
<p><img src="https://velog.velcdn.com/images/kong-e/post/f58c4382-4ce7-47c6-a781-6d54dc1b0abb/image.png" alt=""></p>
</li>
</ul>
</li>
</ul>
<ul>
<li>Chunked Encoding<ul>
<li>메세지 바디를 조각내서 전송할 수 있다.</li>
</ul>
</li>
<li>Range 요청<ul>
<li>비디오 스트리밍을 요청하기 위해서 잘린 구간을 요청할 수 있다.</li>
</ul>
</li>
</ul>
<h1 id="http-20-spdy">HTTP 2.0 (SPDY)</h1>
<ul>
<li>구글이 2009년에 제안한 SPDY 프로토콜</li>
<li>웹 표준 WG에서 SPDY를 기반으로 한 2.0을 정의함</li>
<li>특징<ul>
<li>Binary Protocol<ul>
<li>96만 5432라는 숫자를 만약에 우리가 데이터를 보낸다고 가정. 문자열로 보내려면 9 6 5 4 3 2 여섯 글자니까 6바이트로 데이터를 보내야 된다. 근데 만약에 숫자로 보낸다고 하면, 대충 4바이트 인트 변수 하나에 그냥 날릴 수 있다.</li>
<li>HTTP 프로토콜을 처음 만들 때에는 1kb, 2kb, 이런 내용만 주고받을 줄 알아서 설계했었던 프로토콜이었는데. 여기에 사람들이 이미지도 보내고 음악도 보내고 하다못해 이제 동영상까지 보내기 시작하니까 텍스트로 보내는 게 너무 비효율적이 돼서 이 바이너리로 바꾸는 프로토콜을 제안.</li>
</ul>
</li>
<li>Header Compression<ul>
<li>이전에는 바디 쪽은 gzip 압축을 했지만, 헤더 부분은 압축에 대한 스펙 정의하지 않았었다.</li>
</ul>
</li>
<li>Multiplexing<ul>
<li>데이터 여러 개 보내면 계층을 나눠 한 번에 데이터를 많이 보낼 수 있게 멀티플렉싱도 하겠다.</li>
</ul>
</li>
<li>Server Push<ul>
<li>클라이언트가 서버한테 데이터를 무조건 보내야만 HTTP 프로토콜이 시작이 됐었는데, 서버에서 먼저 데이터를 보내주는 것도 하나의 프로토콜로 정의를 하겠다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="http-30-quic">HTTP 3.0 (QUIC)</h1>
<ul>
<li>TCP 대신 UDP로 인터넷 커넥션을 생성</li>
</ul>
<h1 id="cdn">CDN</h1>
<ul>
<li>개별 서버 장소에 인터넷 사용자가 많은 서비스에 대응하기 위해 복수의 서버를 네트워크로 연결하여 구성</li>
<li>접속 경로 최적화 효과<ul>
<li>지리 데이터 정보에서 사용자 PC까지의 경로 탐색하여 가장 빠른 경로로 연결</li>
</ul>
</li>
<li>Cache 기능<ul>
<li>CDN으로 작동하는 서버는 요청을 받아 요청하는 웹 페이지에 데이터를 조립</li>
</ul>
</li>
<li>서비스: AWS CloudFront</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 자바스크립트 호이스팅]]></title>
            <link>https://velog.io/@kong-e/TIL-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%98%B8%EC%9D%B4%EC%8A%A4%ED%8C%85</link>
            <guid>https://velog.io/@kong-e/TIL-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%98%B8%EC%9D%B4%EC%8A%A4%ED%8C%85</guid>
            <pubDate>Sat, 21 Oct 2023 06:24:36 GMT</pubDate>
            <description><![CDATA[<p>자바스크립트(타입스크립트) 코드를 짜다가 함수가 선언되기도 전에 호출해도 코드가 정상작동 되는 것에 대해 의문을 품게 되었는데, 이것이 자바스크립트의 호이스팅이라는 특성 때문인 것을 알게 되었다.</p>
<h2 id="호이스팅">호이스팅</h2>
<ul>
<li>자바스크립트에서는 함수의 선언 위치가 호출하기 전에는 크게 중요하지 않다. 이는 자바스크립트의 호이스팅 특성 때문이다. 호이스팅은 변수와 함수의 선언을 코드의 상단으로 자동으로 끌어올린다.<ul>
<li>변수와 함수 선언은 코드가 실행되기 전에 메모리에 할당되는데, 이를 호이스팅이라고 한다.</li>
</ul>
</li>
</ul>
<h3 id="자바스크립트는-절차적-언어-아니었나">자바스크립트는 절차적 언어 아니었나?</h3>
<ul>
<li>자바스크립트는 절차적 언어의 특성도 가지고 있지만, 여러 프로그래밍 패러다임을 지원한다. 그 중 &#39;호이스팅&#39;이라는 특징 때문에 함수를 선언하기 전에도 호출할 수 있다. 이 호이스팅은 코드 실행 전에 변수와 함수 선언을 메모리에 저장하는 과정이다. 함수 선언식에만 적용되며, 함수 표현식에는 적용되지 않는다.</li>
</ul>
<h3 id="함수-선언식과-함수-표현식의-호이스팅">함수 선언식과 함수 표현식의 호이스팅</h3>
<ul>
<li>함수 선언식은 호이스팅의 영향을 받아 선언 전에 호출할 수 있다. 기본 형태는 <code>function functionName() {}</code>이다.</li>
</ul>
<pre><code>console.log(funcDeclaration()); // &quot;Hello from Function Declaration&quot;
function funcDeclaration() {
  return &quot;Hello from Function Declaration&quot;;
}</code></pre><ul>
<li>함수 표현식은 변수에 함수를 할당하는 형태로, 호이스팅의 영향을 받지 않아 선언 전에 호출하면 에러가 발생한다. 기본 형태는 <code>const functionName = function() {};</code>이다. 함수 표현식에는 익명 함수와 기명 함수 둘 다 사용할 수 있다.<pre><code>console.log(funcExpression()); // TypeError: funcExpression is not a function
var funcExpression = function() {
return &quot;Hello from Function Expression&quot;;
};</code></pre></li>
</ul>
<h3 id="일반변수인-let과-const에도-호이스팅이-적용되는가">일반변수인 let과 const에도 호이스팅이 적용되는가?</h3>
<h4 id="var의-호이스팅">var의 호이스팅</h4>
<ul>
<li><code>var</code>로 선언된 변수는 호이스팅될 때 <code>undefined</code>로 초기화된다.<pre><code class="language-javascript">console.log(a); // 출력: undefined
var a = 10;</code></pre>
</li>
</ul>
<h4 id="let과-const의-호이스팅">let과 const의 호이스팅</h4>
<ul>
<li><p><code>let</code>과 <code>const</code>로 선언된 변수도 호이스팅 된다.</p>
</li>
<li><p>그러나 <code>let</code>과 <code>const</code>로 선언된 변수는 호이스팅 된 후 초기화되지 않는다.</p>
</li>
<li><p>선언 전에 변수에 접근하면 &quot;일시적 사각지대(Temporal Dead Zone, TDZ)&quot;에 들어가게 되며, 이 때문에 에러가 발생한다.</p>
<pre><code class="language-javascript">console.log(b); // ReferenceError: Cannot access &#39;b&#39; before initialization
let b = 20;

console.log(c); // ReferenceError: Cannot access &#39;c&#39; before initialization
const c = 30;</code></pre>
</li>
</ul>
<p>위의 내용은 <code>let</code>, <code>const</code>, 그리고 <code>var</code>와 호이스팅의 관계를 나타내는 내용이다. <code>let</code>과 <code>const</code>는 호이스팅되긴 하지만, 초기화되지 않는 특징을 가지며, 이로 인해 선언 전에 접근하려고 하면 에러가 발생한다.</p>
<h3 id="그렇다면-함수-표현식도-호이스팅이-적용되는-것-아닌가">그렇다면 함수 표현식도 호이스팅이 적용되는 것 아닌가?</h3>
<ul>
<li><p>함수 표현식도 호이스팅에 영향을 받는다. </p>
</li>
<li><p>하지만 함수 표현식을 사용하면 변수는 호이스팅되어 <code>undefined</code>로 초기화되지만, 함수 자체는 호이스팅되지 않는다.</p>
</li>
<li><p>예를 들어, </p>
<pre><code class="language-javascript">console.log(funcExpression); // 출력 결과는 undefined다.
console.log(funcExpression()); // 에러가 발생하며, &quot;funcExpression is not a function&quot;이라는 메시지가 나온다.

var funcExpression = function() {
   return &quot;Function Expression에서 반환하는 메시지&quot;;
};

console.log(funcExpression()); // 이제는 &quot;Function Expression에서 반환하는 메시지&quot;라고 출력된다.</code></pre>
</li>
<li><p>위 예시에서 <code>funcExpression</code> 변수는 호이스팅되기 때문에 처음에 <code>undefined</code>로 평가된다. 그리고 아직 함수가 할당되지 않았기 때문에 호출하려고 하면 에러가 발생한다.</p>
</li>
<li><p>따라서 함수 표현식을 사용할 때는 함수 호출 전에 반드시 함수가 할당되어 있어야 한다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[SWF 2023 해커톤(7/31~8/2) 회고 #2 NFT(SBT) 기부증서]]></title>
            <link>https://velog.io/@kong-e/SWF-2023-%ED%95%B4%EC%BB%A4%ED%86%A473182-%ED%9A%8C%EA%B3%A0-2-%EA%B8%B0%EB%B6%80%EC%A6%9D%EC%84%9C-NFT</link>
            <guid>https://velog.io/@kong-e/SWF-2023-%ED%95%B4%EC%BB%A4%ED%86%A473182-%ED%9A%8C%EA%B3%A0-2-%EA%B8%B0%EB%B6%80%EC%A6%9D%EC%84%9C-NFT</guid>
            <pubDate>Sun, 27 Aug 2023 16:25:41 GMT</pubDate>
            <description><![CDATA[<p>SWF 해커톤에서 기부증서로서 NFT를 민팅해주는 기능을 구현하기 위해 썼던 코드들이다. </p>
<p>사실 증서로서는 양도불가능이라는 특성을 가진 SBT가 적합하나, web3 입문자로서 당시에는 SBT가 생소하기도 했고 ERC721 관련 자료가 많았기 때문에 ERC721 표준을 바탕으로 구현했다.</p>
<p>그러나 기부증서와 같은 역할이므로 이 NFT를 다른 사람에게 transfer하면 안된다.
이 문제의 해결책으로 단순히 transfer 코드를 제외하기로 했다....!!
그래서 ERC721 표준을 완전히 따른 것이라고는 말할 수 없을 듯하다. 
나중에 알아보니 SBT는 ERC-4671나 ERC-4973 표준을 통해 구현한다고 한다.</p>
<p>해커톤 당시 이 코드를 완전히 이해하고 작성한 것은 아니다..</p>
<p>글을 작성하면서 코드 파일을 지금부터 하나씩 분석해보려 한다.</p>
<p>** !!! 해커톤 당일 작성했던 코드들에 대한 회고 목적에서 작성된 글이며, 잘못된 부분이 있을 수 있습니다(분명 있습니다..) 공부하는 입장에서, 작성했던 코드를 더 이해해보고 코드를 개선해보고자 하는 방향에서 작성하였습니다. **</p>
<h2 id="컨트랙트-디렉토리-구조">컨트랙트 디렉토리 구조</h2>
<pre><code>\CONTRACTS
│  ERC165.sol
│  ERC721.sol
│  ERC721Connector.sol
│  ERC721Enumerable.sol
│  ERC721Metadata.sol
│  Meoww.sol
│  Migrations.sol
│
└─interfaces
        IERC165.sol
        IERC721.sol
        IERC721Enumerable.sol
        IERC721Metadata.sol</code></pre><h2 id="erc165">ERC165</h2>
<h3 id="ierc165sol">IERC165.sol</h3>
<pre><code class="language-javascript">// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC165 {
    /// @notice Query if a contract implements an interface
    /// @param interfaceID The interface identifier, as specified in ERC-165
    /// @dev Interface identification is specified in ERC-165. This function
    ///  uses less than 30,000 gas.
    /// @return `true` if the contract implements `interfaceID` and
    ///  `interfaceID` is not 0xffffffff, `false` otherwise
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}</code></pre>
<p>ERC165의 인터페이스다. ERC165에는 해당 컨트랙트의 인터페이스 지원 여부를 확인하는 supportsInterface라는 함수가 필요하다.</p>
<h3 id="erc165sol">ERC165.sol</h3>
<pre><code class="language-javascript">// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import &#39;./interfaces/IERC165.sol&#39;;

contract ERC165 is IERC165 {

    mapping(bytes4 =&gt; bool) private _supportedInterfaces;

    constructor() {    
        _registerInterface(bytes4(keccak256(&#39;supportsInterface(bytes4)&#39;)));
    }

    function supportsInterface(bytes4 interfaceID) external view override returns (bool) {
        return _supportedInterfaces[interfaceID];
    }

    function _registerInterface(bytes4 interfaceId) internal {
        require(interfaceId != 0xffffffff, &#39;Invalid interface request&#39;);
        _supportedInterfaces[interfaceId] = true;
    }



}</code></pre>
<h4 id="프로퍼티">프로퍼티</h4>
<ul>
<li>_supportedInterfaces</li>
</ul>
<h4 id="생성자">생성자</h4>
<ul>
<li>인터페이스 등록</li>
</ul>
<h4 id="메소드">메소드</h4>
<ul>
<li>supportsInterface</li>
<li>_registerInterface</li>
</ul>
<h4 id="erc-165가-필요한-이유">ERC 165가 필요한 이유?</h4>
<p>EIP(<a href="https://eips.ethereum.org/EIPS/eip-721)%EC%97%90">https://eips.ethereum.org/EIPS/eip-721)에</a> 들어가보면 아래와 같이 써있다.</p>
<blockquote>
<p>Every ERC-721 compliant contract must implement the ERC721 and ERC165 interfaces.</p>
</blockquote>
<p>그 이유를 ChatGPT한테 물어봤다.</p>
<blockquote>
<p>ERC165 is a standard interface detection mechanism that allows smart contracts to declare which interfaces they support. By inheriting ERC165, the ERC721 contract is essentially stating that it supports the ERC721 standard and implements the functions specified by the IERC721 interface.</p>
</blockquote>
<p>ERC165는 스마트컨트랙트가 어느 인터페이스를 지원하는지 알 수 있게 해준다.
ex. ERC165를 상속받는 컨트랙트에서 supportsInterface(0x80ac58cd)을 호출하면, 해당 컨트랙트가 IERC721 를 지원하고 있음을 알아낼 수 있다. (여기서 0x80ac58cd는 ERC721의 인터페이스 ID)</p>
<p>ERC165는 IERC165의 supportsInterface 함수를 오버라이딩 한다. </p>
<h4 id="keccack-256">Keccack-256</h4>
<p>keccack은 다목적 암호 함수로, 인풋에 대해 Keccack-256 해시를 수행해서 unique ID를 만들어준다. </p>
<p>keccak256(&#39;supportsInterface(bytes4)&#39;)를 통해 supportsInterface(bytes4)를 해싱한 뒤 bytes4 데이터 타입으로 변환한다. 결과값(ERC165의 인터페이스 ID)은 0x01ffc9a7이다.
-&gt; 즉 어떤 컨트랙트에서 supportsInterface(&#39;0x01ffc9a7&#39;)를 호출했을 때 true 값을 반환하면 IERC165를 따르고 있는 것이다.</p>
<p>그런데 보통 컨트랙트에서 해시값을 직접 계산하면 가스비가 많이 나와서
아래처럼 인터페이스ID를 프로퍼티로 미리 선언한다고 한다.</p>
<pre><code class="language-javascript">    bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd;

    constructor () public {
        // ERC165를 통한 ERC721의 확인을 위한 지원 인터페이스 등록
        _registerInterface(_INTERFACE_ID_ERC721);
    }</code></pre>
<h2 id="erc721">ERC721</h2>
<h3 id="ierc721sol">IERC721.sol</h3>
<pre><code class="language-javascript">// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC721  {

    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

    function balanceOf(address _owner) external view returns (uint256);

    function ownerOf(uint256 _tokenId) external view returns (address);

    // function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

    // function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

   //  function transferFrom(address _from, address _to, uint256 _tokenId) external;

    // function approve(address _approved, uint256 _tokenId) external payable;

    // function setApprovalForAll(address _operator, bool _approved) external;

    // function getApproved(uint256 _tokenId) external view returns (address);

    // function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}</code></pre>
<p>강의를 보면서 작성한 코드인데 초급자를 위한 강의였던지라 주석처리한 함수가 많다. 
해당 강의에서는 transfer 기능도 구현했기 때문에 transferFrom은 원래 주석처리 되어있지 않았다.</p>
<p>내가 원하는 컨트랙트는 transfer가 불가능한 컨트랙트이기 때문에
Transfer event와 Approval event도 주석처리 했어야하는게 맞지 않나 싶다.</p>
<p>그런데 아래 ERC721.sol 코드를 보면 민팅 시에 Transfer event를 emit해주고 있기 때문에 Transfer는 주석처리 안하는게 맞다.</p>
<p>이게 ERC721 표준을 바탕으로 한 것이기 때문에 Transfer event를 emit 했지만,
Mint라는 event를 생성해서 emit하는게 더 알맞을 듯하다.</p>
<h3 id="erc721sol">ERC721.sol</h3>
<pre><code class="language-javascript">// SPDX-License-Identifier: MIT
pragma solidity &gt;=0.4.22 &lt;0.9.0;

import &#39;./interfaces/IERC721.sol&#39;;
import &#39;./ERC165.sol&#39;;


  /*

  a. nft to point to an address
  b. keep track of the token ids
  c. keep track of the owner of the token ids
  -&gt; NFT가 누구의 소유인지 알 수 있음
  d. keep track of how many tokens an owner address has
  e. create an event that emits a transfer log 
  - contract address, where it is being minted to, the id

  */

contract ERC721 is ERC165, IERC721 {

  // event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

  // mapping in solidity creates a hash table of key pair values

  // Mapping from token id to the owner
  // 토큰 ID를 소유자에게 매핑하고자 하는 경우
  mapping(uint256 =&gt; address) private _tokenOwner;

  // Mapping from owner to number of owned token
  mapping(address =&gt; uint256) private _OwnedTokensCount;

   constructor() {
      _registerInterface(bytes4(keccak256(&#39;balanceOf(bytes4)&#39;)^
      keccak256(&#39;ownerOf(bytes4)&#39;)));
  }

  /// @notice Count all NFTs assigned to an owner
  /// @dev NFTs assigned to the zero address are considered invalid, and this
  ///  function throws for queries about the zero address.
  /// @param _owner An address for whom to query the balance
  /// @return The number of NFTs owned by `_owner`, possibly zero
  // 토큰의 소유자가 가지고 있는 토큰의 개수를 반환하는 함수
  function balanceOf(address _owner) public override view returns(uint256) {
    require(_owner != address(0), &quot;ERC721: balance query for the zero address&quot;);
    return _OwnedTokensCount[_owner];
  }

  /// @notice Find the owner of an NFT
  /// @dev NFTs assigned to zero address are considered invalid, and queries
  ///  about them do throw.
  /// @param _tokenId The identifier for an NFT
  /// @return The address of the owner of the NFT
  // 토큰의 소유자를 반환하는 함수
  function ownerOf(uint256 _tokenId) external override view returns (address) {
    address owner = _tokenOwner[_tokenId];
    require(owner != address(0), &quot;ERC721: owner query for nonexistent token&quot;);
    return owner;
  }

  function _exists(uint256 tokenId) internal view returns (bool) {
    // setting the address of nft owner to check the mapping
    // of the address from tokenOwner at the tokenId
    address owner = _tokenOwner[tokenId];
    // return truthiness the address is not the zero
    return owner != address(0); 
  }

  function _mint(address to, uint256 tokenId) internal virtual {
    require(to != address(0), &quot;ERC721: minting to the zero address&quot;); // 주소가 0이 아니라는걸 증명
    require(!_exists(tokenId), &quot;ERC721: token already minted&quot;); // 토큰이 존재하지 않는다는걸 증명
    _tokenOwner[tokenId] = to; // 해당 토큰아이디를 to에게 매핑
    _OwnedTokensCount[to] += 1; // to의 토큰개수 세기

    emit Transfer(address(0), to, tokenId);
  }
}

// enumerable -&gt; 한 집합 내 모든 항목이 완전히 순서가 매겨진 것</code></pre>
<h4 id="프로퍼티-1">프로퍼티</h4>
<ul>
<li>_tokenOwner</li>
<li>_OwnedTokensCount</li>
</ul>
<h4 id="생성자-1">생성자</h4>
<ul>
<li>인터페이스 등록 : ERC721의 interface ID는 원래 아래와 같이 정해지는데,<pre><code class="language-javascript">    /*
     *     bytes4(keccak256(&#39;balanceOf(address)&#39;)) == 0x70a08231
     *     bytes4(keccak256(&#39;ownerOf(uint256)&#39;)) == 0x6352211e
     *     bytes4(keccak256(&#39;approve(address,uint256)&#39;)) == 0x095ea7b3
     *     bytes4(keccak256(&#39;getApproved(uint256)&#39;)) == 0x081812fc
     *     bytes4(keccak256(&#39;setApprovalForAll(address,bool)&#39;)) == 0xa22cb465
     *     bytes4(keccak256(&#39;isApprovedForAll(address,address)&#39;)) == 0xe985e9c
     *     bytes4(keccak256(&#39;transferFrom(address,address,uint256)&#39;)) == 0x23b872dd
     *     bytes4(keccak256(&#39;safeTransferFrom(address,address,uint256)&#39;)) == 0x42842e0e
     *     bytes4(keccak256(&#39;safeTransferFrom(address,address,uint256,bytes)&#39;)) == 0xb88d4fde
     *
     *     =&gt; 0x70a08231 ^ 0x6352211e ^ 0x095ea7b3 ^ 0x081812fc ^
     *        0xa22cb465 ^ 0xe985e9c ^ 0x23b872dd ^ 0x42842e0e ^ 0xb88d4fde == 0x80ac58cd
     */
    bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd;</code></pre>
내가 작성한 ERC721.sol의 생성자함수를 보면 인터페이스ID를 keccak256(&#39;balanceOf(bytes4)&#39;)과
keccak256(&#39;ownerOf(bytes4)&#39;)만 XOR 하여 생성하는 것으로 코드를 짰다...
이렇게 짜면 내 NFT가 ERC721 인터페이스를 따르고 있다는 것을 알려주지 못할 것 같다.
아무래도 interfaceID에 대한 이해가 부족한 상태로 코드를 짜기도 했고, transfer가 없도록 변형해서 짜다보니 애매한 코드가 되었다...</li>
</ul>
<h4 id="메소드-1">메소드</h4>
<ul>
<li>balanceOf</li>
<li>ownerOf</li>
<li>_exists</li>
<li>_mint : _mint 함수에서 emit Transfer 부분에 적힌 address(0)이 무슨 의미인지도 궁금해졌다. address(0)은 일반적으로 &quot;무효한 주소&quot; 또는 &quot;영국 열왕의 주소&quot;로 알려진 주소다. 이 주소는 실제로 존재하지 않는 주소를 나타내며, 주로 스마트 계약 내에서 상태의 초기화나 변화를 나타내기 위해 사용된다. emit Transfer(address(0), to, tokenId)의 경우, 이벤트는 새로운 토큰이 생성되었음을 나타내는 것으로 이해할 수 있다. 이는 초기 발행 상황에서 흔히 사용되는 패턴 중 하나라고 한다.</li>
</ul>
<h2 id="erc721enumerable">ERC721Enumerable</h2>
<h3 id="ierc721enumerablesol">IERC721Enumerable.sol</h3>
<pre><code class="language-javascript">// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC721Enumerable {

    function totalSupply() external view returns (uint256);

    function tokenByIndex(uint256 _index) external view returns (uint256);

    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}</code></pre>
<p>토큰의 총 공급량 반환, 인덱스를 통한 토큰 반환, 오너의 토큰목록에서 인덱스에 해당하는 토큰 반환으로 이루어진 인터페이스다.</p>
<h3 id="erc721enumerablesol">ERC721Enumerable.sol</h3>
<pre><code class="language-javascript">// SPDX-License-Identifier: MIT
pragma solidity &gt;=0.4.22 &lt;0.9.0;

import &#39;./ERC721.sol&#39;;
import &#39;./interfaces/IERC721Enumerable.sol&#39;;

contract ERC721Enumerable is ERC721, IERC721Enumerable {

  uint256[] private _allTokens;

  // mapping from tokenId to position in the allTokens array
  mapping(uint256 =&gt; uint256) private _allTokensIndex;

  // mapping of owner to list of all owner token ids
  mapping(address =&gt; uint256[]) private _ownedTokens;

  // mapping from token ID index of the owner token ids
  mapping(uint256 =&gt; uint256) private _ownedTokensIndex;

    constructor() {
      _registerInterface(bytes4(keccak256(&#39;totalSupply(bytes4)&#39;)^
      keccak256(&#39;tokenByIndex(bytes4)&#39;)));
  }

  function _mint(address to, uint256 tokenId) internal override(ERC721) {
    super._mint(to, tokenId);
    _addTokenAllTokenEnumeration(tokenId);
    _addTokenToOwnerEnumeration(to, tokenId);
  } // 민트된 토큰은 이뉴머레이션에 추가

  function _addTokenAllTokenEnumeration(uint256 tokenId) private {
    _allTokensIndex[tokenId] = _allTokens.length;
    _allTokens.push(tokenId);
  } // 토큰을 전체목록에 추가

  function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
    _ownedTokensIndex[tokenId] = _ownedTokens[to].length;
    _ownedTokens[to].push(tokenId);
  } // 토큰을 오너목록에 추가

  function tokenByIndex(uint256 index) public view override returns (uint256) {
    require(index &lt; totalSupply(), &quot;ERC721Enumerable: global index out of bounds&quot;);
    return _allTokens[index];
  } // 인덱스에 해당하는 토큰 반환(토큰 검색)

  function tokenOfOwnerByIndex(address owner, uint256 index) public view override returns (uint256) {
    require(index &lt; balanceOf(owner), &quot;ERC721Enumerable: owner index out of bounds&quot;);
    return _ownedTokens[owner][index];
  } // 오너의 토큰목록에서 인덱스에 해당하는 토큰 반환(토큰 검색)

  function totalSupply() public view override returns (uint256) {
    return _allTokens.length;
  } // 토탈서플라이 길이 반환
}</code></pre>
<h4 id="프로퍼티-2">프로퍼티</h4>
<ul>
<li>_allTokensIndex</li>
<li>_ownedTokens</li>
<li>_ownedTokensIndex</li>
</ul>
<h4 id="생성자-2">생성자</h4>
<ul>
<li>인터페이스 등록</li>
</ul>
<h4 id="메소드-2">메소드</h4>
<ul>
<li>_mint</li>
<li>_addTokenAllTokenEnumeration</li>
<li>_addTokenToOwnerEnumeration</li>
<li>tokenByIndex</li>
<li>tokenOfOwnerByIndex</li>
<li>totalSupply</li>
</ul>
<p>ERC721Enumerable은 ERC-721 토큰 컨트랙트에서 NFT를 열거하고 관리하기 위한 기능을 제공한다.</p>
<h2 id="erc721metadata">ERC721Metadata</h2>
<h3 id="ierc721metadatasol">IERC721Metadata.sol</h3>
<pre><code class="language-javascript">// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IERC721Metadata {

    function name() external view returns (string memory _name);

    function symbol() external view returns (string memory _symbol);

    // function tokenURI(uint256 _tokenId) external view returns (string memory);
}</code></pre>
<h3 id="erc721metadatasol">ERC721Metadata.sol</h3>
<pre><code class="language-javascript">// SPDX-License-Identifier: MIT
pragma solidity &gt;=0.4.22 &lt;0.9.0;

import &#39;./interfaces/IERC721Metadata.sol&#39;;
import &#39;./ERC165.sol&#39;;

contract ERC721Metadata is IERC721Metadata, ERC165 {
  string private _name;
  string private _symbol;

  constructor(string memory named, string memory symbolified) {

    _registerInterface(bytes4(keccak256(&#39;name(bytes4)&#39;)^
    keccak256(&#39;symbol(bytes4)&#39;)));

    _name = named;
    _symbol = symbolified;
}

  function name() external view override returns (string memory) {
    return _name;
  }

  function symbol() external view override returns (string memory) {
    return _symbol;
  }
}</code></pre>
<h4 id="프로퍼티-3">프로퍼티</h4>
<ul>
<li>_name</li>
<li>_symbol</li>
</ul>
<h4 id="생성자-3">생성자</h4>
<ul>
<li>인터페이스 등록 : ERC721 Metadata의 인터페이스 ID는 보통 아래처럼 정해진다.<pre><code class="language-javascript">/*
 *  bytes4(keccak256(&#39;name()&#39;)) == 0x06fdde03
 *  bytes4(keccak256(&#39;symbol()&#39;)) == 0x95d89b41
 *  bytes4(keccak256(&#39;tokenURI(uint256)&#39;)) == 0xc87b56dd
 *
 *  =&gt; 0x06fdde03 ^ 0x95d89b41 ^ 0xc87b56dd == 0x5b5e139f
 */</code></pre>
 내가 작성한 코드는 표준에 따르지 않는 거라고 보면 되겠다...</li>
<li>name, symbol 설정</li>
</ul>
<h4 id="메소드-3">메소드</h4>
<ul>
<li>name</li>
<li>symbol</li>
</ul>
<h2 id="erc721connector">ERC721Connector</h2>
<h3 id="erc721connectorsol">ERC721Connector.sol</h3>
<pre><code class="language-javascript">// SPDX-License-Identifier: MIT
pragma solidity &gt;=0.4.22 &lt;0.9.0;

import &quot;./ERC721Metadata.sol&quot;;
import &quot;./ERC721Enumerable.sol&quot;;

contract ERC721Connector is ERC721Metadata, ERC721Enumerable {

  // we want to carry the metadata info over
  constructor (string memory named, string memory symbolified) ERC721Metadata(named, symbolified) {
  }
}</code></pre>
<p>ERC721Connector는 ERC721Metadata와 ERC721Enumaerable을 상속받아 ERC165, ERC721, ERCMetadata, ERC721Enumerable의 프로퍼티와 메소드를 모두 갖게 된다.</p>
<h2 id="nftmeoww">NFT(Meoww)</h2>
<h3 id="meowwsol">Meoww.sol</h3>
<pre><code class="language-javscript">// SPDX-License-Identifier: MIT
pragma solidity &gt;=0.4.22 &lt;0.9.0;

import &#39;./ERC721Connector.sol&#39;;

contract Meoww is ERC721Connector {

  // 배열 -&gt; 민팅작업으로 생긴 NFT가 저장됨
  string[] public meowwz;

  mapping(string =&gt; bool) _meowwzExists;

  // ERC721 -&gt; tokenId 민트
  // Meoww -&gt; 사진 민트
  function mint(address to, string memory _meoww) public {

    require(!_meowwzExists[_meoww], &#39;Error - meoww already exists&#39;);
    // this is deprecated - uint _id = meowwz.push(_meoww);
    meowwz.push(_meoww);
    uint _id = meowwz.length - 1;

    // .push no longer returns the length but a ref to the added element
    _mint(to, _id);

    _meowwzExists[_meoww] = true;
  }

  // initialize this contract to inherit
  // name and symbol from ERC721Metadata so that
  // the name is Meoww and the symbol is MEW

  constructor() ERC721Connector(&quot;Meoww&quot;, &quot;MEW&quot;) {
  }

}</code></pre>
<h4 id="프로퍼티-4">프로퍼티</h4>
<ul>
<li>meowwz</li>
<li>_meowwzExists</li>
</ul>
<h4 id="생성자-4">생성자</h4>
<ul>
<li>name, symbol 설정</li>
</ul>
<h4 id="메소드-4">메소드</h4>
<ul>
<li>mint : 당시 <code>msg.sender</code> 개념이 잘 안잡혀 있어서, <code>mint</code> 함수에 <code>address to</code> 매개변수를 추가하고 <code>_mint</code>에 msg.sender 대신 to를 집어넣었었다. <code>mint</code>에 <code>address to</code> 매개변수를 빼고 <code>_mint</code>에 <code>to</code> 대신 <code>msg.sender</code>를 넣는 것이 더 알맞을 듯 하다.</li>
</ul>
<h2 id="migration">Migration</h2>
<h3 id="migrationssol">Migrations.sol</h3>
<pre><code class="language-javascript">// SPDX-License-Identifier: MIT
pragma solidity &gt;=0.4.22 &lt;0.9.0;

contract Migrations {
    address public owner;
    uint256 public last_completed_migration;

    constructor() {
        owner = msg.sender; // 생성자의 주소를 owner에 저장
    }

    modifier restricted() {
        require(msg.sender == owner,
        &quot;This function is restricted to the contract&#39;s owner&quot;
        );
        _;
    }

    function setCompleted(uint256 completed) public restricted {
        last_completed_migration = completed;
    }

    function upgrade(address new_address) public restricted {
        Migrations upgraded = Migrations(new_address); // 상속받은 Migrations의 인스턴스를 생성
        upgraded.setCompleted(last_completed_migration);
    }
}</code></pre>
<p><code>Migrations.sol</code>은 주로 Truffle 프레임워크와 함께 사용되는 스마트 컨트랙트으로, 스마트 컨트랙트 배포 및 업그레이드 관리를 위한 용도로 만들어진 것이다. 이 스마트 컨트랙트은 일반적으로 다음과 같은 목적을 가지고 있다.</p>
<ol>
<li><p><strong>스마트 컨트랙트 업그레이드</strong>: 스마트 컨트랙트를 업그레이드하여 관리하기 위한 목적으로 사용된다. <code>upgrade</code> 함수를 통해 새로운 컨트랙트 주소로 업그레이드할 수 있다. 스마트 컨트랙트의 버전 업그레이드나 수정을 허용하며, 이전 데이터와 상태를 유지하면서 새로운 스마트 컨트랙트를 사용할 수 있게 한다.</p>
</li>
<li><p><strong>스마트 컨트랙트 배포 및 소유권 관리</strong>: 컨트랙트를 배포할 때 <code>owner</code> 변수에 계약 생성자의 주소를 저장하여, 스마트 컨트랙트의 소유자를 추적하고 특정 함수를 소유자에게만 제한적으로 사용할 수 있게 한다. <code>restricted</code> 제한자를 사용하여 특정 함수가 스마트 컨트랙트 소유자에 의해서만 호출될 수 있도록 한다.</p>
</li>
<li><p><strong>스마트 컨트랙트 버전 관리</strong>: <code>last_completed_migration</code> 변수를 사용하여 현재 스마트 컨트랙트의 버전을 추적한다. 이것은 업그레이드된 스마트 컨트랙트의 이전 버전을 추적하고 관리하는 데 사용된다.</p>
</li>
</ol>
<p>Remix IDE에서 컨트랙트를 deploy 할 때마다 새로운 컨트랙트 Address가 생긴다.
Migration.sol은 truffle에서 위와 같은 것을 지원하는 컨트랙트인 것 같다.</p>
<h2 id="hooks">hooks</h2>
<h3 id="usemeowwcontractjs">useMeowwContract.js</h3>
<pre><code class="language-javascript">// useMeowwzContract.js
import { useState, useEffect } from &quot;react&quot;;
import Web3 from &quot;web3&quot;;
import Meoww from &quot;../truffle_abis/Meoww.json&quot;; // Replace this with the actual contract JSON file.

const useMeowwzContract = (imageList, idx) =&gt; {
  const [account, setAccount] = useState(&quot;&quot;);
  const [contract, setContract] = useState(null);
  const [totalSupply, setTotalSupply] = useState(0);
  const [meowwz, setMeowwz] = useState([]);
  const [isMinting, setIsMinting] = useState(false);

  useEffect(() =&gt; {
    const loadWeb3 = async () =&gt; {
      if (window.ethereum) {
        window.web3 = new Web3(window.ethereum);
        await window.ethereum.enable();
        console.log(&quot;Ethereum wallet is connected&quot;);
      } else if (window.web3) {
        window.web3 = new Web3(window.web3.currentProvider);
        console.log(&quot;Legacy web3 browser detected&quot;);
      } else {
        window.alert(
          &quot;No ethereum browser detected! You can check out MetaMask!&quot;
        );
      }
    };

    const loadBlockchainData = async () =&gt; {
      const web3 = window.web3;

      if (web3) {
        try {
          const accounts = await web3.eth.getAccounts();
          setAccount(accounts[0]);
          console.log(accounts[0]);

          const networkId = await web3.eth.net.getId();
          const networkData = Meoww.networks[networkId];
          console.log(networkData);

          if (networkData) {
            const abi = Meoww.abi;
            const address = networkData.address;
            const contract = new web3.eth.Contract(abi, address);
            setContract(contract);
            console.log(contract);

            const totalSupply = await contract.methods.totalSupply().call();
            setTotalSupply(totalSupply);

            const meowwzArray = [];
            for (let i = 0; i &lt; totalSupply; i++) {
              const Meoww = await contract.methods.meowwz(i).call();
              console.log(Meoww);
              meowwzArray.push(Meoww);
            }
            setMeowwz(meowwzArray);
          }
        } catch (error) {
          console.error(&quot;Error loading blockchain data:&quot;, error);
        }
      } else {
        console.log(
          &quot;Web3 object is undefined. Make sure the provider is properly initialized.&quot;
        );
      }
    };

    loadWeb3();
    loadBlockchainData();
  }, []);

  const mint = async () =&gt; {
    try {
      await contract.methods
        .mint(account, imageList[idx])
        .send({ from: account });

      setMeowwz((prevState) =&gt; [...prevState, imageList[idx]]);
      setIsMinting(true);
    } catch (error) {
      console.error(&quot;Error minting NFT:&quot;, error);
    }
  };

  return {
    account,
    contract,
    totalSupply,
    meowwz,
    mint,
    isMinting,
  };
};

export default useMeowwzContract;</code></pre>
<p>사실 hooks로 만들지 않아도 됐는데 이 로직을 넣을 페이지에 코드가 너무 많아서 hooks로 따로 뺐다.</p>
<h4 id="loadweb3">loadWeb3</h4>
<p>window.ethereum을 통해 web3 인스턴스를 생성한다. 
당시에 왜인지 web3 undefined 에러가 떠서 여러 if 문으로 나누었었다.
그런데 이 방식 말고 <code>const web3Instance = new Web3(window.ethereum)</code>로 해도 정상적으로 인스턴스가 생성된다.</p>
<h4 id="loadblockchaindata">loadBlockchainData</h4>
<ul>
<li>const networkId = await web3.eth.net.getId();: 연결된 Ethereum 네트워크의 ID를 가져온다.</li>
<li>const networkData = Meoww.networks[networkId];: 스마트 계약 Meoww가 배포된 네트워크에 대한 정보를 가져온다.</li>
<li>const abi = Meoww.abi;, const address = networkData.address;: 스마트 계약의 ABI (Application Binary Interface) 및 주소를 가져온다.</li>
<li>const contract = new web3.eth.Contract(abi, address);: 스마트 계약 인스턴스를 생성한다.</li>
<li>setTotalSupply(totalSupply);: 스마트 계약의 totalSupply 함수를 호출하여 총 공급량을 가져온다.</li>
<li>for (let i = 0; i &lt; totalSupply; i++) { ... }: 스마트 계약의 meowwz 함수를 호출하여 NFT (Non-Fungible Token)의 정보를 가져와 배열에 추가한다.</li>
</ul>
<h4 id="mint">mint</h4>
<p>imageList의 특정 인덱스에 있는 이미지를 account에 민팅한다.</p>
<h2 id="test">Test</h2>
<h3 id="meowwtestjs">Meoww.test.js</h3>
<pre><code class="language-javascript">const { assert } = require(&quot;chai&quot;);

const Meoww = artifacts.require(&quot;./Meoww&quot;);

// check for chai
require(&quot;chai&quot;).use(require(&quot;chai-as-promised&quot;)).should();

contract(&quot;Meoww&quot;, (accounts) =&gt; {
  let contract;
  // before tells our tests to run this first before anything else
  before(async () =&gt; {
    contract = await Meoww.deployed();
  });

  // testing container - describe

  describe(&quot;deployment&quot;, () =&gt; {
    // test samples with writing it
    it(&quot;deploys successfuly&quot;, async () =&gt; {
      const address = contract.address;
      assert.notEqual(address, &quot;&quot;);
      assert.notEqual(address, null);
      assert.notEqual(address, undefined);
      assert.notEqual(address, 0x0);
    });
    it(&quot;has a name&quot;, async () =&gt; {
      const name = await contract.name();
      assert.equal(name, &quot;Meoww&quot;);
    });
    it(&quot;has a symbol&quot;, async () =&gt; {
      const symbol = await contract.symbol();
      assert.equal(symbol, &quot;MEW&quot;);
    });
  });

  describe(&quot;minting&quot;, () =&gt; {
    it(&quot;creates a new token&quot;, async () =&gt; {
      const result = await contract.mint(
        &quot;0x86948078a2bC9A367DE4c1E24E9E8573f09cF20b&quot;,
        &quot;https...1&quot;
      );
      const totalSupply = await contract.totalSupply();

      //Success
      assert.equal(totalSupply, 1);
      const event = result.logs[0].args;
      assert.equal(
        event._from,
        &quot;0x0000000000000000000000000000000000000000&quot;,
        &quot;from the contract&quot;
      );
      assert.equal(event._to, accounts[0], &quot;to is msg.sender&quot;);

      //Failure
      await contract.mint(
        &quot;0x86948078a2bC9A367DE4c1E24E9E8573f09cF20b&quot;,
        &quot;https...1&quot;
      ).should.be.rejected;
    });
  });

  describe(&quot;indexing&quot;, () =&gt; {
    it(&quot;lists Meowwz&quot;, async () =&gt; {
      // Mint three new tokens
      await contract.mint(
        &quot;0x86948078a2bC9A367DE4c1E24E9E8573f09cF20b&quot;,
        &quot;https...2&quot;
      );
      await contract.mint(
        &quot;0x86948078a2bC9A367DE4c1E24E9E8573f09cF20b&quot;,
        &quot;https...3&quot;
      );
      await contract.mint(
        &quot;0x86948078a2bC9A367DE4c1E24E9E8573f09cF20b&quot;,
        &quot;https...4&quot;
      );
      const totalSupply = await contract.totalSupply();
      // Loop through list and grab Meowwz from list
      let result = [];
      let Meowwz;
      for (let i = 1; i &lt;= totalSupply; i++) {
        Meowwz = await contract.meowwz(i - 1);
        result.push(Meowwz);
      }
    });
  });
});</code></pre>
<p>Mocha와 Chai를 이용한 테스트코드.
should는 의무조건, assert는 평가</p>
<hr>
<h2 id="참고자료">참고자료</h2>
<ul>
<li>udemy &lt;NFT 웹 개발 완벽 마스터 – 기초부터 전문가까지&gt;</li>
<li>EIP(Ethereum Improvement Proposals)</li>
<li>클레이튼 Docs</li>
<li>OpenZeppelin</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[SWF 2023 해커톤(7/31~8/2) 회고 #1]]></title>
            <link>https://velog.io/@kong-e/SWF-2023-%ED%95%B4%EC%BB%A4%ED%86%A473182-%ED%9A%8C%EA%B3%A0-1</link>
            <guid>https://velog.io/@kong-e/SWF-2023-%ED%95%B4%EC%BB%A4%ED%86%A473182-%ED%9A%8C%EA%B3%A0-1</guid>
            <pubDate>Tue, 08 Aug 2023 01:15:02 GMT</pubDate>
            <description><![CDATA[<p>올 초부터 본격적으로 웹개발 공부를 시작한 후 처음으로 참여한 해커톤이었다. </p>
<p>사실 Web2에 대해서도 아직 모르는 부분이 많은데 첫 해커톤이 Web3 해커톤이었다.
Web3는 학교 내 블록체인 학회 활동(무려 1기 멤버)을 하게 되면서 처음으로 공부하게 되었다. 
나를 포함한 대부분의 학회원들이 블록체인 입문자이지만, 
학회원들끼리 일단 도전해보자하고 입문자 6명들끼리 모여 기획자나 디자이너도 없이 다소 무모하게 참가했다.</p>
<p>하지만 결과적으로 본선에도 진출하게 되었고, 과정도 매우 재밌었다.</p>
<h2 id="준비과정">준비과정</h2>
<p>우리팀은 해커톤 일주일전부터 본격적인 준비를 시작했다.</p>
<p>팀원들이 모두 개발자 Role이지만 학회장님과 여러 현직자 분들의 피드백을 받으며 함께 머리를 싸매고 기획을 했다. 팀원들 모두 컴퓨터공학 주전공이거나 소프트웨어 복수전공생들이었는데 다들 기획, 디자인 능력이 뒤쳐진다고 생각되지 않았다.</p>
<p>(계속해서 느끼지만 개발자라고 해서 개발만 하면 안되고 함께 서비스를 기획해나가는 능력도 중요해지는 요즘인 것 같다)</p>
<h3 id="기획">기획</h3>
<p>SWF 2023 해커톤의 대주제는 <strong>&#39;사람과 기술&#39;</strong>이었고 소주제는 3가지였다. 소주제 중 하나는 선공개 되었는데** &#39;약자와의 동행&#39;**이었다. </p>
<p>우리는 선공개된 소주제를 바탕으로 미리 대략적인 서비스 기획 틀을 잡기 시작했다. (3번이상 뒤엎은건 안비밀...) 우리가 기획을 하며 중요시한 건 <strong>Why Blockchain?</strong>이었다. &quot;웹2로 충분히 구현 가능하다면, 굳이 웹3로 구현해야할 필요가 있나? 이용자들도 지갑 생성 등 번거로운 과정이 없는 웹2를 더 선호하지 않을까?&quot;와 같은 생각이었다. 즉 웹3로 구현을 한 데에는 납득할만한 이유가 필요했다.</p>
<p>우리팀의 주제를 대략설명하자면 <strong>암호화폐 기반 기부 서비스 &quot;기부런&quot;</strong>이었다. (서울시에서 개최하는 대회임을 감안해 &quot;서울런&quot;에서 따온 것이었지만, 막상 직접 가보니 심사위원들 중 공직에 계신 분은 없었던 걸로 기억한다...) </p>
<p><strong>&quot;암호화폐를 갖고 있는 사람은 많은데, 그것으로 기부가 이루어지는 플랫폼은 없다. 또한 암호화폐 기반으로 기부가 이루어지면 투명성을 보장할 수 있다.&quot;</strong>라는 생각에서 나온 주제였다.</p>
<p>기능은 대략 다음과 같다.</p>
<ol>
<li>이용자들은 암호화폐(코인)로 진행중인 기부 사업에 기부를 한다.</li>
<li>기부자는 기부 즉시 기부증서와 같은 역할을 하는 NFT를 민팅받는다.</li>
<li>기부자는 기부 즉시 토큰을 지급받는다.</li>
<li>이용자들은 지급받은 토큰을 이용해 기부 대기 중인 사업들에 투표를 할 수 있다.</li>
<li>투표로 최종 기부 사업들이 선정된다.</li>
</ol>
<p>사실 처음부터 암호화폐 기반 기부를 생각한 것은 아니다. 처음에는 보통의 기부 플랫폼들처럼 현금(예금) 기부를 생각했으나, 이러한 경우 투명성을 블록체인을 이용해 어떻게 증명할 것이냐가 문제였다. 
여기서 <strong>오라클</strong>이라는 개념이 언급되기도 했다. (오라클은 블록체인 외부에 있는 데이터를 블록체인 안으로 가져오는 미들웨어) 
그러나 우리는 Web3 병아리라 이것을 어떻게 구현하느냐와 오라클의 신뢰 문제 등등 복잡해지면서 현금 기부 아이디어는 무산되었다..</p>
<p>다시 암호화폐 기반 기부 아이디어로 돌아왔을 때, <strong>이 암호화폐를 약자들에게 어떻게 전달할 것이냐</strong>하는 문제가 대두되었다. 대략 나왔던 의견은 다음과 같다.</p>
<ol>
<li>기부를 받은 사람에게 SBT를 지급하여 이를 어음 형식으로 활용해 기부금을 받도록 한다.</li>
<li>서울시 공무원과 같은 중개자를 두어 코인을 현금으로 매도하여 지급한다.</li>
</ol>
<p>그러나 이 의견들에도 각각의 문제점이 있었다.</p>
<ol>
<li>사회적 약자들이 메타마스크와 같은 지갑을 활용하여 토큰을 받을 수 있을 만큼 디지털 환경에 친화적일 가능성이 높지 않다.</li>
<li>결국엔 중개자가 개입하여 또다시 신뢰성 문제가 발생한다.</li>
</ol>
<p>이외에도 &quot;암호화폐를 갖고 있지 않은 사람들의 경우, 굳이 복잡한 블록체인 지갑 형성 과정을 거쳐 암호화폐로 기부를 할까?&quot; 하는 문제도 대두되었다.</p>
<p>이러한 쟁점들을 그대로 남겨둔 채로 우리는 해커톤에 가게 되었다...</p>
<h3 id="디자인">디자인</h3>
<p>대부분의 팀원들이 피그마 사용이 처음이었기 때문에 내가 먼저 피그마 협업 공간을 만들어 대략적인 틀을 제시했다. 그 후 팀원들 모두 참여하여 메인 색상을 선정하고 페이지 화면을 구상했다.</p>
<h3 id="개발">개발</h3>
<p>나를 제외한 모든 팀원들이 git을 통한 협업, React를 통한 프론트엔드 개발 모두 처음이었다.
그래서 해커톤 당일날부터 프로덕트 구축을 시작하는 것은 무리였기에 일주일전부터 빡세게 준비를 시작했다.</p>
<p>나는 git과 react를 이용해 프로젝트를 해본 경험이 있고 해오고 있었기 때문에, 팀원들이 막힐 때마다 알려주는 역할을 했다. 내가 먼저 리액트 프로젝트와 디렉토리를 구성하여 깃허브에 올리고, 팀원들이 이 위에 연습을 하는 식으로 준비했다.</p>
<p>컨트랙트 개발은 몇달전 학회 세션에서 Remix IDE를 이용해 Solidity 실습을 몇번 해본 것과 개인적으로 관련 강의를 보며 몇번 따라해본 것 제외하고는 해커톤을 위한 준비를 거의 하지 못했다..</p>
<h2 id="해커톤73182">해커톤(7/31~8/2)</h2>
<h3 id="행사장-환경">행사장 환경<img src="https://velog.velcdn.com/images/kong-e/post/e14812fe-1d16-4f1a-9f31-9c4c90372fcc/image.jpg" alt=""><img src="https://velog.velcdn.com/images/kong-e/post/f9c013e2-3389-4835-9889-46d3b42e0e66/image.jpg" alt=""></h3>
<p>7월31일부터 8월2일까지 동대문디자인플라자(DDP) A1에서 진행되었다. 
식사, 간식, 라면 등이 무료로 제공되었고 밥은 꽤 푸짐하게 나오는 편이었다. 
다만 머리를 감는 등의 샤워는 할 수 없고, 잠도 휴게실의 빈백에 쭈그려 자야하는 점은 힘들었다..
이외에도 여러 기업들의 부스에 가면 미니선풍기, 반팔티, 스티커 등을 받을 수 있었다! 여기서 LBank, 크로노스 같은 웹3 기업들을 처음 알게되었다.</p>
<h3 id="기획-1">기획</h3>
<p>기부를 받는 사람들은 결국엔 현금이 필요할 것이기 때문에, 코인-&gt;현금 과정에서의 매도를 누가 어떻게 하는지의 문제에 대해 계속해서 의논했다. 그렇게 해서 나온 의견은 기부 수혜자가 직접 암호화폐를 현금으로 매도하는 것이었다.</p>
<p>대신, 매도 전에 수혜자들이 블록체인 지갑의 이용법에 대한 교육을 이수해야한다는 점을 추가했다. 이를 &quot;향후 중앙은행이 발행할 CBDC에 대한 적응에 도움이 될 수 있다.&quot;, &quot;약자들이 디지털 환경에 적응할 수 있도록 도울 수 있다.&quot;와 같은 기대효과와 덧붙이기로 했다.</p>
<h3 id="개발-1">개발</h3>
<p>일주일 간의 준비를 통해 프론트엔드는 구상을 어느정도 해놓은 상태였기 때문에, 해커톤 당일에는 컨트랙트 개발에 집중하고 싶었다.
기부런에서 기부자에게 즉시 NFT를 민팅해주는 기능을 구상했었기 때문에 이 부분을 내가 맡아 개발하기로 했다. </p>
<p>해커톤 당일 개발 측면에서 고민하고 구현한 내용들은 2편에서 자세히 다뤄보려고 한다. (대단한 것은 전혀 없지만...) </p>
<p>결과적으로 이틀만에 처음해보는 NFT 민팅 기능을 구현하고 프론트엔드와 연결시키는데 성공했다.</p>
<p>한편 다른 팀원은 토큰발행 기능을 infura를 이용해 구현하였다.</p>
<h3 id="예선-발표-준비">예선 발표 준비</h3>
<p>8월 1일에서 2일로 넘어가는 날엔 예선 발표 준비로 모두 밤을 샜다.</p>
<p>다른 팀원들은 최종 기획을 바탕으로 기획서를 작성했다. Web3 업계의 특성상 영어가 많이 쓰이기 때문에 기획서는 한글, 영어 버전 모두 작성했다. </p>
<p>기획서의 목차는 크게 1. 프로젝트 소개, 2. 선행사례, 3. 차별점, 4. 기대효과로 구성했다.</p>
<p>사실 나도 기획서 작성에 참여하고 싶었으나 NFT 민팅 기능을 구축하기에도 바빴기 때문에 크게 관여하지 못한 점이 조금 아쉬웠다.</p>
<p>나는 최종적으로 구현한 NFT 발행 기능을 화면녹화하여 발표자에게 제공했다.</p>
<p>오전 7시 30분에 깃허브 주소와 기획서, 발표자료를 USB에 담아 최종 제출했다.</p>
<p>발표자를 맡은 학우와 다른 학우 1명은 발표 준비를 위해 다른 곳으로 갔다.
나와 나머지 팀원들은 함께 아침을 먹고, 발표를 준비하러 간 두 팀원들에게는 미안하지만 DDP 야외에서 쪽잠을 자게 되었다...</p>
<p><img src="https://velog.velcdn.com/images/kong-e/post/addc9b59-83c2-40dc-bf33-f3915aee8386/image.jpg" alt="쪽잠자기 1초전 찍은 사진"></p>
<p>예선 발표는 오전 9시부터 진행되었던 걸로 기억하는데, 우리팀은 예상치못하게 가장 먼저 예선장에서 발표하게 되었다. 그렇게 발표자를 갑작스럽게 예선장으로 보내게 되었다..</p>
<p>5분 뒤에 발표자가 돌아왔다. 심사위원들이 생각보다 우리 주제에 대해 질문을 많이 하였다는 것이다. 좋은 신호라 생각했다.</p>
<h3 id="본선">본선</h3>
<p>예선을 마친뒤, 본선에 진출한 팀은 사전에 공지되지 않았다. 본선 발표 시간에 사회자가 팀 이름을 호명하면 그 팀이 나와 즉시 발표하는 식으로 긴장감있게 진행되었다. 소주제 별로 6팀 씩 본선에 진출하게 된다.</p>
<p>소주제 &#39;약자와의 동행&#39;의 발표가 시작되었다. 다른 팀들의 발표에 대해 심사위원들은 &quot;이 프로젝트가 웹3일 필요가 있나요?&quot;와 같은 <strong>Why Blockchain, Why Web3</strong>에 대한 질문도 했다. 역시 중요한 질문이라 생각했다.</p>
<p>우리팀은 중간 차례가 지나서도 호명되지 않았다. 그러다 5번째 본선 진출 팀의 발표 차례가 다가왔고, 우리팀 &quot;2PO&quot;가 호명되었다. 너무 깜짝 놀랐다. 다른 참가자들에 비하면 백지 상태에서 참가한 우리가 본선에 진출하다니...</p>
<p>발표자는 몇 백명 앞에서 우리의 주제에 대해 발표를 하게 되었다. 우리가 함께 고민한 주제를 드디어 여러 사람들 앞에서 소개하게 되는 자리였다. 발표를 맡은 학우는 침착하게 발표를 잘 해냈다. 뒤이어 심사위원들의 질문과 피드백이 이어졌다.</p>
<p>기억나는 질문은 &#39;기부사업을 선정하는 것에는 결국 중개자가 개입해야하는 것이냐&#39;하는 질문이었다. 이 부분에 있어서는 깊게 논의하지 못하기도 했고, 중개자 개입이 필요함을 인정하게 되었다. 
피드백에서는 우리가 우려했던 바와 같이 &quot;사람들이 굳이 블록체인 지갑까지 생성해서 기부를 하려할까?&quot;와 같은 의견이 나왔다. Web3가 대중화가 되지 않는 가장 큰 걸림돌은 역시 지갑 생성 과정인 것 같다...</p>
<p>다른 팀들의 발표를 들으면서 많이 나온 키워드는 &#39;<strong>영지식증명(ZK proof)</strong>&#39;이었다. 나는 영지식증명이 &quot;아무것도 없는 상태에서 개인의 신분을 증명하는 것&quot;이라고 이론적으로만 알고 있었는데, 이 기술을 실제로 해커톤에 적용한 뛰어난 팀들이 많았다. </p>
<p>가장 인상깊었던 팀은 Mass Adoption 소주제에 참가한 NFC 기능을 이용해 트랜잭션 서명을 가능케한 팀이었다. 우리 일상에 스며들어있는 NFC 기능을 블록체인 지갑과 연결시킬 수 있다면 확실히 Web3 대중화에 기여할 수 있을 것이라 생각했다. 이 팀도 영지식증명 기술을 지갑의 authentication을 위해 사용했다. </p>
<p>영지식증명이 중요한 기술임을 알게 되었고, 앞으로 이에 대해 더 공부해봐야겠다.</p>
<p>이외에도 유동성풀 개념을 이용한 팀도 있었다.</p>
<h2 id="회고">회고</h2>
<p>첫 해커톤이 막을 내렸다. 
우리 학회원들은 웹3와 블록체인에 입문한지 얼마안된 병아리들이고, 웹개발과 git 협업이 대부분 처음이었다. 그럼에도 불구하고 해커톤에서 본선진출(62팀 중 18팀이내)을 했다는 것은 너무너무 놀라운 결과였다. 부족하더라도 각자의 역할을 묵묵히 잘 해낸 결과인 것 같다.</p>
<p>그렇지만 후에 해커톤을 뒤돌아봤을 때 <strong>보완해야할 점</strong>들이 많이 보였다.</p>
<h3 id="git-커밋-메시지를-의미있게-작성하기">Git 커밋 메시지를 의미있게 작성하기</h3>
<p>심사위원들이 모든 팀의 코드를 자세히 볼 수는 없을 것이라 생각한다. 그래서 커밋메시지를 통해 우리가 구현한 기술을 보여주는 게 중요하지 않을까 생각이 들었다. 해커톤을 마치고 우리가 제출한 깃허브의 커밋메시지를 봤을 때, 정확히 해당 커밋에서 무엇을 구현한 것인지 모호하다는 생각이 들었다.</p>
<h3 id="배포된-사이트를-보여주기">배포된 사이트를 보여주기</h3>
<p>실제 배포된 사이트를 발표할 때 보여주는게 확실히 플러스가 된다고 생각했다. 배포사이트를 구축한 팀들은 실제로 대상을 타거나 1등을 했다.</p>
<h3 id="github-readme-작성하기">Github README 작성하기</h3>
<p>기획서의 내용을 리드미에도 반영을 하면 좋을 것 같다는 생각을 했다.</p>
<h3 id="기술">기술</h3>
<p>아직 우리 학회는 블록체인 기술과 컨트랙트 개발 측면에서 많이 부족하다고 생각한다. 이에 대해 더 깊이 공부한 후 구현을 해봤으면 한다.</p>
<h3 id="발표자료">발표자료</h3>
<p>기술적으로 구현을 완료하지 못했더라도 프레젠테이션 자료를 통해 기획한 내용을 잘 보여준다면 마이너스 되지 않을 것이라 생각했다.</p>
<h3 id="핵심-feature에-집중하기">핵심 feature에 집중하기</h3>
<p>프론트엔드 페이지는 핵심 페이지 정도만 구현하고, 컨트랙트 개발 중심으로 다같이 고민했으면 기술력 점수를 더 얻지 않았을까 싶다. 우리 학회는 해커톤에서 핵심 feature가 아닌 상세페이지에 치중한 점이 아쉬웠다. 만약 페이지를 많이 만들었다면 배포해서 보여주는 게 나을 것이라 생각한다.</p>
<h3 id="프론트엔드-개발환경설정">프론트엔드 개발환경설정</h3>
<p>eslint나 prettier로 코드스타일을 통일하고, 절대경로 설정 같은 것을 다음 프로젝트에서는 도입해봐야 겠다.</p>
<p>+ 얼마전 쟁글과 LG 사이언스파크가 주관하는 웹3 교육에 참여했는데 해커톤 행사장에서 해당 LG 담당자님을 만나게 되었다. 우리 학회에 대해 기억을 하시는 듯 했는데, 우리가 본선에 진출하는 바람에 더욱 눈도장을 찍게 되었다. 웹3를 더 열심히 공부하지 않으면 안되겠다는 생각이 들었다..!</p>
]]></description>
        </item>
    </channel>
</rss>