<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>comet_strike.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 30 Jan 2024 12:38:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>comet_strike.log</title>
            <url>https://velog.velcdn.com/images/comet_strike/profile/31f098a5-e133-4b24-aa4a-d97e591c2ffa/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. comet_strike.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/comet_strike" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[12. Spring, Spring Boot]]></title>
            <link>https://velog.io/@comet_strike/Spring-Spring-Boot-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@comet_strike/Spring-Spring-Boot-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Tue, 30 Jan 2024 12:38:24 GMT</pubDate>
            <description><![CDATA[<h1 id="spring">Spring</h1>
<p><strong>스프링이란 POJO(Plain Old Java Object)를 통해 자바 애플리케이션을 쉽게 만들 수 있도록 도와주는 프레임워크</strong></p>
<p>Spring Framework is a Java platform that provides comprehensive infrastructure support for developing Java applications. Spring handles the infrastructure so you can focus on your application.</p>
<blockquote>
<p>Spring Framework는 Java 애플리케이션 개발을 위한 포괄적인 인프라 지원을 제공하는 Java 플랫폼입니다. Spring은 인프라를 처리하므로 애플리케이션에 집중할 수 있습니다.</p>
</blockquote>
<p>Spring enables you to build applications from “plain old Java objects” (POJOs) and to apply enterprise services non-invasively to POJOs. This capability applies to the Java SE programming model and to full and partial Java EE. </p>
<blockquote>
<p>Spring을 사용하면 POJO(Plain Old Java Object)에서 애플리케이션을 구축하고 POJO에 엔터프라이즈 서비스를 비침투적으로 적용할 수 있습니다. 이 기능은 Java SE 프로그래밍 모델과 전체 및 부분 Java EE에 적용됩니다.</p>
</blockquote>
<ul>
<li>Introduction to Spring Framework - spring.io<blockquote>
<p><a href="https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/overview.html">https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/overview.html</a></p>
</blockquote>
</li>
</ul>
<h3 id="프레임워크">프레임워크</h3>
<p>프레임워크란 복잡한 문제를 해결하거나 서술하는 데 사용되는 기본 개념 구조이다.</p>
<ul>
<li>소프트웨어 프레임워크 - 위키백과<blockquote>
<p><a href="https://ko.wikipedia.org/wiki/%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4_%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC">https://ko.wikipedia.org/wiki/%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4_%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC</a></p>
</blockquote>
</li>
</ul>
<p>프레임워크란 프로젝트에서 사용할 수 있는 뼈대가 되는 코드의 집합을 의미한다.</p>
<h3 id="라이브러리">라이브러리</h3>
<p>주로 소프트웨어를 개발할 때 컴퓨터 프로그램이 사용하는 비휘발성 자원의 모임이다.</p>
<ul>
<li>라이브러리(컴퓨팅) - 위키백과<blockquote>
<p><a href="https://ko.wikipedia.org/wiki/%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC_(%EC%BB%B4%ED%93%A8%ED%8C%85)">https://ko.wikipedia.org/wiki/%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC_(%EC%BB%B4%ED%93%A8%ED%8C%85)</a></p>
</blockquote>
</li>
</ul>
<p>라이브러리란 혼자서 동작하는 완전한 프로그램이 아닌, 특정한 부분 기능만을 수행하도록 제작된 프로그램이다.</p>
<h2 id="spring-특징">Spring 특징</h2>
<h3 id="pojo-프로그래밍-지향">POJO 프로그래밍 지향</h3>
<p>POJO는 자바 언어 사양 외에 어떠한 제한에도 묶이지 않은 자바 오브젝트라 할 수 있다.
특정 자바 모델이나 기능, 프레임워크 등을 따르지 않은 자바 오브젝트를 지칭하는 말로 사용되었다.</p>
<ul>
<li>Plain Old Java Object - 위키백과<blockquote>
<p><a href="https://ko.wikipedia.org/wiki/Plain_Old_Java_Object">https://ko.wikipedia.org/wiki/Plain_Old_Java_Object</a></p>
</blockquote>
</li>
</ul>
<h3 id="ioc">IoC</h3>
<p>Inverse Of Control, 제어의 역전이란 프로그래머가 작성한 프로그램이 재사용 라이브러리의 흐름 제어를 받게 되는 소프트웨어 디자인 패턴을 의미한다.</p>
<p>전통적인 프로그래밍에서는 프로그래머가 작성한 프로그램이 외부 라이브러리 코드를 호출하지만, IoC가 적용된 구조에서는 외부 라이브러리의 코드가(프레임워크) 프로그래머가 작성한 코드를 호출한다.</p>
<p>제어권이 프로그래머가 작성한 프로그램에서 프레임워크로 역전된다.</p>
<h3 id="di">DI</h3>
<p>Dipendency Injection, 의존성 주입이란 제어의 역전의 방법 중 하나로, 사용할 객체를 직접 생성하지 않고 외부 컨테이너가 생성한 객체를 주입받아 사용하는 방식을 의미한다.</p>
<ul>
<li>스프링 부트 핵심 가이드<blockquote>
<p><a href="https://www.yes24.com/Product/Goods/110142898">https://www.yes24.com/Product/Goods/110142898</a></p>
</blockquote>
</li>
</ul>
<h3 id="aop">AOP</h3>
<p>Aspect of Programing, 관점 지향 프로그래밍으로 관점이란 어떤 기능을 구현할 때 핵심 기능, 부가 기능으로 구분해 각각을 하나의 관점으로 보는 것을 의미한다.</p>
<p>핵심 기능이 비즈니스 로직이라면 부가 기능은 로깅, 트랜잭션 등의 코드가 될 수 있다.</p>
<p>AOP 방식이 아닌경우 한 기능에 핵심 기능 + 부가기능으로 개발해야 한다. 개발해야하는 기능이 여러개라면 각 기능마다 부가기능을 개발해야한다. 부가 기능(로깅, 트랜잭션)등은 공통 코드가 많으므로 비효율적이다.</p>
<p>AOP 방식을 이용한다면 핵심 기능이 어떤 기능인지와 무고나하게 해당 기능이 수행되기 전, 후로 부가 기능을 수행할 수 있다. 여러 비즈니스 로직에서 반복되는 부가 기능을 하나의 공통 로직으로 처리하도록 모듈화 해 사용하는 방식을 AOP라 한다.</p>
<ul>
<li>스프링 부트 핵심 가이드<blockquote>
<p><a href="https://www.yes24.com/Product/Goods/110142898">https://www.yes24.com/Product/Goods/110142898</a></p>
</blockquote>
</li>
</ul>
<h1 id="spring-boot">Spring Boot</h1>
<p>스프링 프레임워크는 기존 개발 방식의 문제와 한계를 극복하기 위해 다양한 기능을 제공하지만 설정이 복잡하다. 스프링 부트는를 이용해 기존의 복잡한 설정을 간편하게 처리해주는 별도의 프레임워크이다.</p>
<p>스프링 부투는 내장 웹 서버가 있기 때문에 실행 시 바로 웹 서비스를 제공할 수 있다. 또한, 독립적으로 실행 가능한 Jar 파일로 프로젝트를 빌드할 수 있어 다양한 환경에서 배포할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[11. CORS, CSRF 해결]]></title>
            <link>https://velog.io/@comet_strike/11.-CORS-CSRF-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@comet_strike/11.-CORS-CSRF-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Sun, 05 Nov 2023 11:44:42 GMT</pubDate>
            <description><![CDATA[<h2 id="spring-security-cors-csrf">Spring Security CORS, CSRF</h2>
<p> CORS, CSRF가 무엇인지 부터 알아야 하겠지만, 우선 테스트가 목적이므로 간략하게 알아본 뒤 코드를 적용한다.</p>
<p> 결론부터 말하자면 CORS는 테스트를 위해서 모든 Origin(출처)에 대해서 허용하도록 수행하고, CSRF는 Disable 상태로 변경한다.</p>
<h3 id="cors">CORS</h3>
<p> CORS는 Cross Origin Resuorce Sharing의 약자로 교차-출처 리소스 공유로 다른 출처 사이에 정보를 공유하게 되면 발생하는 정책이다.</p>
<p> 출처는 Protocol, Host, 포트번호를 의미한다. 따라서 프론트 주소인 localhost:3000와 백엔드 주소인 localhost:8080은 서로 다른 출처가 되게 되어 CORS 정책에 의해 차단된다.</p>
<p> CORS가 없을 경우, CSRF(Cross Site Request Forgery) 문제가 발생한다.</p>
<p> <img src="https://velog.velcdn.com/images/comet_strike/post/98b8fe95-64cf-426d-ac20-54cb11e89ca3/image.png" alt=""></p>
<ul>
<li><p>CORS란 무엇인가요? - amazon</p>
<blockquote>
<p><a href="https://aws.amazon.com/ko/what-is/cross-origin-resource-sharing/">https://aws.amazon.com/ko/what-is/cross-origin-resource-sharing/</a></p>
</blockquote>
</li>
<li><p>CORS는 왜 이렇게 우리를 힘들게 하는걸까?</p>
<blockquote>
<p><a href="https://evan-moon.github.io/2020/05/21/about-cors/">https://evan-moon.github.io/2020/05/21/about-cors/</a></p>
</blockquote>
</li>
<li><p>CSRF 공격이란? 그리고 CSRF 방어 방법</p>
<blockquote>
<p><a href="https://itstory.tk/entry/CSRF-%EA%B3%B5%EA%B2%A9%EC%9D%B4%EB%9E%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-CSRF-%EB%B0%A9%EC%96%B4-%EB%B0%A9%EB%B2%95">https://itstory.tk/entry/CSRF-%EA%B3%B5%EA%B2%A9%EC%9D%B4%EB%9E%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-CSRF-%EB%B0%A9%EC%96%B4-%EB%B0%A9%EB%B2%95</a></p>
</blockquote>
</li>
</ul>
<h3 id="csrf">CSRF</h3>
<p>  CSRF(Cross Site Request Forgery) - 사이트 간 요청 위조. 어떤 서버에서 인증된 브라우저의 정보를 공격자가 만든 악성 페이지를 통해 사용하여 사용자 모르게 공격을 수행한다.  </p>
<p>  쿠키를 통한 CSRF 공격이 수행되기 때문에 쿠키나 세션에 의존하지 않는 REST API, JWT를 사용하면 비교적 안전하다. 그래도 CSRF를 disable할 경우 보안 취약점이 생길 수 있으므로 주의해야 한다.</p>
<ul>
<li>CSRF(Cross-Site Request Forgery) 공격과 방어<blockquote>
<p><a href="https://junhyunny.github.io/information/security/spring-boot/spring-security/cross-site-reqeust-forgery/">https://junhyunny.github.io/information/security/spring-boot/spring-security/cross-site-reqeust-forgery/</a></p>
</blockquote>
</li>
</ul>
<h3 id="spring-security-cors-csrf-예외-적용">Spring Security CORS, CSRF 예외 적용</h3>
<p> SecurityFilterChain에 CORS, CSRF 예외 정책을 적용한다.</p>
<p> <img src="https://velog.velcdn.com/images/comet_strike/post/02f59e8a-b9ab-41b4-95ff-a35a79a25074/image.png" alt=""></p>
<p>  SecurityConfig.java 파일을 생성하고. Configuration 어노테이션 등을 붙여준다. 이후 Security FilterChain에 cors, csrf 예외 코드를 적용한다. 
  CorsConfigurationSource를 생성해서 모든 Origin을 허용하도록 적용시키고 csrf.disable()을 통해 csrf를 비활성화 시킨다. (csrf 활성화 되어 있는 경우 403 Error 발생)</p>
<p>이후 실행하면 정상적으로 값을 가져오는 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/comet_strike/post/62e421d8-7d9c-4ab2-af16-74c1353f8d6e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/f356c5ef-58d5-4f34-b28e-70ed10d0c680/image.png" alt=""></p>
<h3 id="github">GitHub</h3>
<p>수정 사항 Git 반영.</p>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/4cef0100-c9be-4ca6-b4ef-019f77841f2d/image.png" alt=""></p>
<h2 id="전체-코드">전체 코드</h2>
<p>SecurityConfig.java</p>
<pre><code class="language-java">package com.toyproject.toyproject;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .cors(Customizer.withDefaults())
                .csrf((csrf) -&gt; csrf.disable());

        return http.build();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration(&quot;/**&quot;, new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
}</code></pre>
<p>tempController.java : RequestBody Parameter 수정</p>
<pre><code class="language-java">package com.toyproject.toyproject;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping(&quot;/user&quot;)
public class tempController {

    @PostMapping(&quot;/signUp&quot;)
    public String userSignUp(@RequestBody Map&lt;String, String&gt; userInfo) {
        System.out.println(&quot;signUp&quot;);
        System.out.println(userInfo);
        return &quot;returnValue&quot;;
        //return new ResponseEntity&lt;Boolean&gt;(Boolean.TRUE, HttpStatus.OK);
        //return new ResponseEntity&lt;Boolean&gt;(Boolean.FALSE, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @GetMapping(&quot;/test&quot;)
    public String userSignUp() {
        System.out.println(&quot;Test&quot;);
        return &quot;Test&quot;;
    }
}</code></pre>
<p>build.gradle</p>
<pre><code class="language-bash">plugins {
    id &#39;java&#39;
    id &#39;org.springframework.boot&#39; version &#39;3.1.2&#39;
    id &#39;io.spring.dependency-management&#39; version &#39;1.1.2&#39;
}

group = &#39;com.toy-project&#39;
version = &#39;0.0.1-SNAPSHOT&#39;

java {
    sourceCompatibility = &#39;17&#39;
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-oauth2-client&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-oauth2-resource-server&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
    implementation &#39;org.springframework.kafka:spring-kafka&#39;
    compileOnly &#39;org.projectlombok:lombok&#39;
    runtimeOnly &#39;com.h2database:h2&#39;
    annotationProcessor &#39;org.springframework.boot:spring-boot-configuration-processor&#39;
    annotationProcessor &#39;org.projectlombok:lombok&#39;
    testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39;
    testImplementation &#39;org.springframework.kafka:spring-kafka-test&#39;
    testImplementation &#39;org.springframework.security:spring-security-test&#39;
}

tasks.named(&#39;test&#39;) {
    useJUnitPlatform()
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[10. 회원가입 기능 구현 2 (Fetch API VS Axios)]]></title>
            <link>https://velog.io/@comet_strike/10.-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-2-Fetch-API-VS-Axios</link>
            <guid>https://velog.io/@comet_strike/10.-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-2-Fetch-API-VS-Axios</guid>
            <pubDate>Mon, 09 Oct 2023 11:56:50 GMT</pubDate>
            <description><![CDATA[<p>이메일 양식 검사와 패스워드 양식 검사를 통과한 후 SIGN ON 버튼을 클릭 했을 때, Client는 (웹 브라우저) Server로 HTTPS / POST 방식으로 email과 password를 전송해야 한다.
(물론 그 전에 email 중복 체크를 해야 한다.)</p>
<p>Client에서 Server로 HTTP 형식의 데이터를 전송하기 위해서 사용하는 방법으로는 Fetch API와 Axios가 잇다.</p>
<h3 id="fetch-api">Fetch API</h3>
<p>Fetch API는 HTTP 파이프라인을 구성하는 요청과 응답 등의 요소를 JavaScript에서 접근하고 조작할 수 있는 인터페이스를 제공한다. Fetch API가 제공하는 전역 fetch() 메서드로 네트워크의 리소스를 쉽게 비동기적으로 취득할 수도 있습니다.</p>
<ul>
<li>Fetch API 사용하기<blockquote>
<p><a href="https://developer.mozilla.org/ko/docs/Web/API/Fetch_API/Using_Fetch">https://developer.mozilla.org/ko/docs/Web/API/Fetch_API/Using_Fetch</a></p>
</blockquote>
</li>
</ul>
<h3 id="axios">Axios</h3>
<p>Axios는 node.js와 브라우저를 위한 Promise 기반 HTTP 클라이언트로 비동기 통신 라이브러리.</p>
<ul>
<li>Axios Docs<blockquote>
<p><a href="https://axios-http.com/kr/docs/intro">https://axios-http.com/kr/docs/intro</a></p>
</blockquote>
</li>
</ul>
<h3 id="fetch-api-vs-axios">Fetch API VS Axios</h3>
<p>대체로 큰 차이는 없으나 Library 설치 유무 및 브라우저 지원 여부 및 사용법에 따라 결정하면 된다.</p>
<p>Fetch API : 라이브러리 미 설치
Axios : 라이브러리 설치, Fetch 보다 다양한 브라우저 지원</p>
<ul>
<li>Axios vs. fetch(): Which is best for making HTTP requests?<blockquote>
<p><a href="https://blog.logrocket.com/axios-vs-fetch-best-http-requests/">https://blog.logrocket.com/axios-vs-fetch-best-http-requests/</a></p>
</blockquote>
</li>
</ul>
<p>기존에 Axios를 사용해 API를 개발했기 때문에 Axios를 사용할 예정이다.</p>
<h2 id="axios를-통한-회원-가입-정보-전달">Axios를 통한 회원 가입 정보 전달</h2>
<p>Axios를 이용해서 회원 가입 정보를 Server로 (Spring Boot) 전달한다.</p>
<h3 id="axios-설치">Axios 설치</h3>
<p>Axios 라이브러리를 설치한다.</p>
<pre><code class="language-bash">npm install axios</code></pre>
<h3 id="axios-설정">Axios 설정</h3>
<p>Axios를 통한 통신을 관리하기 위해서 
<strong>1. .env 파일을 통해 서버 URL을 설정한다.</strong></p>
<p>ios를 통해서 서버와 통신하기 위해서는 서버 URL을 사용해야 한다. 매번 URL을 입력하거나, URL이 바뀔 경우 일괄적으로 처리하기 위해서 .env 파일을 생성한다.</p>
<p><strong>2. util 폴더를 생성해 http 통신 관련 모듈을 관리한다.</strong></p>
<p>src 폴더 밑 util 폴더를 생성하고, 폴더 밑에 http-common.js 파일을 생성해 axios 밑 http 통신에 필요한 정보를 관리한다.</p>
<p><strong>3. service 폴더를 생성해 request를 관리한다.</strong></p>
<p>src 폴더 밑 service 폴더를 생성하고, 폴더 밑에 필요에 따라 js  파일을 생성해 axios 요청을 생성한다. 
해당 요청이 필요시에는  vue파일에서 js를 import 해서 사용한다.</p>
<h4 id="1-env-파일을-통해-서버-url을-설정한다">1. .env 파일을 통해 서버 URL을 설정한다.</h4>
<p>.env 파일은 프로젝트 폴더 최상위에 생성한다.
<img src="https://velog.velcdn.com/images/comet_strike/post/a2908c0e-9f35-4c01-88c5-e8e3a6d21ba4/image.png" alt=""></p>
<p>파일을 생성한후 서버 URL 주소를 입력한다.
Spring Boot를 실행하게 될 경우 8080(http) 톰캣 서버가 실행되는 걸 알 수 있다.
SSL 인증서가 없어 http로 실행된다. 인증서를 추가해 http -&gt;https로 변경해 보안을 강화해야 한다.
<img src="https://velog.velcdn.com/images/comet_strike/post/bbb7a5fc-216b-4c33-9dc6-6f897d2b434d/image.png" alt=""></p>
<pre><code class="language-bash">LOCAL_DEVELOP_SERVER_URL = http://localhost:8080/</code></pre>
<h4 id="2-util-폴더를-생성해-http-통신-관련-모듈을-관리한다">2. util 폴더를 생성해 http 통신 관련 모듈을 관리한다.</h4>
<p>이후 src 폴더 밑에 util 폴더를 생성하고 http-common.js 파일을 생성한다.
js 파일에 axios를 import 하고 통신할 서버를 설정한다.</p>
<pre><code class="language-javascript">import axios from &quot;axios&quot;;

export default axios.create({
    baseURL: process.env.LOCAL_DEVELOP_SERVER_URL,
    headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
    },
});</code></pre>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/9579e50a-4b7b-42af-8ff5-0b3ccd3d303a/image.png" alt=""></p>
<h4 id="3-service-폴더를-생성해-request를-관리한다">3. service 폴더를 생성해 request를 관리한다.</h4>
<p>마찬가지로 src 폴더 밑에 service 폴더를 생성하고 UserService.js를 생성한다.</p>
<pre><code class="language-javascript">import http from &quot;@/util/http-commons&quot;

export default class UserService {
    async postSignUp(params) {
        // params
        // userEmail : [email Type (string)]
        // passowrd : [password Type (string)]
        // return : boolean (creation result)
        return await http.post(&quot;user/signUp&quot;, params).then((data) =&gt; data.data)
    }
}</code></pre>
<p>Axios를 정의한 @/util/http-commons를 호출하고 http.post 방식으로 정보를 전달한다.
parmas를 매개변수로 받아서 호출 시 파라미터를 전달할 수 있도록 설정한다. [URL]/user/signUp 으로 POST 하도록 설정한다.</p>
<p>async와 await를 이용해서 비동기 처리를 하도록 한다.</p>
<ul>
<li>Axios Docs - Post Example<blockquote>
<p><a href="https://axios-http.com/kr/docs/post_example">https://axios-http.com/kr/docs/post_example</a></p>
</blockquote>
</li>
</ul>
<h3 id="axios-post-적용">Axios Post 적용</h3>
<p>SginUpPage.vue에서 [SIGN UP] 버튼이 눌렸을 때, Axios를 통해 POST 요청을 보내도록 적용한다.
위에서 UserService.js에 axios를 사용하기 위한 셋팅을 해두었으므로 import한 후 parameter를 적절하게 설정하면 된다.</p>
<p>Script 부분에서 import를 시켜준다. data에 userService를 생성하고 created 될 때, userService에 UserService 클래스를 담을 수 있도록 설정한다.</p>
<pre><code class="language-javascript">import UserService from &#39;../service/UserService&#39;

export default {
    created() {
        this.userService = new UserService();
    },
    data: () =&gt; ({
        form: false,
        email: null,
        password: null,
        passwordVerify: null,
        loading: false,
        userService: null,</code></pre>
<p>[SIGN UP] 버튼이 눌렸을 때, methods의 onSubmit()이 호출 되므로 입력된 정보를 양식에 맞게 설정한 후 전달한다.</p>
<pre><code class="language-javascript"> methods: {
        onSubmit () {
            if (!this.form) { 
                alert(&#39;Please follow the input form&#39;)
                return
            }

            this.loading = true

            let userData = {
                userEmail : this.email,
                userPassword : this.password
            }

            this.userService.postSignUp(userData).then(data =&gt; {
                if(!!data) alert(&#39;Creation has been completed.&#39;)
                else alert(&#39;A problem has occurred.&#39;)
            })

            this.loading = false
        },</code></pre>
<p>입력 양식을 체크한 후 userData 변수를 생성해서 param 양식을 맞춘다. userService에서 미리 정의해 두었던 postSignUp을 호출한다. 
.then을 통해서 return data에 따라 alert 메시지를 다르게 표시한다.</p>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/94ea95c6-a9e0-4caa-9b79-20f383f80930/image.png" alt=""></p>
<p>SIGN UP 시도 시 404 에러가 발생한다. Spring Boot를 통한 톰캣 서버를 실행하지 않아 서버를 못찾아서 발생한 에러로 서버를 설정한 후 최종 테스트를 진행한다.</p>
<h3 id="vue-server-port-설정">Vue Server Port 설정</h3>
<p>현재 vue는 아무런 설정을 하지 않았기 때문에 localhost:8080으로 실행중이다. 포트를 변경해 포트가 겹치지 않도록 설정한다.
<img src="https://velog.velcdn.com/images/comet_strike/post/23bbfb31-82d1-4c65-8fc7-ab3e3fbed6fb/image.png" alt=""></p>
<p>package.json 파일안에 &quot;scripts&quot; 부분을 변경한다. &quot;serve&quot; 부분을 변경해 실행시 포트 3000으로 실행되도록 설정한다.</p>
<pre><code class="language-json">  &quot;scripts&quot;: {
    &quot;serve&quot;: &quot;vue-cli-service serve --port 3000&quot;,
    &quot;build&quot;: &quot;vue-cli-service build&quot;,
    &quot;lint&quot;: &quot;vue-cli-service lint&quot;
  },</code></pre>
<h2 id="spring-boot-설정">Spring Boot 설정</h2>
<p>Spring Boot를 본격적으로 진행하기 전에 Axios를 통해 값이 정상적으로 넘어갔는지만 확인하기 위해 간단하게 수신만 하도록 설정한다.</p>
<p>Intellij를 실행해서 전에 실행했던 프로젝트를 띄운다. 임시적으로 사용할 tempController를 만들어준다.
<img src="https://velog.velcdn.com/images/comet_strike/post/024774e2-3255-43d1-b02f-a1bb15d0dba3/image.png" alt=""></p>
<p>@RestController 어노테이션을 이용해서 REST API에 사용됨을 명시한다. 이후 @RequestMapping 어노테이션을 통해서 &quot;/user&quot;에 해당하는 url을 가져온 후 tempController 클래스에 userSignUp의 ResponseEntity를 생성한다. return으로 Boolean 값을 설정한다.</p>
<pre><code class="language-java">package com.toyproject.toyproject;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping(&quot;/user&quot;)
public class tempController {

    @PostMapping(&quot;signUp&quot;)
    public ResponseEntity&lt;?&gt; userSignUp(@RequestBody List&lt;Map&gt; userInfo) {
        System.out.println(userInfo);
        return new ResponseEntity&lt;Boolean&gt;(Boolean.TRUE, HttpStatus.OK);
        //return new ResponseEntity&lt;Boolean&gt;(Boolean.FALSE, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}</code></pre>
<h2 id="test">TEST</h2>
<p>Spring Boot 실행하고 서버가 정상적으로 작동하는 것을 확인하고 다시 테스트 진행.
테스트 결과... POST 요청 URL이 .env 설정한데로 지정되지 않는 현상으로 인해 Error가 발생했다.</p>
<pre><code class="language-javascript">console.log(&quot;url : &quot;, process.env.LOCAL_DEVELOP_SERVER_URL)</code></pre>
<p>userService.js에 console.log 추가한 후 콘솔에 출력시 undefined 발생... 구글링 결과 .env 파일에서 사용할려면 VUE_APP을 PREFIX로 지정해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/eff1c657-8cb5-4efd-855b-4bb2b859f00e/image.png" alt=""></p>
<p><strong>.env 파일</strong></p>
<pre><code class="language-javascript">VUE_APP_LOCAL_DEVELOP_SERVER_URL = http://localhost:8080/</code></pre>
<p><strong>http-commons.js 파일</strong></p>
<pre><code class="language-javascript">import axios from &quot;axios&quot;;

export default axios.create({
    baseURL: process.env.VUE_APP_LOCAL_DEVELOP_SERVER_URL,
    headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
    },
});</code></pre>
<p><strong>UserService.js 파일</strong></p>
<pre><code class="language-javascript">import http from &quot;@/util/http-commons&quot;

export default class UserService {
    async postSignUp(params) {
        // params
        // userEmail : [email Type (string)]
        // passowrd : [password Type (string)]
        // return : boolean (creation result)
        console.log(&quot;url : &quot;, process.env.VUE_APP_LOCAL_DEVELOP_SERVER_URL)
        return await http.post(&quot;user/signUp&quot;, params).then((data) =&gt; data.data)
    }
}</code></pre>
<p>파일 수정 후 실행 시 동일한 현상으로 차단되는 경우 서버를 재실행</p>
<blockquote>
<p><a href="https://stackoverflow.com/questions/55510326/vue-cli-3-environment-variables-all-undefined">https://stackoverflow.com/questions/55510326/vue-cli-3-environment-variables-all-undefined</a></p>
</blockquote>
<p>실행 결과 다른 에러가 발생했다.
<img src="https://velog.velcdn.com/images/comet_strike/post/83c7eb2d-e646-406f-9607-7807abc777c5/image.png" alt=""></p>
<p>Network Error 발생... CORS로 인해 발생한 에러로 CORS를 설정한다.
tempController 어노테이션 위에 @CrossOrigin 어노테이션을 추가한다. </p>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/019a375e-ef69-41db-a023-d6932c3e8aab/image.png" alt=""></p>
<p>tempController.java</p>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/user&quot;)
@CrossOrigin(origins = { &quot;*&quot; }, methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE} , maxAge = 6000)
public class tempController {

    @PostMapping(&quot;/signUp&quot;)
    public ResponseEntity&lt;?&gt; userSignUp(@RequestBody List&lt;Map&gt; userInfo) {
        System.out.println(userInfo);
        return new ResponseEntity&lt;Boolean&gt;(Boolean.TRUE, HttpStatus.OK);
        //return new ResponseEntity&lt;Boolean&gt;(Boolean.FALSE, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}</code></pre>
<p>@CrossOrigin 어노테이션을 적용해도... 동일한 에러가 발생
찾아보니 Spring Security에서 CORS를 적용하기 위해서는  설정이 추가로 필요하다.</p>
<ul>
<li>@CrossOrigin not working<blockquote>
<p><a href="https://www.skyer9.pe.kr/wordpress/?p=3571">https://www.skyer9.pe.kr/wordpress/?p=3571</a></p>
</blockquote>
</li>
</ul>
<h4 id="spring-security에서-cors-설정">Spring Security에서 CORS 설정</h4>
<p>Spring security에서 CORS설정해서 진행할려 했지만... 최근에 업데이트가 된건지 찾았던 코드들은 모두 삭제가 되었다... 
Spring Boot와 Spring Security를 공부하고 CORS를 적용한 후에 테스트를 다시 수행한다.</p>
<h2 id="전체-코드">전체 코드</h2>
<h3 id="1-axios를-통한-회원-가입-정보-전달">1. Axios를 통한 회원 가입 정보 전달</h3>
<p>SignUpPage.vue</p>
<pre><code class="language-vue">&lt;template&gt;
    &lt;v-sheet class=&quot;pa-12&quot; rounded&gt;
        &lt;v-card class=&quot;mx-auto px-6 py-8&quot; max-width=&quot;60%&quot; :elevation=&quot;12&quot;&gt;
            &lt;v-form
            v-model=&quot;form&quot;
            @submit.prevent=&quot;onSubmit&quot;
            &gt;
                &lt;v-text-field
                    v-model=&quot;email&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[rules.required, rules.email.expertTest, emailDuplicateCheck, rules.email.lengthCheck]&quot;
                    class=&quot;mb-2&quot;
                    clearable
                    label=&quot;Email&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;v-text-field
                    v-model=&quot;password&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[rules.required, rules.passowrd.expertTest, rules.passowrd.lengthCheck]&quot;
                    type=&quot;password&quot;
                    clearable
                    label=&quot;Password&quot;
                    placeholder=&quot;Enter your password&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;v-text-field
                    v-model=&quot;passwordVerify&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[rules.required, passwordMatchCheck]&quot;
                    type=&quot;password&quot;
                    clearable
                    label=&quot;Password&quot;
                    placeholder=&quot;Enter your password&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;br&gt;

                &lt;v-btn
                    :disabled=&quot;!form&quot;
                    :loading=&quot;loading&quot;
                    block
                    color=&quot;primary&quot;
                    size=&quot;large&quot;
                    type=&quot;submit&quot;
                    variant=&quot;elevated&quot;
                &gt;
                    Sign Up
                &lt;/v-btn&gt;
            &lt;/v-form&gt;
        &lt;/v-card&gt;
    &lt;/v-sheet&gt;
&lt;/template&gt;

&lt;script&gt;
import UserService from &#39;../service/UserService&#39;

export default {
    created() {
        this.userService = new UserService();
    },
    data: () =&gt; ({
        form: false,
        email: null,
        password: null,
        passwordVerify: null,
        loading: false,
        userService: null,
        rules: {
            email: {
                expertTest: v =&gt; {
                    const emailExpert = /^[A-Za-z0-9_\\.\\-]+@[A-Za-z0-9\\-]+\.[A-Za-z0-9\\-]+/
                    return !!emailExpert.test(v) || &#39;It must be in email format.&#39;
                },
                lengthCheck: v =&gt; {
                    return v.length &lt; 50 || &#39;The email you entered is too long.&#39;
                }
            },
            passowrd: {
                expertTest: v =&gt; {
                    const passwordExpert = /^(?=(?:[^a-zA-Z]*[a-zA-Z]))(?=(?:\D*\d))(?=(?:[^\W_]*[\W_])).{8,}$/
                    return !!passwordExpert.test(v) || &#39;It must be at least 8 characters and a combination of 3 or more letters, numbers, and special characters.&#39;
                },
                lengthCheck: v =&gt; {
                    return v.length &lt; 50 || &#39;The password you entered is too long.&#39;
                }
            },
            required: v =&gt; !!v || &#39;Field is required&#39;
        },
    }),

    methods: {
        onSubmit () {
            if (!this.form) { 
                alert(&#39;Please follow the input form&#39;)
                return
            }

            this.loading = true

            let userData = {
                userEmail : this.email,
                userPassword : this.password
            }

            this.userService.postSignUp(userData).then(data =&gt; {
                if(data) alert(&#39;Creation has been completed.&#39;)
                else alert(&#39;A problem has occurred.&#39;)
            })

            this.loading = false
        },
        emailDuplicateCheck (email) {
            console.log(email)
            // duplicateCheck: v =&gt; this.emailDuplicateCheck(v) || &#39;This is a duplicate email. Please use a different email.&#39;,
            return true;
            // true가 아닐 경우 &#39;This is a duplicate email. Please use a different email.&#39;,
        },
        passwordMatchCheck (passwordVerify) {
            return (this.password === passwordVerify) || &#39;It does not match your password.&#39;
        },

    },
}
&lt;/script&gt;</code></pre>
<p>.env</p>
<pre><code>VUE_APP_LOCAL_DEVELOP_SERVER_URL = http://localhost:8080/</code></pre><p>http-common.js</p>
<pre><code class="language-javascript">import axios from &quot;axios&quot;;

export default axios.create({
    baseURL: process.env.VUE_APP_LOCAL_DEVELOP_SERVER_URL,
    headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
    },
});
</code></pre>
<p>UserService.js</p>
<pre><code class="language-javascript">import http from &quot;@/util/http-commons&quot;

export default class UserService {
    async postSignUp(params) {
        // params
        // userEmail : [email Type (string)]
        // passowrd : [password Type (string)]
        // return : boolean (creation result)
        console.log(&quot;url : &quot;, process.env.VUE_APP_LOCAL_DEVELOP_SERVER_URL)
        return await http.post(&quot;user/signUp&quot;, params).then((data) =&gt; data.data)
    }
}</code></pre>
<p>package.json</p>
<pre><code>{
  &quot;name&quot;: &quot;toy-project&quot;,
  &quot;version&quot;: &quot;0.1.0&quot;,
  &quot;private&quot;: true,
  &quot;scripts&quot;: {
    &quot;serve&quot;: &quot;vue-cli-service serve --port 3000&quot;,
    &quot;build&quot;: &quot;vue-cli-service build&quot;,
    &quot;lint&quot;: &quot;vue-cli-service lint&quot;
  },
  &quot;dependencies&quot;: {
    &quot;@mdi/font&quot;: &quot;5.9.55&quot;,
    &quot;axios&quot;: &quot;^1.5.1&quot;,
    &quot;core-js&quot;: &quot;^3.8.3&quot;,
    &quot;roboto-fontface&quot;: &quot;*&quot;,
    &quot;vue&quot;: &quot;^3.2.13&quot;,
    &quot;vue-router&quot;: &quot;^4.2.4&quot;,
    &quot;vuetify&quot;: &quot;^3.3.11&quot;,
    &quot;webfontloader&quot;: &quot;^1.0.0&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;@babel/core&quot;: &quot;^7.12.16&quot;,
    &quot;@babel/eslint-parser&quot;: &quot;^7.12.16&quot;,
    &quot;@vue/cli-plugin-babel&quot;: &quot;~5.0.0&quot;,
    &quot;@vue/cli-plugin-eslint&quot;: &quot;~5.0.0&quot;,
    &quot;@vue/cli-plugin-router&quot;: &quot;^5.0.8&quot;,
    &quot;@vue/cli-service&quot;: &quot;~5.0.0&quot;,
    &quot;eslint&quot;: &quot;^7.32.0&quot;,
    &quot;eslint-plugin-vue&quot;: &quot;^8.0.3&quot;,
    &quot;vue-cli-plugin-vuetify&quot;: &quot;~2.5.8&quot;,
    &quot;webpack-plugin-vuetify&quot;: &quot;^2.0.0-alpha.0&quot;
  },
  &quot;eslintConfig&quot;: {
    &quot;root&quot;: true,
    &quot;env&quot;: {
      &quot;node&quot;: true
    },
    &quot;extends&quot;: [
      &quot;plugin:vue/vue3-essential&quot;,
      &quot;eslint:recommended&quot;
    ],
    &quot;parserOptions&quot;: {
      &quot;parser&quot;: &quot;@babel/eslint-parser&quot;
    },
    &quot;rules&quot;: {}
  },
  &quot;browserslist&quot;: [
    &quot;&gt; 1%&quot;,
    &quot;last 2 versions&quot;,
    &quot;not dead&quot;,
    &quot;not ie 11&quot;
  ]
}</code></pre><p>tempController.java</p>
<pre><code class="language-java">package com.toyproject.toyproject;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@CrossOrigin(originPatterns = &quot;http://localhost:3000&quot;)
@RestController
@RequestMapping(&quot;/user&quot;)
public class tempController {

    @PostMapping(&quot;/signUp&quot;)
    public String userSignUp(@RequestBody List&lt;Map&gt; userInfo) {
        System.out.println(userInfo);
        return &quot;returnValue&quot;;
        //return new ResponseEntity&lt;Boolean&gt;(Boolean.TRUE, HttpStatus.OK);
        //return new ResponseEntity&lt;Boolean&gt;(Boolean.FALSE, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @GetMapping(&quot;/test&quot;)
    public String userSignUp() {
        return &quot;Test&quot;;
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[9. 회원가입 기능 구현]]></title>
            <link>https://velog.io/@comet_strike/9.-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@comet_strike/9.-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sun, 08 Oct 2023 12:19:28 GMT</pubDate>
            <description><![CDATA[<p>회원가입 기능을 구현한다.</p>
<p>회원 가입 기능을 구현하기 위해서는 Passowrd 필드 아래에 입력한 Password가 맞는지 한번 더 확인하는 필드가 추가로 필요하다. v-model 또한 script에서 data로 지정이 필요하다.</p>
<h2 id="회원-가입-기능-구현">회원 가입 기능 구현</h2>
<ol>
<li>Passowrd Verify를 추가한다.</li>
<li>Email, Password, Password Verify를 검증하는 rules를 지정하고 SIGN UP form을 활성화 시킨다.</li>
<li>Client에서 Server로 로그인 정보를 전송한다.</li>
</ol>
<h3 id="1-password-verify-추가">1. Password Verify 추가</h3>
<p>Password 와 동일하게 생성하되 v-model만 다르게 지정한다.</p>
<p>script의 data에 passwordVerify를 생성한다.
script는 Vuetify 참고 (Forms)</p>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/b64c3ee7-61f1-4435-870d-fcd38956d31e/image.png" alt=""></p>
<blockquote>
<p><a href="https://vuetifyjs.com/en/components/text-fields/">https://vuetifyjs.com/en/components/text-fields/</a></p>
</blockquote>
<pre><code class="language-vue">                &lt;v-text-field
                    v-model=&quot;password&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[required]&quot;
                    clearable
                    label=&quot;Password&quot;
                    placeholder=&quot;Enter your password&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;v-text-field
                    v-model=&quot;passwordVerify&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[required]&quot;
                    clearable
                    label=&quot;Password&quot;
                    placeholder=&quot;Enter your password&quot;
                &gt;&lt;/v-text-field&gt;</code></pre>
<pre><code class="language-javascript">&lt;script&gt;
  export default {
    data: () =&gt; ({
      form: false,
      email: null,
      password: null,
      passwordVerify: null,
      loading: false,
    }),

    methods: {
      onSubmit () {
        if (!this.form) return

        this.loading = true

        setTimeout(() =&gt; (this.loading = false), 2000)
      },
      required (v) {
        return !!v || &#39;Field is required&#39;
      },
    },
  }
&lt;/script&gt;</code></pre>
<h3 id="2-email-password-password-verify-형식-검사">2. Email, Password, Password Verify 형식 검사</h3>
<h4 id="email-형식-검사">Email 형식 검사</h4>
<p>rules를 만들어서 실시간 검증이 되도록 적용한다.</p>
<ol>
<li>required를 통해서 필수 필드임을 나타낸다.</li>
<li>expertTest를 통해서 email 형식인지 검증한다.</li>
<li>duplicateCheck를 통해서 이미 가입된 이메일인지 확인한다.</li>
</ol>
<p>duplicateCheck의 경우에는 emailDuplicatecheck 메소드를 생성해서 Server로 데이터를 전송하고 리턴값을 받아서 처리해야 한다. Server와 연동 후 구현 예정이라 임시적으로 true를 리턴하도록 한다.</p>
<pre><code class="language-vue">                &lt;v-text-field
                    v-model=&quot;email&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[rules.required, rules.email.expertTest, rules.email.emailDuplicateCheck]&quot;
                    class=&quot;mb-2&quot;
                    clearable
                    label=&quot;Email&quot;
                &gt;&lt;/v-text-field&gt;</code></pre>
<pre><code class="language-javascript">&lt;script&gt;
export default {
    data: () =&gt; ({
        form: false,
        email: null,
        password: null,
        passwordVerify: null,
        loading: false,
        rules: {
            email: {
                expertTest: v =&gt; {
                    const emailExpert = /^[A-Za-z0-9_\\.\\-]+@[A-Za-z0-9\\-]+\.[A-Za-z0-9\\-]+/
                    return !!emailExpert.test(v) || &#39;It must be in email format.&#39;
                },
                duplicateCheck: v =&gt; {
                    return this.emailDuplicateCheck(v) || &#39;This is a duplicate email. Please use a different email.&#39;
                }
            },
            required: v =&gt; !!v || &#39;Field is required&#39;
        },
    }),

    methods: {
        onSubmit () {
            if (!this.form) return

            this.loading = true

            setTimeout(() =&gt; (this.loading = false), 2000)
        },
        emailDuplicateCheck (email) {
            console.log(email)
            return true;
        }

    },
}
&lt;/script&gt;</code></pre>
<ul>
<li>Vuetify 방식의 실시간 검증 방식<blockquote>
<p><a href="https://sundries-in-myidea.tistory.com/130">https://sundries-in-myidea.tistory.com/130</a></p>
</blockquote>
</li>
</ul>
<p>위 내용에 따르면 rules를 이용해 실시간 검증 방식을 사용하게 되면 Server와 통신하면서 제대로 반영이 안되는 문제가 있는 것으로 판단된다. 이메일 중복 검사의 경우 버튼을 생성해 처리하던가 중복과 관련된 상태값을 추가로 설정해서 판별 후 화면에 출력하는 형태로 바꾸어야 할 거 같다.</p>
<h4 id="password-형식-검사">Password 형식 검사</h4>
<p>Password 규칙은 KISA 패스워드 선택 및 이용 안내서를 따른다.</p>
<blockquote>
<p><a href="https://www.kisa.or.kr/2060305/form?postSeq=14&amp;page=1#fnPostAttachDownload">https://www.kisa.or.kr/2060305/form?postSeq=14&amp;page=1#fnPostAttachDownload</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/79e7c6ec-26ec-41f1-b4bc-d53170627115/image.png" alt=""></p>
<p>패스워드 생성 규칙만 정리하면 다음과 같다.</p>
<ol>
<li>패스워드 최소 8자 이상</li>
<li>영문, 숫자, 특수 기호를 조합하여 사용할 수 있도록 허용</li>
</ol>
<p>위 규칙으로만은 모호한 부분이 있어서 추가로 정보를 찾은 결과 개인정보의_기술적_관리적_보호조치_기준(제2020-5호)_해설서(2020.12월)를 참고 했다.</p>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/a7c70225-640f-4113-a0d5-d01bd704b2a7/image.png" alt=""></p>
<ul>
<li>개인정보의_기술적_관리적_보호조치_기준(제2020-5호)_해설서(2020.12월) <strong>(클릭 시 PDF 다운로드)</strong><blockquote>
<p><a href="https://www.privacy.go.kr/cmm/fms/FileDown.do?atchFileId=FILE_000000000841151&amp;fileSn=0">https://www.privacy.go.kr/cmm/fms/FileDown.do?atchFileId=FILE_000000000841151&amp;fileSn=0</a></p>
</blockquote>
</li>
</ul>
<p>정리하면 다음과 같다.</p>
<ol>
<li>영문, 숫자, 특수문자 중 2종류 이상 조합 최소 10자리</li>
<li>영문, 숫자, 특수문자 중 3종류 이상 조합 최소 8자리</li>
<li>연속적인 숫자 불가</li>
</ol>
<p>이 규칙을 참고해서 영문, 숫자, 특수문자 중 3종류 이상 조합 최소 8자리 rules를 지정한다.</p>
<pre><code class="language-vue">                &lt;v-text-field
                    v-model=&quot;password&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[rules.required, rules.passowrd.expertTest, rules.passowrd.lengthCheck]&quot;
                    clearable
                    label=&quot;Password&quot;
                    placeholder=&quot;Enter your password&quot;
                &gt;&lt;/v-text-field&gt;</code></pre>
<pre><code class="language-javascript">            passowrd: {
                expertTest: v =&gt; {
                    const passwordExpert = /^(?=(?:[^a-zA-Z]*[a-zA-Z]))(?=(?:\D*\d))(?=(?:[^\W_]*[\W_])).{8,}$/
                    return !!passwordExpert.test(v) || &#39;It must be at least 8 characters and a combination of 3 or more letters, numbers, and special characters.&#39;
                },
                lengthCheck: v =&gt; {
                    return v.length &lt; 50 || &#39;The password you entered is too long.&#39;
                }
            },</code></pre>
<p>정규식을 사용해서 8자리 이상, 영문, 숫자, 특수문자 중 3종류 이상 조합 되도록 규칙을 구성했다. 그리고 너무 긴 문자열 입력을 방지하기 위해 lengthCheck를 적용했다.(email 부분에도 적용했다.)</p>
<h4 id="password-verify">Password Verify</h4>
<p>Script</p>
<pre><code class="language-javascript">            passwordVerify: {
                passwordCheck: v =&gt; {
                    return this.passwordMatchCheck(v) || &#39;It does not match your password.&#39;
                }
            },
...

        passwordMatchCheck (passwordVerify) {
            return (this.password === passwordVerify)
        },</code></pre>
<p>Text-field</p>
<pre><code class="language-vue">                &lt;v-text-field
                    v-model=&quot;passwordVerify&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[rules.required, rules.passwordVerify.passwordCheck]&quot;
                    clearable
                    label=&quot;Password&quot;
                    placeholder=&quot;Enter your password&quot;
                &gt;&lt;/v-text-field&gt;</code></pre>
<p>arrow function에서 this는 사용이 안되는 것을 알고 있었는데 email 형식검사에서 에러 발생 안되서 의아해 했는데 확인해보니 rules로 지정한 것과 실제 지정된 rules의 이름이 달라서 매칭이 안되고 있었던 것...
Passowrd Verify 위 코드 적용시 Cannot read property of undefined가 발생했다.</p>
<p>arrow function에서는 this가 안먹히므로 arrow function 형태를 바꿔준다. </p>
<p>...</p>
<p>단순히 arrow function을 사용하지 않고 this.~ 형태로 적용하면 된다고 생각했지만... undefined 에러가 계속 발생한다... 여러 다양한 시행착오 끝에 최종적으로 rules에 원하는 methods를 바로 지정하면 되는 것을 알게됬다.</p>
<pre><code>:rules=&quot;[rules.required, rules.email.expertTest, emailDuplicateCheck, rules.email.lengthCheck]&quot;</code></pre><p>emailDuplicateCheck라는 methods를 만들고 rules에 바로 지정하면 된다.</p>
<pre><code class="language-javascript">    emailDuplicateCheck (email) {
            console.log(email)
            return true;
        },</code></pre>
<p>Password Verify도 Script에 methods를 추가하고</p>
<pre><code class="language-javascript">        passwordMatchCheck (passwordVerify) {
            return (this.password === passwordVerify) || &#39;It does not match your password.&#39;
        },</code></pre>
<p>rules 부분에 생성한 method를 넣어준다.</p>
<pre><code class="language-vue">                &lt;v-text-field
                    v-model=&quot;passwordVerify&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[rules.required, passwordMatchCheck]&quot;
                    clearable
                    label=&quot;Password&quot;
                    placeholder=&quot;Enter your password&quot;
                &gt;&lt;/v-text-field&gt;</code></pre>
<p>테스트 결과 전부 정상적으로 처리되는것을 확인했다.</p>
<h4 id="password-타입-지정">password 타입 지정</h4>
<p>password 타입으로 지정해서 노출되는것을 방지한다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/comet_strike/post/073f620a-1e9c-4f66-bec0-e9ea84e2b470/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/comet_strike/post/8b3bbd5c-11b3-4673-92f2-b2e1f3dcd544/image.png" alt=""></th>
</tr>
</thead>
</table>
<pre><code>                 &lt;v-text-field
                    v-model=&quot;password&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[rules.required, rules.passowrd.expertTest, rules.passowrd.lengthCheck]&quot;
                    type=&quot;password&quot;
                    clearable
                    label=&quot;Password&quot;
                    placeholder=&quot;Enter your password&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;v-text-field
                    v-model=&quot;passwordVerify&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[rules.required, passwordMatchCheck]&quot;
                    type=&quot;password&quot;
                    clearable
                    label=&quot;Password&quot;
                    placeholder=&quot;Enter your password&quot;
                &gt;&lt;/v-text-field&gt;
</code></pre><h4 id="form-활성화">Form 활성화</h4>
<p>양식이 모두 활성화가 되었다면 Form 상태를 변경해서 SIGN UP 버튼이 활성화 되도록 변경한다.</p>
<p>v-form은 다음과 같이 v-model이 &quot;form&quot;으로 바인딩 되어 있다. 이는 rules와 연동되어 모든 조건을 만족하게 된다면 form 값도 바뀌게 된다. 따라서 별도의 코드 수정은 필요 없다.</p>
<pre><code>              &lt;v-form
            v-model=&quot;form&quot;
            @submit.prevent=&quot;onSubmit&quot;
            &gt;</code></pre><ul>
<li>[VueJS] Vuetify v-text-field 유효성 검사 (rules)<blockquote>
<p><a href="https://minu0807.tistory.com/82">https://minu0807.tistory.com/82</a></p>
</blockquote>
</li>
</ul>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/comet_strike/post/c5ef42c7-ef47-4ce6-802c-7280d409dbc5/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/comet_strike/post/5e633654-b5de-47dc-a4e0-e6ef0ed44d3f/image.png" alt=""></th>
</tr>
</thead>
</table>
<h3 id="3-client에서-server-정보-전송">3. Client에서 Server 정보 전송</h3>
<p>Fetch API와 Axios를 통해 HTTP 요청을 보낼 수 있다. 두 개를 비교하는데 상당히 길어질거 같기도 하므로 다음 글에서 작성!</p>
<h2 id="전체-코드">전체 코드</h2>
<h3 id="1-email-password-password-verify-형식-검사-적용-코드">1. Email, Password, Password Verify 형식 검사 적용 코드</h3>
<p>SignUpPage.vue</p>
<pre><code class="language-vue">&lt;template&gt;
    &lt;v-sheet class=&quot;pa-12&quot; rounded&gt;
        &lt;v-card class=&quot;mx-auto px-6 py-8&quot; max-width=&quot;60%&quot; :elevation=&quot;12&quot;&gt;
            &lt;v-form
            v-model=&quot;form&quot;
            @submit.prevent=&quot;onSubmit&quot;
            &gt;
                &lt;v-text-field
                    v-model=&quot;email&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[rules.required, rules.email.expertTest, emailDuplicateCheck, rules.email.lengthCheck]&quot;
                    class=&quot;mb-2&quot;
                    clearable
                    label=&quot;Email&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;v-text-field
                    v-model=&quot;password&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[rules.required, rules.passowrd.expertTest, rules.passowrd.lengthCheck]&quot;
                    type=&quot;password&quot;
                    clearable
                    label=&quot;Password&quot;
                    placeholder=&quot;Enter your password&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;v-text-field
                    v-model=&quot;passwordVerify&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[rules.required, passwordMatchCheck]&quot;
                    type=&quot;password&quot;
                    clearable
                    label=&quot;Password&quot;
                    placeholder=&quot;Enter your password&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;br&gt;

                &lt;v-btn
                    :disabled=&quot;!form&quot;
                    :loading=&quot;loading&quot;
                    block
                    color=&quot;primary&quot;
                    size=&quot;large&quot;
                    type=&quot;submit&quot;
                    variant=&quot;elevated&quot;
                &gt;
                    Sign Up
                &lt;/v-btn&gt;
            &lt;/v-form&gt;
        &lt;/v-card&gt;
    &lt;/v-sheet&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
    data: () =&gt; ({
        form: false,
        email: null,
        password: null,
        passwordVerify: null,
        loading: false,
        rules: {
            email: {
                expertTest: v =&gt; {
                    const emailExpert = /^[A-Za-z0-9_\\.\\-]+@[A-Za-z0-9\\-]+\.[A-Za-z0-9\\-]+/
                    return !!emailExpert.test(v) || &#39;It must be in email format.&#39;
                },
                lengthCheck: v =&gt; {
                    return v.length &lt; 50 || &#39;The email you entered is too long.&#39;
                }
            },
            passowrd: {
                expertTest: v =&gt; {
                    const passwordExpert = /^(?=(?:[^a-zA-Z]*[a-zA-Z]))(?=(?:\D*\d))(?=(?:[^\W_]*[\W_])).{8,}$/
                    return !!passwordExpert.test(v) || &#39;It must be at least 8 characters and a combination of 3 or more letters, numbers, and special characters.&#39;
                },
                lengthCheck: v =&gt; {
                    return v.length &lt; 50 || &#39;The password you entered is too long.&#39;
                }
            },
            required: v =&gt; !!v || &#39;Field is required&#39;
        },
    }),

    methods: {
        onSubmit () {
            if (!this.form) return

            this.loading = true

            setTimeout(() =&gt; (this.loading = false), 2000)
        },
        emailDuplicateCheck (email) {
            console.log(email)
            // duplicateCheck: v =&gt; this.emailDuplicateCheck(v) || &#39;This is a duplicate email. Please use a different email.&#39;,
            return true;
            // true가 아닐 경우 &#39;This is a duplicate email. Please use a different email.&#39;,
        },
        passwordMatchCheck (passwordVerify) {
            return (this.password === passwordVerify) || &#39;It does not match your password.&#39;
        },

    },
}
&lt;/script&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[8. 로그인 - 전송 시 암호화?]]></title>
            <link>https://velog.io/@comet_strike/8.-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%A0%84%EC%86%A1-%EC%8B%9C-%EC%95%94%ED%98%B8%ED%99%94</link>
            <guid>https://velog.io/@comet_strike/8.-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%A0%84%EC%86%A1-%EC%8B%9C-%EC%95%94%ED%98%B8%ED%99%94</guid>
            <pubDate>Sun, 08 Oct 2023 10:35:48 GMT</pubDate>
            <description><![CDATA[<p>어떤 로그인을 방식을 진행하던 아이디와 비밀번호를 사용하는 로그인 방식이라면. 웹 페이지의 브라우저 (Client)에서 Server로 아이디와 비밀번호 데이터를 전송을 해야한다.</p>
<h3 id="아이디의-경우에는-평문-전송이-된다고-하더라도-비밀번호의-경우-평문-전송을-사용해도-될까">아이디의 경우에는 평문 전송이 된다고 하더라도 비밀번호의 경우 평문 전송을 사용해도 될까?</h3>
<p>비밀번호 전송의 경우 HTTPS와 POST 방식으로 비밀번호를 전송하는 것이 표준 관행이다. Clinet - Server간의 통신은 TLS에 따라 암호화되므로 HTTPS는 비밀번호를 보호 하게 된다.</p>
<p>비밀번호에 대해 Client Side에서 해싱을 구현할 수 있다. 하지만 네트워크 도청자가 사용자의 비밀번호(일반 비밀번호 또는 해싱 비밀번호)를 볼 수 있다면 별 차이가 없다.</p>
<ul>
<li>Is &#39;plain text&#39; password safe to send over network? - LinkedIn<blockquote>
<p><a href="https://www.linkedin.com/pulse/plain-text-password-safe-send-over-network-manish-dhar-dwivedi">https://www.linkedin.com/pulse/plain-text-password-safe-send-over-network-manish-dhar-dwivedi</a></p>
</blockquote>
</li>
</ul>
<h3 id="https--tls">HTTPS / TLS</h3>
<p>HTTPS는 암호화 프로토콜을 사용하여 통신을 암호화 합니다. 이 프로토콜은 전송 계층 보안(TLS)이라고 불립니다. 이 프로토콜은 비대칭 공개 키를 사용하여 통신을 보호합니다. </p>
<h4 id="tls">TLS</h4>
<p>Transport Layer Security, 인터넷 상의 통신을 위한 개인 정보와 데이터 보안을 용이하게 하기 위한 프로토콜. 웹 응용 프로그램과 서버 간의 커뮤니케이션을 암호화 하는데 주로 사용된다.</p>
<ul>
<li>HTTPS란 무엇입니까? - cloudflare<blockquote>
<p><a href="https://www.cloudflare.com/ko-kr/learning/ssl/what-is-https/">https://www.cloudflare.com/ko-kr/learning/ssl/what-is-https/</a></p>
</blockquote>
</li>
<li>TLS(Transport Layer Security)는 무엇입니까? - cloudflare<blockquote>
<p><a href="https://www.cloudflare.com/ko-kr/learning/ssl/transport-layer-security-tls/">https://www.cloudflare.com/ko-kr/learning/ssl/transport-layer-security-tls/</a></p>
</blockquote>
</li>
<li>TLS 암호화란 무엇이며 어떻게 작동하나요?<blockquote>
<p><a href="https://powerdmarc.com/ko/what-is-tls-encryption/">https://powerdmarc.com/ko/what-is-tls-encryption/</a></p>
</blockquote>
</li>
</ul>
<p>따라서, 로그인 정보 전송시 HTTPS와 POST 방식을 이용하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[7. 로그인 화면 구성]]></title>
            <link>https://velog.io/@comet_strike/7.-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%99%94%EB%A9%B4-%EA%B5%AC%EC%84%B1</link>
            <guid>https://velog.io/@comet_strike/7.-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%99%94%EB%A9%B4-%EA%B5%AC%EC%84%B1</guid>
            <pubDate>Mon, 02 Oct 2023 07:40:10 GMT</pubDate>
            <description><![CDATA[<p>프로젝트의 뼈대를 만드는 작업 중이므로 HTTP에 맞는 Stateless 구현하기 위해 JWT 방식과 DB Resource 측면 및 서버 확장 가능성 등을 고려해 JWT, OAuth 로그인 방식을 구현하기로 결정 추후 필요시 Session을 추가로 구현하는 방식을 고려하며 로그인 기능을 구현할 예정이다.</p>
<p>화면 디자인의 경우에는 모바일과 웹에서 사용자 경험을 높일 수 있도록 반응형 웹앱 형태로 변경할 예정이다.
(HelloWorlde.vue 컴포넌트 이름도 변경할 예정, Vue 파일 관리 방식도 확인 필요)</p>
<ul>
<li>a-hands-on-guide-to-mobile-first-design<blockquote>
<p><a href="https://www.uxpin.com/studio/blog/a-hands-on-guide-to-mobile-first-design/">https://www.uxpin.com/studio/blog/a-hands-on-guide-to-mobile-first-design/</a></p>
</blockquote>
</li>
</ul>
<p>우선 로그인 및 회원 가입 기능을 구현하고 메인 페이지에 반영하도록 적용한 후에 디자인은 추후 적용할 예정이다.</p>
<h2 id="작업-branch-생성">작업 Branch 생성</h2>
<pre><code class="language-bash">git branch login_signup_screen feature</code></pre>
<h1 id="로그인-화면-구성">로그인 화면 구성</h1>
<p>가장 기본적으로 로그인 화면을 생성하고 해당 화면에서 ID, PW를 입력받도록 화면을 구성한다.</p>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/647add65-879a-4a57-995f-e5e6cee952a9/image.png" alt=""></p>
<p>로그인 할 수 있도록 오른쪽 상단에 위치한 Toggle order 옆에 로그인 버튼을 생성하고 로그인 화면으로 갈 수 있도록 만든다.</p>
<p>로그인 아이콘은 뷰티파이 아이콘을 사용한다. </p>
<ul>
<li><p>Where can I find a list of icons to be used in vuetify? [closed]</p>
<blockquote>
<p><a href="https://stackoverflow.com/questions/52400086/where-can-i-find-a-list-of-icons-to-be-used-in-vuetify">https://stackoverflow.com/questions/52400086/where-can-i-find-a-list-of-icons-to-be-used-in-vuetify</a></p>
</blockquote>
</li>
<li><p>Material Design Icons</p>
<blockquote>
<p><a href="https://pictogrammers.github.io/@mdi/font/3.6.95/">https://pictogrammers.github.io/@mdi/font/3.6.95/</a></p>
</blockquote>
</li>
</ul>
<p>mdi-login / mdi-login-variant 로고를 임시로 사용한다.
<img src="https://velog.velcdn.com/images/comet_strike/post/e9f26507-6aa0-4020-b676-46b9c53ddd65/image.png" alt=""> </p>
<h3 id="뷰티파이-아이콘-적용">뷰티파이 아이콘 적용</h3>
<ul>
<li><p>뷰티파이 아이콘 적용 예제</p>
<blockquote>
<p><a href="https://v2.vuetifyjs.com/en/components/icons/#examples">https://v2.vuetifyjs.com/en/components/icons/#examples</a></p>
</blockquote>
</li>
<li><p>Activator 적용 코드 (v-app-bar) 아래에 v-btn icon을 추가해준다. </p>
</li>
</ul>
<pre><code class="language-vue">      &lt;v-btn icon&gt;
        &lt;v-icon&gt;mdi-login&lt;/v-icon&gt;
      &lt;/v-btn&gt;</code></pre>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/comet_strike/post/99fbab91-e7b9-4d5d-b6d5-6cd80b165b9a/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/comet_strike/post/4d737a1e-ec9f-4e63-a1cd-c869a49bfba8/image.png" alt=""></th>
</tr>
</thead>
</table>
<p>아이콘이 적용되고 클릭도 되는 것을 확인할 수 있다. (코드는 전체 코드 참고)</p>
<h3 id="버튼-클릭-후-로그인-페이지로-이동">버튼 클릭 후 로그인 페이지로 이동</h3>
<h4 id="1-로그인-화면-생성">1. 로그인 화면 생성</h4>
<ul>
<li>components 밑 LoginPage.vue 생성 / 로그인 페이지를 확인할 수 있도록 Login Page 문구 출력하도록 코드 구성
<img src="https://velog.velcdn.com/images/comet_strike/post/9b13b519-d609-4f91-baf4-afb3077d070e/image.png" alt=""></li>
</ul>
<pre><code class="language-vue">&lt;template&gt;
    &lt;div&gt;
        Login Page
    &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;

&lt;/script&gt;</code></pre>
<h4 id="2-router-path-생성">2. Router Path 생성</h4>
<ul>
<li>router 밑 index.js에 login에 해당하는 경로 설정
LoginPage import / path, name, component 생성</li>
</ul>
<pre><code class="language-javascript">import { createRouter, createWebHistory } from &#39;vue-router&#39;
import HomeView from &#39;../views/HomeView.vue&#39;
import LoginPage from &#39;../components/LoginPage.vue&#39;

const routes = [
  {
    path: &#39;/&#39;,
    name: &#39;home&#39;,
    component: HomeView
  },
  {
    path: &#39;/about&#39;,
    name: &#39;about&#39;,
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: &quot;about&quot; */ &#39;../views/AboutView.vue&#39;)
  },
  {
    path: &#39;/login&#39;,
    name: &#39;login&#39;,
    component: LoginPage
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router
</code></pre>
<h4 id="3-버튼-클릭-시-page-이동">3. 버튼 클릭 시 Page 이동</h4>
<ul>
<li>router-link 또는 @click에 함수를 적용해 구현할 수 있지만 $router.push를 이용해 로그인 페이지로 이동하게 만들었다.</li>
</ul>
<pre><code class="language-vue">      &lt;v-btn icon @click=&quot;$router.push(&#39;/login&#39;)&quot;&gt;
        &lt;v-icon&gt;mdi-login&lt;/v-icon&gt;
      &lt;/v-btn&gt;</code></pre>
<p>클릭 시 로그인 페이지(LoginPage.vue)로 이동하는 것을 확인했다.
<img src="https://velog.velcdn.com/images/comet_strike/post/d93a80ce-db4d-4b24-9a09-bd8e6d627729/image.png" alt=""></p>
<h3 id="로그인-페이지-화면-구성">로그인 페이지 화면 구성</h3>
<p>뷰티파이 text-fileds 이용한다.</p>
<ul>
<li><p>text-fileds 참고</p>
<blockquote>
<p><a href="https://vuetifyjs.com/en/components/text-fields/">https://vuetifyjs.com/en/components/text-fields/</a></p>
</blockquote>
</li>
<li><p>elevation 참고</p>
<blockquote>
<p><a href="https://vuetifyjs.com/en/components/sheets/#color">https://vuetifyjs.com/en/components/sheets/#color</a></p>
</blockquote>
</li>
</ul>
<p>LoginPage.vue 코드</p>
<pre><code class="language-vue">&lt;template&gt;
    &lt;v-sheet class=&quot;pa-12&quot; rounded&gt;
        &lt;v-card class=&quot;mx-auto px-6 py-8&quot; max-width=&quot;60%&quot; :elevation=&quot;12&quot;&gt;
            &lt;v-form
            v-model=&quot;form&quot;
            @submit.prevent=&quot;onSubmit&quot;
            &gt;
                &lt;v-text-field
                    v-model=&quot;email&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[required]&quot;
                    class=&quot;mb-2&quot;
                    clearable
                    label=&quot;Email&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;v-text-field
                    v-model=&quot;password&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[required]&quot;
                    clearable
                    label=&quot;Password&quot;
                    placeholder=&quot;Enter your password&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;br&gt;

                &lt;v-btn
                    :disabled=&quot;!form&quot;
                    :loading=&quot;loading&quot;
                    block
                    color=&quot;success&quot;
                    size=&quot;large&quot;
                    type=&quot;submit&quot;
                    variant=&quot;elevated&quot;
                &gt;
                    Sign In
                &lt;/v-btn&gt;

                &lt;br&gt;

                &lt;v-btn
                    :disabled=&quot;!form&quot;
                    :loading=&quot;loading&quot;
                    block
                    color=&quot;primary&quot;
                    size=&quot;large&quot;
                    type=&quot;submit&quot;
                    variant=&quot;elevated&quot;
                &gt;
                    Sign Up
                &lt;/v-btn&gt;
            &lt;/v-form&gt;
        &lt;/v-card&gt;
    &lt;/v-sheet&gt;
&lt;/template&gt;

&lt;script&gt;

&lt;/script&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/0c61f669-b320-4b47-b86b-835ec4b07fd2/image.png" alt=""></p>
<h3 id="회원-가입-페이지-화면-구성">회원 가입 페이지 화면 구성</h3>
<ul>
<li>components 밑 SignUpPage.vue 생성, 로그인 페이지에서 SIGN IN 버튼만 제거한 후 생성</li>
</ul>
<p>SignUpPage.vue 코드</p>
<pre><code class="language-vue">&lt;template&gt;
    &lt;v-sheet class=&quot;pa-12&quot; rounded&gt;
        &lt;v-card class=&quot;mx-auto px-6 py-8&quot; max-width=&quot;60%&quot; :elevation=&quot;12&quot;&gt;
            &lt;v-form
            v-model=&quot;form&quot;
            @submit.prevent=&quot;onSubmit&quot;
            &gt;
                &lt;v-text-field
                    v-model=&quot;email&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[required]&quot;
                    class=&quot;mb-2&quot;
                    clearable
                    label=&quot;Email&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;v-text-field
                    v-model=&quot;password&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[required]&quot;
                    clearable
                    label=&quot;Password&quot;
                    placeholder=&quot;Enter your password&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;br&gt;

                &lt;v-btn
                    :disabled=&quot;!form&quot;
                    :loading=&quot;loading&quot;
                    block
                    color=&quot;primary&quot;
                    size=&quot;large&quot;
                    type=&quot;submit&quot;
                    variant=&quot;elevated&quot;
                &gt;
                    Sign Up
                &lt;/v-btn&gt;
            &lt;/v-form&gt;
        &lt;/v-card&gt;
    &lt;/v-sheet&gt;
&lt;/template&gt;

&lt;script&gt;

&lt;/script&gt;</code></pre>
<p>LoginPage.vue에서 Sign Up 버튼을 눌렀을 경우 해당 페이지로 이동할 수 있도록 index.js에서 route 설정 및 버튼 이벤트 추가</p>
<p>index.js 코드</p>
<pre><code class="language-javascript">...
import LoginPage from &#39;../components/LoginPage.vue&#39;
import SignUpPage from &#39;../components/SignUpPage.vue&#39;

const routes = [
  ...
  {
    path: &#39;/login&#39;,
    name: &#39;login&#39;,
    component: LoginPage
  },
  {
    path: &#39;/signUp&#39;,
    name: &#39;signUp&#39;,
    component: SignUpPage
  }
]
</code></pre>
<p>LoginPage.vue 코드</p>
<pre><code class="language-vue">...
                &lt;v-btn
                    :disabled=&quot;!form&quot;
                    :loading=&quot;loading&quot;
                    block
                    color=&quot;success&quot;
                    size=&quot;large&quot;
                    type=&quot;submit&quot;
                    variant=&quot;elevated&quot;
                &gt;
                    Sign In
                &lt;/v-btn&gt;

                &lt;br&gt;

                &lt;v-btn
                    :disabled=&quot;!form&quot;
                    :loading=&quot;loading&quot;
                    block
                    color=&quot;primary&quot;
                    size=&quot;large&quot;
                    type=&quot;submit&quot;
                    variant=&quot;elevated&quot;
                    @click=&quot;$router.push(&#39;/signUp&#39;)&quot;
                &gt;
                    Sign Up
                &lt;/v-btn&gt;
...</code></pre>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/5639a1db-dff4-4467-bc2a-91a2bbae8896/image.png" alt=""></p>
<p>로그인 페이지, 회원 가입 페이지 화면 구성 완료.
단순히 화면만 구성 완료된 것으로 Script를 통해 백엔드 통신 및 Password가 노출되지 않도록 type 지정 추가 필요.</p>
<p>LoginPage와 SignUpPage의 버튼에서 form을 검사하는 구문이 있다.
LoginPage에서 Sign Up의 경우에는 양식 검사 없이 바로 접근할 수 있어야 하므로 코드를 삭제한다.</p>
<pre><code class="language-vue">:disabled=&quot;!form&quot;</code></pre>
<p>기능 구현이 완료되면 UX를 고려한 디자인도 적용할 예정...!</p>
<ul>
<li>UX 참고 자료들<blockquote>
<p><a href="https://dribbble.com/shots/3902549-LOGIN-Animation-UX-Motion-Design">https://dribbble.com/shots/3902549-LOGIN-Animation-UX-Motion-Design</a>
<a href="https://taegon.kim/archives/9658">https://taegon.kim/archives/9658</a>
<a href="https://github.com/cgoldsby/LoginCritter">https://github.com/cgoldsby/LoginCritter</a>
<a href="https://www.tunnelbear.com/account/login">https://www.tunnelbear.com/account/login</a></p>
</blockquote>
</li>
</ul>
<h3 id="git-push">Git push</h3>
<p>git add / commit / push</p>
<pre><code class="language-bash">git add .

git commit -m &quot;HelloWorlde.vue : 로그인 아이콘 생성,
LoginPage.vue : 로그인 페이지 화면 생성,
SignUpPage.vue : 회원가입 페이지 화면 생성,
index.js : login, signUp 경로 생성&quot;

git push origin login_signup_screen</code></pre>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/13b36b2d-26b8-4b55-b8fb-c882414a0ee6/image.png" alt=""></p>
<p>브랜치 정상반영 확인, Compare &amp; pull request</p>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/d825283a-7856-47a6-93ca-3fa32074aa36/image.png" alt=""></p>
<p>pull request &amp; Merge 이후 delete branch
<img src="https://velog.velcdn.com/images/comet_strike/post/4494cbb2-123b-4b40-8c1a-bcb17a0eba78/image.png" alt=""></p>
<ul>
<li>Merged 로그인, 회원가입 화면 구성 #2<blockquote>
<p><a href="https://github.com/Fortuna3Co/Toy-Project/pull/2">https://github.com/Fortuna3Co/Toy-Project/pull/2</a></p>
</blockquote>
</li>
</ul>
<h2 id="전체-코드">전체 코드</h2>
<h3 id="1-아이콘-적용-후-전체-코드">1. 아이콘 적용 후 전체 코드</h3>
<ul>
<li><p>HelloWorld.vue</p>
<pre><code class="language-vue">&lt;template&gt;
&lt;v-app id=&quot;inspire&quot;&gt;
  &lt;v-navigation-drawer v-model=&quot;drawer&quot;&gt;
    &lt;v-sheet
      color=&quot;grey-lighten-4&quot;
      class=&quot;pa-4&quot;
    &gt;
      &lt;v-avatar
        class=&quot;mb-4&quot;
        color=&quot;grey-darken-1&quot;
        size=&quot;64&quot;
      &gt;&lt;/v-avatar&gt;
      &lt;div&gt;john@google.com&lt;/div&gt;
    &lt;/v-sheet&gt;

    &lt;v-divider&gt;&lt;/v-divider&gt;

    &lt;v-list&gt;
      &lt;v-list-item
        v-for=&quot;[icon, text] in links&quot;
        :key=&quot;icon&quot;
        link
      &gt;
        &lt;template v-slot:prepend&gt;
          &lt;v-icon&gt;{{ icon }}&lt;/v-icon&gt;
        &lt;/template&gt;

        &lt;v-list-item-title&gt;{{ text }}&lt;/v-list-item-title&gt;
      &lt;/v-list-item&gt;
    &lt;/v-list&gt;
  &lt;/v-navigation-drawer&gt;

  &lt;v-app-bar
    :order=&quot;order&quot;
    flat
    title=&quot;Application bar&quot;
  &gt;

    &lt;template v-slot:append&gt;
      &lt;v-switch
        v-model=&quot;order&quot;
        hide-details
        inset
        label=&quot;Toggle order&quot;
        true-value=&quot;-1&quot;
        false-value=&quot;0&quot;
      &gt;&lt;/v-switch&gt;
    &lt;/template&gt;

    &lt;!-- Activator Slot Start --&gt;
    &lt;v-menu&gt;
      &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
        &lt;v-btn
          color=&quot;primary&quot;
          v-bind=&quot;props&quot;
        &gt;
          Activator slot
        &lt;/v-btn&gt;
      &lt;/template&gt;
      &lt;v-list&gt;
        &lt;v-list-item
          v-for=&quot;(item, index) in items&quot;
          :key=&quot;index&quot;
          :value=&quot;index&quot;
        &gt;
          &lt;v-list-item-title&gt;{{ item.title }}&lt;/v-list-item-title&gt;
        &lt;/v-list-item&gt;
      &lt;/v-list&gt;
    &lt;/v-menu&gt;
    &lt;!-- Activator Slot End --&gt;

    &lt;!-- Activator2 Slot Start --&gt;
    &lt;v-menu&gt;
      &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
        &lt;v-btn
          color=&quot;primary&quot;
          v-bind=&quot;props&quot;
        &gt;
          Activator2 slot
        &lt;/v-btn&gt;
      &lt;/template&gt;
      &lt;v-list&gt;
        &lt;v-list-item
          v-for=&quot;(item, index) in items2&quot;
          :key=&quot;index&quot;
          :value=&quot;index&quot;
        &gt;
          &lt;v-list-item-title&gt;{{ item.title }}&lt;/v-list-item-title&gt;
        &lt;/v-list-item&gt;
      &lt;/v-list&gt;
    &lt;/v-menu&gt;
    &lt;!-- Activator2 Slot End --&gt;

</code></pre>
</li>
</ul>
<pre><code>  &lt;v-btn icon&gt;
    &lt;v-icon&gt;mdi-login&lt;/v-icon&gt;
  &lt;/v-btn&gt;

&lt;/v-app-bar&gt;

&lt;v-main class=&quot;bg-grey-lighten-2&quot;&gt;
  &lt;v-container&gt;
    &lt;v-row&gt;
      &lt;template v-for=&quot;n in 4&quot; :key=&quot;n&quot;&gt;
        &lt;v-col
          class=&quot;mt-2&quot;
          cols=&quot;12&quot;
        &gt;
          &lt;strong&gt;Category {{ n }}&lt;/strong&gt;
        &lt;/v-col&gt;

        &lt;v-col
          v-for=&quot;j in 6&quot;
          :key=&quot;`${n}${j}`&quot;
          cols=&quot;6&quot;
          md=&quot;2&quot;
        &gt;
          &lt;v-sheet height=&quot;150&quot;&gt;&lt;/v-sheet&gt;
        &lt;/v-col&gt;
      &lt;/template&gt;
    &lt;/v-row&gt;
  &lt;/v-container&gt;
&lt;/v-main&gt;</code></pre>  </v-app>

</template>

<script>

export default {
  name: 'HelloWorld',

  data: () => ({
    order: 0,
    drawer: null,
    items: [
        { title: 'Item1 Click Me 1' },
        { title: 'Item1 Click Me 2' },
        { title: 'Item1 Click Me 3' },
        { title: 'Item1 Click Me 4' },
      ],
    items2: [
        { title: 'Item2 Click Me 1' },
        { title: 'Item2 Click Me 2' },
        { title: 'Item2 Click Me 3' },
        { title: 'Item2 Click Me 4' },
      ],
      links: [
        ['mdi-inbox-arrow-down', 'Inbox'],
        ['mdi-send', 'Send'],
        ['mdi-delete', 'Trash'],
        ['mdi-alert-octagon', 'Spam'],
      ],
  }),
}
</script>
<pre><code>
### 2. 로그인 페이지 이동 후 전체 코드
* HelloWorld.vue
``` vue
&lt;template&gt;
  &lt;v-app id=&quot;inspire&quot;&gt;
    &lt;v-navigation-drawer v-model=&quot;drawer&quot;&gt;
      &lt;v-sheet
        color=&quot;grey-lighten-4&quot;
        class=&quot;pa-4&quot;
      &gt;
        &lt;v-avatar
          class=&quot;mb-4&quot;
          color=&quot;grey-darken-1&quot;
          size=&quot;64&quot;
        &gt;&lt;/v-avatar&gt;
        &lt;div&gt;john@google.com&lt;/div&gt;
      &lt;/v-sheet&gt;

      &lt;v-divider&gt;&lt;/v-divider&gt;

      &lt;v-list&gt;
        &lt;v-list-item
          v-for=&quot;[icon, text] in links&quot;
          :key=&quot;icon&quot;
          link
        &gt;
          &lt;template v-slot:prepend&gt;
            &lt;v-icon&gt;{{ icon }}&lt;/v-icon&gt;
          &lt;/template&gt;

          &lt;v-list-item-title&gt;{{ text }}&lt;/v-list-item-title&gt;
        &lt;/v-list-item&gt;
      &lt;/v-list&gt;
    &lt;/v-navigation-drawer&gt;

    &lt;v-app-bar
      :order=&quot;order&quot;
      flat
      title=&quot;Application bar&quot;
    &gt;

      &lt;template v-slot:append&gt;
        &lt;v-switch
          v-model=&quot;order&quot;
          hide-details
          inset
          label=&quot;Toggle order&quot;
          true-value=&quot;-1&quot;
          false-value=&quot;0&quot;
        &gt;&lt;/v-switch&gt;
      &lt;/template&gt;

      &lt;!-- Activator Slot Start --&gt;
      &lt;v-menu&gt;
        &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
          &lt;v-btn
            color=&quot;primary&quot;
            v-bind=&quot;props&quot;
          &gt;
            Activator slot
          &lt;/v-btn&gt;
        &lt;/template&gt;
        &lt;v-list&gt;
          &lt;v-list-item
            v-for=&quot;(item, index) in items&quot;
            :key=&quot;index&quot;
            :value=&quot;index&quot;
          &gt;
            &lt;v-list-item-title&gt;{{ item.title }}&lt;/v-list-item-title&gt;
          &lt;/v-list-item&gt;
        &lt;/v-list&gt;
      &lt;/v-menu&gt;
      &lt;!-- Activator Slot End --&gt;

      &lt;!-- Activator2 Slot Start --&gt;
      &lt;v-menu&gt;
        &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
          &lt;v-btn
            color=&quot;primary&quot;
            v-bind=&quot;props&quot;
          &gt;
            Activator2 slot
          &lt;/v-btn&gt;
        &lt;/template&gt;
        &lt;v-list&gt;
          &lt;v-list-item
            v-for=&quot;(item, index) in items2&quot;
            :key=&quot;index&quot;
            :value=&quot;index&quot;
          &gt;
            &lt;v-list-item-title&gt;{{ item.title }}&lt;/v-list-item-title&gt;
          &lt;/v-list-item&gt;
        &lt;/v-list&gt;
      &lt;/v-menu&gt;
      &lt;!-- Activator2 Slot End --&gt;


      &lt;v-btn icon @click=&quot;$router.push(&#39;/login&#39;)&quot;&gt;
        &lt;v-icon&gt;mdi-login&lt;/v-icon&gt;
      &lt;/v-btn&gt;

    &lt;/v-app-bar&gt;

    &lt;v-main class=&quot;bg-grey-lighten-2&quot;&gt;
      &lt;v-container&gt;
        &lt;v-row&gt;
          &lt;template v-for=&quot;n in 4&quot; :key=&quot;n&quot;&gt;
            &lt;v-col
              class=&quot;mt-2&quot;
              cols=&quot;12&quot;
            &gt;
              &lt;strong&gt;Category {{ n }}&lt;/strong&gt;
            &lt;/v-col&gt;

            &lt;v-col
              v-for=&quot;j in 6&quot;
              :key=&quot;`${n}${j}`&quot;
              cols=&quot;6&quot;
              md=&quot;2&quot;
            &gt;
              &lt;v-sheet height=&quot;150&quot;&gt;&lt;/v-sheet&gt;
            &lt;/v-col&gt;
          &lt;/template&gt;
        &lt;/v-row&gt;
      &lt;/v-container&gt;
    &lt;/v-main&gt;
  &lt;/v-app&gt;

&lt;/template&gt;

&lt;script&gt;

export default {
  name: &#39;HelloWorld&#39;,

  data: () =&gt; ({
    order: 0,
    drawer: null,
    items: [
        { title: &#39;Item1 Click Me 1&#39; },
        { title: &#39;Item1 Click Me 2&#39; },
        { title: &#39;Item1 Click Me 3&#39; },
        { title: &#39;Item1 Click Me 4&#39; },
      ],
    items2: [
        { title: &#39;Item2 Click Me 1&#39; },
        { title: &#39;Item2 Click Me 2&#39; },
        { title: &#39;Item2 Click Me 3&#39; },
        { title: &#39;Item2 Click Me 4&#39; },
      ],
      links: [
        [&#39;mdi-inbox-arrow-down&#39;, &#39;Inbox&#39;],
        [&#39;mdi-send&#39;, &#39;Send&#39;],
        [&#39;mdi-delete&#39;, &#39;Trash&#39;],
        [&#39;mdi-alert-octagon&#39;, &#39;Spam&#39;],
      ],
  }),
}
&lt;/script&gt;
```
* index.js
``` javascript
import { createRouter, createWebHistory } from &#39;vue-router&#39;
import HomeView from &#39;../views/HomeView.vue&#39;
import LoginPage from &#39;../components/LoginPage.vue&#39;

const routes = [
  {
    path: &#39;/&#39;,
    name: &#39;home&#39;,
    component: HomeView
  },
  {
    path: &#39;/about&#39;,
    name: &#39;about&#39;,
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: &quot;about&quot; */ &#39;../views/AboutView.vue&#39;)
  },
  {
    path: &#39;/login&#39;,
    name: &#39;login&#39;,
    component: LoginPage
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router
```


### 3. 회원 가입 페이지 이후 전체 코드

**LoginPage.vue**
``` vue
&lt;template&gt;
    &lt;v-sheet class=&quot;pa-12&quot; rounded&gt;
        &lt;v-card class=&quot;mx-auto px-6 py-8&quot; max-width=&quot;60%&quot; :elevation=&quot;12&quot;&gt;
            &lt;v-form
            v-model=&quot;form&quot;
            @submit.prevent=&quot;onSubmit&quot;
            &gt;
                &lt;v-text-field
                    v-model=&quot;email&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[required]&quot;
                    class=&quot;mb-2&quot;
                    clearable
                    label=&quot;Email&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;v-text-field
                    v-model=&quot;password&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[required]&quot;
                    clearable
                    label=&quot;Password&quot;
                    type=&quot;password&quot;
                    placeholder=&quot;Enter your password&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;br&gt;

                &lt;v-btn
                    :disabled=&quot;!form&quot;
                    :loading=&quot;loading&quot;
                    block
                    color=&quot;success&quot;
                    size=&quot;large&quot;
                    type=&quot;submit&quot;
                    variant=&quot;elevated&quot;
                &gt;
                    Sign In
                &lt;/v-btn&gt;

                &lt;br&gt;

                &lt;v-btn
                    :disabled=&quot;!form&quot;
                    :loading=&quot;loading&quot;
                    block
                    color=&quot;primary&quot;
                    size=&quot;large&quot;
                    type=&quot;submit&quot;
                    variant=&quot;elevated&quot;
                    @click=&quot;$router.push(&#39;/signUp&#39;)&quot;
                &gt;
                    Sign Up
                &lt;/v-btn&gt;
            &lt;/v-form&gt;
        &lt;/v-card&gt;
    &lt;/v-sheet&gt;
&lt;/template&gt;

&lt;script&gt;

&lt;/script&gt;
```

**SignUpPage.vue**
``` vue
&lt;template&gt;
    &lt;v-sheet class=&quot;pa-12&quot; rounded&gt;
        &lt;v-card class=&quot;mx-auto px-6 py-8&quot; max-width=&quot;60%&quot; :elevation=&quot;12&quot;&gt;
            &lt;v-form
            v-model=&quot;form&quot;
            @submit.prevent=&quot;onSubmit&quot;
            &gt;
                &lt;v-text-field
                    v-model=&quot;email&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[required]&quot;
                    class=&quot;mb-2&quot;
                    clearable
                    label=&quot;Email&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;v-text-field
                    v-model=&quot;password&quot;
                    :readonly=&quot;loading&quot;
                    :rules=&quot;[required]&quot;
                    clearable
                    label=&quot;Password&quot;
                    placeholder=&quot;Enter your password&quot;
                &gt;&lt;/v-text-field&gt;

                &lt;br&gt;

                &lt;v-btn
                    :disabled=&quot;!form&quot;
                    :loading=&quot;loading&quot;
                    block
                    color=&quot;primary&quot;
                    size=&quot;large&quot;
                    type=&quot;submit&quot;
                    variant=&quot;elevated&quot;
                &gt;
                    Sign Up
                &lt;/v-btn&gt;
            &lt;/v-form&gt;
        &lt;/v-card&gt;
    &lt;/v-sheet&gt;
&lt;/template&gt;

&lt;script&gt;

&lt;/script&gt;
```

**index.js**
``` javascript
import { createRouter, createWebHistory } from &#39;vue-router&#39;
import HomeView from &#39;../views/HomeView.vue&#39;
import LoginPage from &#39;../components/LoginPage.vue&#39;
import SignUpPage from &#39;../components/SignUpPage.vue&#39;

const routes = [
  {
    path: &#39;/&#39;,
    name: &#39;home&#39;,
    component: HomeView
  },
  {
    path: &#39;/about&#39;,
    name: &#39;about&#39;,
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =&gt; import(/* webpackChunkName: &quot;about&quot; */ &#39;../views/AboutView.vue&#39;)
  },
  {
    path: &#39;/login&#39;,
    name: &#39;login&#39;,
    component: LoginPage
  },
  {
    path: &#39;/signUp&#39;,
    name: &#39;signUp&#39;,
    component: SignUpPage
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router
```

**HelloWorld.vue**
```
&lt;template&gt;
  &lt;v-app id=&quot;inspire&quot;&gt;
    &lt;v-navigation-drawer v-model=&quot;drawer&quot;&gt;
      &lt;v-sheet
        color=&quot;grey-lighten-4&quot;
        class=&quot;pa-4&quot;
      &gt;
        &lt;v-avatar
          class=&quot;mb-4&quot;
          color=&quot;grey-darken-1&quot;
          size=&quot;64&quot;
        &gt;&lt;/v-avatar&gt;
        &lt;div&gt;john@google.com&lt;/div&gt;
      &lt;/v-sheet&gt;

      &lt;v-divider&gt;&lt;/v-divider&gt;

      &lt;v-list&gt;
        &lt;v-list-item
          v-for=&quot;[icon, text] in links&quot;
          :key=&quot;icon&quot;
          link
        &gt;
          &lt;template v-slot:prepend&gt;
            &lt;v-icon&gt;{{ icon }}&lt;/v-icon&gt;
          &lt;/template&gt;

          &lt;v-list-item-title&gt;{{ text }}&lt;/v-list-item-title&gt;
        &lt;/v-list-item&gt;
      &lt;/v-list&gt;
    &lt;/v-navigation-drawer&gt;

    &lt;v-app-bar
      :order=&quot;order&quot;
      flat
      title=&quot;Application bar&quot;
    &gt;

      &lt;template v-slot:append&gt;
        &lt;v-switch
          v-model=&quot;order&quot;
          hide-details
          inset
          label=&quot;Toggle order&quot;
          true-value=&quot;-1&quot;
          false-value=&quot;0&quot;
        &gt;&lt;/v-switch&gt;
      &lt;/template&gt;

      &lt;!-- Activator Slot Start --&gt;
      &lt;v-menu&gt;
        &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
          &lt;v-btn
            color=&quot;primary&quot;
            v-bind=&quot;props&quot;
          &gt;
            Activator slot
          &lt;/v-btn&gt;
        &lt;/template&gt;
        &lt;v-list&gt;
          &lt;v-list-item
            v-for=&quot;(item, index) in items&quot;
            :key=&quot;index&quot;
            :value=&quot;index&quot;
          &gt;
            &lt;v-list-item-title&gt;{{ item.title }}&lt;/v-list-item-title&gt;
          &lt;/v-list-item&gt;
        &lt;/v-list&gt;
      &lt;/v-menu&gt;
      &lt;!-- Activator Slot End --&gt;

      &lt;!-- Activator2 Slot Start --&gt;
      &lt;v-menu&gt;
        &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
          &lt;v-btn
            color=&quot;primary&quot;
            v-bind=&quot;props&quot;
          &gt;
            Activator2 slot
          &lt;/v-btn&gt;
        &lt;/template&gt;
        &lt;v-list&gt;
          &lt;v-list-item
            v-for=&quot;(item, index) in items2&quot;
            :key=&quot;index&quot;
            :value=&quot;index&quot;
          &gt;
            &lt;v-list-item-title&gt;{{ item.title }}&lt;/v-list-item-title&gt;
          &lt;/v-list-item&gt;
        &lt;/v-list&gt;
      &lt;/v-menu&gt;
      &lt;!-- Activator2 Slot End --&gt;


      &lt;v-btn icon @click=&quot;$router.push(&#39;/login&#39;)&quot;&gt;
        &lt;v-icon&gt;mdi-login&lt;/v-icon&gt;
      &lt;/v-btn&gt;

    &lt;/v-app-bar&gt;

    &lt;v-main class=&quot;bg-grey-lighten-2&quot;&gt;
      &lt;v-container&gt;
        &lt;v-row&gt;
          &lt;template v-for=&quot;n in 4&quot; :key=&quot;n&quot;&gt;
            &lt;v-col
              class=&quot;mt-2&quot;
              cols=&quot;12&quot;
            &gt;
              &lt;strong&gt;Category {{ n }}&lt;/strong&gt;
            &lt;/v-col&gt;

            &lt;v-col
              v-for=&quot;j in 6&quot;
              :key=&quot;`${n}${j}`&quot;
              cols=&quot;6&quot;
              md=&quot;2&quot;
            &gt;
              &lt;v-sheet height=&quot;150&quot;&gt;&lt;/v-sheet&gt;
            &lt;/v-col&gt;
          &lt;/template&gt;
        &lt;/v-row&gt;
      &lt;/v-container&gt;
    &lt;/v-main&gt;
  &lt;/v-app&gt;

&lt;/template&gt;

&lt;script&gt;

export default {
  name: &#39;HelloWorld&#39;,

  data: () =&gt; ({
    order: 0,
    drawer: null,
    items: [
        { title: &#39;Item1 Click Me 1&#39; },
        { title: &#39;Item1 Click Me 2&#39; },
        { title: &#39;Item1 Click Me 3&#39; },
        { title: &#39;Item1 Click Me 4&#39; },
      ],
    items2: [
        { title: &#39;Item2 Click Me 1&#39; },
        { title: &#39;Item2 Click Me 2&#39; },
        { title: &#39;Item2 Click Me 3&#39; },
        { title: &#39;Item2 Click Me 4&#39; },
      ],
      links: [
        [&#39;mdi-inbox-arrow-down&#39;, &#39;Inbox&#39;],
        [&#39;mdi-send&#39;, &#39;Send&#39;],
        [&#39;mdi-delete&#39;, &#39;Trash&#39;],
        [&#39;mdi-alert-octagon&#39;, &#39;Spam&#39;],
      ],
  }),
}
&lt;/script&gt;
```</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[6. 로그인 방식 분석 (쿠키, Session, JWT)]]></title>
            <link>https://velog.io/@comet_strike/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%99%94%EB%A9%B4-%EA%B5%AC%EC%84%B1-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@comet_strike/%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%99%94%EB%A9%B4-%EA%B5%AC%EC%84%B1-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 25 Sep 2023 12:57:25 GMT</pubDate>
            <description><![CDATA[<p>Vuetify를 통해 기본 메인 환경에 대한 구성은 끝났으므로, 로그인 화면을 구성하고 로그인 기능을 만들 예정.
로그인은 Cookie, Session, JWT를 이용하는 방식이 있으며, Spring Security, Redis를 이용한 Token 처리 등 다양한 방법이 있으므로 이를 정리한 후 구현.</p>
<h1 id="1-로그인-방식">1. 로그인 방식</h1>
<p>사용자가 로그인화면에서 로그인을 수행할 경우 기본적으로 다음과 같은 방식으로 수행된다. </p>
<ol>
<li>로그인 정보를 입력 받는다.</li>
<li>로그인 정보를 서버에 전송한다.</li>
<li>서버는 로그인 정보를 수신한다.</li>
<li>인증 절차를 수행한다.</li>
<li>인증 결과를 클라이언트에 전송한다.</li>
</ol>
<h2 id="11-인증인가">1.1. 인증/인가</h2>
<p>인증 : 인증(認證, authentication)은 참이라는 근거가 있는 무언가를 확인하거나 확증하는 행위
인가 : 제3자의 법률적 행위를 행정청이 동의 · 승인의 형식으로 보충하여 그 법률상 효과를 완성시켜 주는 행정행위</p>
<p>Cookie, Session, JWT 방식 인증은 아래 링크 참고.</p>
<ul>
<li>인증/인가는 어디에 어떻게 구현해야 할까?<blockquote>
<p><a href="https://dev.gmarket.com/45">https://dev.gmarket.com/45</a></p>
</blockquote>
</li>
</ul>
<h4 id="로그인에서-인증인가">로그인에서 인증/인가</h4>
<p>사용자가 로그인을 하는 행위는 인증에 해당하며, 로그인 정보를 토대로 자원에 접근할 수 있도록 하는 것이 인가에 해당한다.</p>
<p>일반적인 웹 서비스는 stateless 상태이므로, 서버에서 인가를 받을 경우 이를 저장하는 방식이 필요하다.(그렇지 않을 경우, 지속적으로 인증을 수행해야한다.)
(※ stateless : 서버로 가는 모든 요청이 독립접으로 수행)</p>
<ul>
<li><p>statefull, stateless 참고 자료</p>
<blockquote>
<p><a href="https://www.geeksforgeeks.org/difference-between-stateless-and-stateful-protocol/">https://www.geeksforgeeks.org/difference-between-stateless-and-stateful-protocol/</a></p>
</blockquote>
</li>
<li><p>세션 vs 토큰 vs 쿠키? 기초개념 잡아드림. 10분 순삭! - 노마드코더 Nomad Coders</p>
<blockquote>
<p><a href="https://www.youtube.com/watch?v=tosLBcAX1vk">https://www.youtube.com/watch?v=tosLBcAX1vk</a></p>
</blockquote>
</li>
</ul>
<h2 id="12-cookie를-통한-로그인">1.2. Cookie를 통한 로그인</h2>
<h4 id="http-cookie">Http Cookie?</h4>
<p>HTTP 쿠키란 하이퍼 텍스트의 기록서의 일종으로서 인터넷 사용자가 어떠한 웹사이트를 방문할 경우 사용자의 웹 브라우저를 통해 인터넷 <strong>사용자의 컴퓨터나 다른 기기에 설치되는 작은 기록 정보 파일</strong>을 일컫는다.</p>
<p>세션을 통한 로그인도 세션 정보(Session ID)를 Cookie로 저장하므로 Cookie를 통한 로그인이라 볼 수 있지만,
여기서 Cookie를 통한 로그인은 Cookie에 ID, PW 등을 Cookie에 직접 저장 해 지속적으로 인증하는 것을 의미한다.</p>
<ul>
<li>Http Cookie - Wikipedia<blockquote>
<p><a href="https://ko.wikipedia.org/wiki/HTTP_%EC%BF%A0%ED%82%A4">https://ko.wikipedia.org/wiki/HTTP_%EC%BF%A0%ED%82%A4</a></p>
</blockquote>
</li>
</ul>
<p>HTTP 쿠키(웹 쿠키, 브라우저 쿠키)는 서버가 사용자의 웹 브라우저에 전송하는 작은 데이터 조각, 브라우저는 데이터 조각들을 저장해 놓았다가, 동일한 서버에 재 요청 시 저장된 데이터를 함께 전송한다.</p>
<ul>
<li>HTTP Cookie (mozilla.org)<blockquote>
<p><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Cookies">https://developer.mozilla.org/ko/docs/Web/HTTP/Cookies</a></p>
</blockquote>
</li>
</ul>
<p>Cookie를 통한 로그인 구현 시, 사용자의 로그인 정보를 저장하고 이를 서버에 전송하는 형태를 사용하게 된다. 이 경우 여러 보안 문제점을 가지게 된다.</p>
<ol>
<li>쿠키 탈취 (Ex, XSS 공격)</li>
<li>쿠키 변조</li>
<li>세션 하이재킹</li>
<li>쿠키 평문 저장</li>
</ol>
<p>쿠키는 클라이언트(브라우저)에 저장된다. 쿠키 기반 로그인에서 브라우저는 저장된 쿠키를 서버에 전송해 인가를 수행하게 된다. 이 쿠키는 인증을 위해 인증과 관련된 정보를 가지고 있어야 한다.</p>
<p>만약 쿠키를 탈취 당하게 된다면 인증 정보를 탈취 당한 것과 동일해 진다.
XSS(Cross-Site Scripting) 공격 기법을 통해 쿠키를 탈취할 수 있다. 보통 Cookie의 HttpOnly(XSS 방어) 및 Secure(Sniffing 방어) 설정을 통해 이를 예방 할 수 있다.</p>
<ul>
<li><p>Cookie Tracking and Stealing using Cross-Site Scripting</p>
<blockquote>
<p><a href="https://www.geeksforgeeks.org/cookie-tracking-stealing-using-cross-site-scripting/">https://www.geeksforgeeks.org/cookie-tracking-stealing-using-cross-site-scripting/</a></p>
</blockquote>
</li>
<li><p>HTTP Only flag와 Secure Cookie에 대하여</p>
<blockquote>
<p><a href="https://mitny.github.io/articles/2019-04/httpOnly-secureCookie">https://mitny.github.io/articles/2019-04/httpOnly-secureCookie</a></p>
</blockquote>
</li>
<li><p>Using HTTP cookies (Restrict access to cookies)</p>
<blockquote>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies">https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies</a></p>
</blockquote>
</li>
</ul>
<p>쿠키가 탈취되었고 ID, PW 같은 인증 정보가 평문으로 저장되었다면, 쿠키가 변경되어도 ID, PW를 통한 로그인이 가능해진다. </p>
<p>만약 세션 ID가 탈취되었다면, 이를 이용한 세션 하이재킹이 발생할 수 있다. (Session을 통한 로그인 구현 시)
HTTPS 프로토콜 사용과 세션에 만료 시간을 넣어 예방할 수 있다.</p>
<ul>
<li>세션 하이재킹<blockquote>
<p><a href="https://ko.wikipedia.org/wiki/%EC%84%B8%EC%85%98_%ED%95%98%EC%9D%B4%EC%9E%AC%ED%82%B9">https://ko.wikipedia.org/wiki/%EC%84%B8%EC%85%98_%ED%95%98%EC%9D%B4%EC%9E%AC%ED%82%B9</a></p>
</blockquote>
</li>
</ul>
<p>또한, 사용자가 쿠키 정보를 임의로 변경해 인가 및 권한 정보에 대한 공격 시도를 수행할 수 있다.</p>
<ul>
<li><p>Webhacking.kr 1번 풀이 - Cookie 변조 (Burp Suite를 통한 Cookie 변조)</p>
<blockquote>
<p><a href="https://dosnipe.tistory.com/66">https://dosnipe.tistory.com/66</a></p>
</blockquote>
</li>
<li><p>[EditThisCookie] 크롬에서 쿠키 수정 하기 (EditThisCookie를 통한 쿠키 변조)</p>
<blockquote>
<p><a href="https://minimin2.tistory.com/169">https://minimin2.tistory.com/169</a></p>
</blockquote>
</li>
</ul>
<h2 id="13-session을-통한-로그인">1.3. Session을 통한 로그인</h2>
<h4 id="session">Session?</h4>
<p>세션(session)은 컴퓨터 과학에서, 특히 네트워크 분야에서 반영구적이고 <strong>상호작용적인 정보 교환을 전제하는 둘 이상의 통신 장치나 컴퓨터와 사용자 간의 대화나 송수신 연결</strong>상태를 의미하는 보안적인 다이얼로그(dialogue) 및 시간대를 가리킨다. </p>
<ul>
<li>세션 (컴퓨터 과학) - Wikipedia<blockquote>
<p><a href="https://ko.wikipedia.org/wiki/%EC%84%B8%EC%85%98_(%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99)">https://ko.wikipedia.org/wiki/%EC%84%B8%EC%85%98_(%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99)</a>)</p>
</blockquote>
</li>
</ul>
<p>쿠키를 통한 ID, PW 전송 방식은 많은 보안 문제를 일으킨다. 인증 후 인가와 함께 사용자를 식별할 수 있는 고유값을 생성해 Cookie를 통해 주고 받는다면 ID, PW 같은 인가정보의 탈취는 예방할 수 있다.</p>
<p>다만, 이 경우에도 쿠키에 저장된 고유값을 탈취하면 Session Hijacking 기법을 통해 사용자 계정에 액세스할 수 있다.</p>
<h4 id="session-로그인">Session 로그인</h4>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/d729a906-b882-4667-b7ae-1b51380abd0d/image.png" alt=""></p>
<p>Session을 통한 로그인은</p>
<ol>
<li>사용자가 로그인 요청</li>
<li>서버 인증 수행</li>
<li>Session 정보 DB에 저장</li>
<li>DB에 저장된 Session 정보를 사용자 Cookie로 전달</li>
</ol>
<p>인증이 필요한 경우</p>
<ol>
<li>사용자는 수신한 쿠키를 서버에 전달</li>
<li>서버는 수신받은 쿠키에 해당하는 Session ID를 확인</li>
<li>Session ID와 유저를 확인해 인증</li>
</ol>
<ul>
<li>Session vs Token Based Authentication - geeksforgeeks.org<blockquote>
<p><a href="https://www.geeksforgeeks.org/session-vs-token-based-authentication/">https://www.geeksforgeeks.org/session-vs-token-based-authentication/</a></p>
</blockquote>
</li>
</ul>
<p>클라이언트와 서버의 세션 정보를 저장해야 하기 때문에 사용자가 많아 질 경우 부하가 많아진다. 단일 서버에서 다중 서버로 확장하게 될 경우 모든 서버가 세션에 대한 정보를 공유해야 하기 때문에 확장성 또한 좋지 않은 편이다.</p>
<h2 id="14-token을-통한-로그인">1.4. Token을 통한 로그인</h2>
<h4 id="토큰-기반-인증token-based-authentication">토큰 기반 인증(Token based Authentication)</h4>
<p>토큰 기반 인증이란 사용자가 자신의 아이덴티티를 확인하고 고유한 <strong>액세스 토큰</strong>을 받을 수 있는 프로토콜을 말한다.
(※ 액세스 토큰은 많은 양의 데이터를 포함하는 작은 코드 조각입니다. 사용자, 권한, 그룹 및 기간에 대한 정보는 서버에서 사용자의 장치로 전달되는 하나의 토큰에 포함)</p>
<ul>
<li>토큰 기반 인증이란? 인증 토큰의 종류와 JWT의 이점<blockquote>
<p><a href="https://www.okta.com/kr/identity-101/what-is-token-based-authentication/">https://www.okta.com/kr/identity-101/what-is-token-based-authentication/</a></p>
</blockquote>
</li>
</ul>
<h4 id="jwt-json-web-tokens">JWT (JSON Web Tokens)</h4>
<p>RFC 7519 표준으로 당사자 간에 정보를 JSON 개체로 안정하게 전송하는 간결하고 독립적인 방법. 디지털 서명이 되어 있어 신뢰할 수 있다.</p>
<ul>
<li>Introduction to JSON Web Tokens - jwt.io<blockquote>
<p><a href="https://jwt.io/introduction">https://jwt.io/introduction</a></p>
</blockquote>
</li>
</ul>
<p>JWT Token을 이용하면 내용 저장 없이 전달 받은 토큰을 통해 인증을 수행하고 필요한 정보를 전달할 수 있다. JWT는 Header, Payload, Signature로 구성되어  있다. 
Header는 토큰의 타입과 JWT를 서명하는데 사용한 알고리즘을 명시한다. 
Payload는 유저의 정보 또는 추가 데이터에 대한 내용이 들어있다. 서명된 토큰의 경우 해당 정보들은 보호되지만 누구나 읽을 수 있다. 암호화 되지 않는 경우 민감 정보를 담아서는 안된다. 
Signature 서버에서 토큰의 정보가 서버로부터 생성된 것인지 증명하기 위해 사용한다.</p>
<ul>
<li>JWT 설명 및 구조<blockquote>
<p><a href="https://doqtqu.tistory.com/275">https://doqtqu.tistory.com/275</a></p>
</blockquote>
</li>
<li>JWT란 무엇인가?<blockquote>
<p><a href="https://etloveguitar.tistory.com/101">https://etloveguitar.tistory.com/101</a> </p>
</blockquote>
</li>
</ul>
<h4 id="취약점">취약점</h4>
<ol>
<li>none Attack : JWT 대표적인 공격 방식으로 Header 영역에 alg값을 통해 알고리즘을 명시. 일부 JWT 라이브러리들은 alg값에 none 등 비 서명 처리 시 서명 검증을 하지 않는 취약점을 가지고 있다. 이를 이용하면 Secret 값을 몰라도 임의로 토큰을 생성, 수정할 수 있다.</li>
</ol>
<ul>
<li>JWT (JSON Web Token) Security<blockquote>
<p><a href="https://www.hahwul.com/cullinan/jwt/">https://www.hahwul.com/cullinan/jwt/</a></p>
</blockquote>
</li>
</ul>
<ol start="2">
<li>Brute force attack : 대칭키와 서명을 비교하여 JWT를 무효화.</li>
</ol>
<ul>
<li>JWT 해킹 사례를 통한 취약점 분석하기<blockquote>
<p><a href="https://anpigon.tistory.com/138">https://anpigon.tistory.com/138</a></p>
</blockquote>
</li>
</ul>
<ol start="3">
<li>JWT 도용 : 정상적으로 발급 된 JWT 토큰을 탈취당할 경우 해당 계정의 아이디, 비밀번호를 도용한 것과 같이 계정을 사용할 수 있다.서버 입장에서는 정상적인 JWT이므로 구분할 방법이 없다. 
JWT 탈취를 막기 위해서 Http Only Cookie에 JWT를 저장한다. 또한, 탈취 당할경우를 대비해 Access Token을 짧게 적용한 후, Refresh Token을 통해 Access Token을 새로 발급 받는 방식을 적용한다.
Refresh Token 조차 탈취 당할 경우 Access Token과 Refresh Token의 1:1 맵핑을 확인하는 등 여러가지 방법으로 기존 토큰을 만료시키는 방법을 사용할 수 있다. 다만 JWT를 임의로 만료시키는 방법은 없으므로 만료시간 만큼 DB에 저장하여 관리한다.</li>
</ol>
<ul>
<li>토큰(token)은 어떻게 탈취 당하는가?<blockquote>
<p><a href="https://velog.io/@ckdwns9121/%ED%86%A0%ED%81%B0token%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%83%88%EC%B7%A8-%EB%8B%B9%ED%95%98%EB%8A%94%EA%B0%80">https://velog.io/@ckdwns9121/%ED%86%A0%ED%81%B0token%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%83%88%EC%B7%A8-%EB%8B%B9%ED%95%98%EB%8A%94%EA%B0%80</a></p>
</blockquote>
</li>
<li>토큰(token)의 탈취를 최대한 예방하기<blockquote>
<p><a href="https://dkswnkk.tistory.com/684">https://dkswnkk.tistory.com/684</a></p>
</blockquote>
</li>
<li>JWT에서 Refresh Token은 왜 필요한가?<blockquote>
<p><a href="https://velog.io/@park2348190/JWT%EC%97%90%EC%84%9C-Refresh-Token%EC%9D%80-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80">https://velog.io/@park2348190/JWT%EC%97%90%EC%84%9C-Refresh-Token%EC%9D%80-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80</a></p>
</blockquote>
</li>
</ul>
<h2 id="15-oauth를-통한-로그인">1.5. OAuth를 통한 로그인</h2>
<h4 id="oauth-open-authorization">OAuth, Open Authorization</h4>
<p>인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준이다.</p>
<p>OAuth가 사용되기 전에는 인증방식의 표준이 없었기 때문에 기존의 기본인증인 아이디와 비밀번호를 사용하였는데, 이는 보안상 취약한 구조일 가능성이 매우 많다.</p>
<p>기본 인증이 아닐 경우는 각 애플리케이션들이 각자의 개발한 회사의 방법대로 사용자를 확인하였다. 예를 들면 구글의 AuthSub, AOL의 OpenAuth, 야후의 BBAuth, 아마존의 웹서비스 API 등이 있다.</p>
<p>OAuth는 이렇게 제각각인 인증방식을 표준화한 인증방식이다. OAuth를 이용하면 이 인증을 공유하는 애플리케이션끼리는 별도의 인증이 필요없다. 따라서 여러 애플리케이션을 통합하여 사용하는 것이 가능하게 된다.</p>
<h4 id="oauth-20의-동작-메커니즘">OAuth 2.0의 동작 메커니즘</h4>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/c7445f59-d9ca-412a-a446-59ea681c1b40/image.png" alt=""></p>
<p>클라이언트 : 앱. 휴대기기 또는 기존 웹 앱에서 실행되는 앱. 앱은 리소스 소유자를 대싱하녀 리소스 서버에 요청을 보낸다.</p>
<p>리소스 소유자 : 최종 사용자. 리소스에 대한 액세스 권한을 부여할 수 있다.</p>
<p>리소스 서버 : Facebook, Google, Twitter와 같은 서비스. OAuth 토큰 검증이 필요할 때마다 API 요청을 처리하는 리소스 서버.</p>
<p>승인 서버 : OAuth 2.0 사양을 준수하며 구현되며 액세스 토큰 발급을 담당.</p>
<p>보호된 리소스 : 리소스 소유자가 소유한 데이터. (연락처, 계정 정보 등)</p>
<ul>
<li>OAuth -  Wiki<blockquote>
<p><a href="https://ko.wikipedia.org/wiki/OAuth">https://ko.wikipedia.org/wiki/OAuth</a></p>
</blockquote>
</li>
<li>OAuth 2.0 소개 - Google Cloud<blockquote>
<p><a href="https://showerbugs.github.io/2017-11-16/OAuth-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C">https://showerbugs.github.io/2017-11-16/OAuth-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</a></p>
</blockquote>
</li>
</ul>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><p>인증/인가는 어디에 어떻게 구현해야 할까?</p>
<blockquote>
<p><a href="https://dev.gmarket.com/45">https://dev.gmarket.com/45</a></p>
</blockquote>
</li>
<li><p>statefull, stateless 참고 자료</p>
<blockquote>
<p><a href="https://www.geeksforgeeks.org/difference-between-stateless-and-stateful-protocol/">https://www.geeksforgeeks.org/difference-between-stateless-and-stateful-protocol/</a></p>
</blockquote>
</li>
<li><p>세션 vs 토큰 vs 쿠키? 기초개념 잡아드림. 10분 순삭! - 노마드코더 Nomad Coders</p>
<blockquote>
<p><a href="https://www.youtube.com/watch?v=tosLBcAX1vk">https://www.youtube.com/watch?v=tosLBcAX1vk</a></p>
</blockquote>
</li>
<li><p>Http Cookie - Wikipedia</p>
<blockquote>
<p><a href="https://ko.wikipedia.org/wiki/HTTP_%EC%BF%A0%ED%82%A4">https://ko.wikipedia.org/wiki/HTTP_%EC%BF%A0%ED%82%A4</a></p>
</blockquote>
</li>
<li><p>HTTP Cookie (mozilla.org)</p>
<blockquote>
<p><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Cookies">https://developer.mozilla.org/ko/docs/Web/HTTP/Cookies</a></p>
</blockquote>
</li>
<li><p>Cookie Tracking and Stealing using Cross-Site Scripting</p>
<blockquote>
<p><a href="https://www.geeksforgeeks.org/cookie-tracking-stealing-using-cross-site-scripting/">https://www.geeksforgeeks.org/cookie-tracking-stealing-using-cross-site-scripting/</a></p>
</blockquote>
</li>
<li><p>HTTP Only flag와 Secure Cookie에 대하여</p>
<blockquote>
<p><a href="https://mitny.github.io/articles/2019-04/httpOnly-secureCookie">https://mitny.github.io/articles/2019-04/httpOnly-secureCookie</a></p>
</blockquote>
</li>
<li><p>Using HTTP cookies (Restrict access to cookies)</p>
<blockquote>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies">https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies</a></p>
</blockquote>
</li>
<li><p>세션 하이재킹</p>
<blockquote>
<p><a href="https://ko.wikipedia.org/wiki/%EC%84%B8%EC%85%98_%ED%95%98%EC%9D%B4%EC%9E%AC%ED%82%B9">https://ko.wikipedia.org/wiki/%EC%84%B8%EC%85%98_%ED%95%98%EC%9D%B4%EC%9E%AC%ED%82%B9</a></p>
</blockquote>
</li>
<li><p>Webhacking.kr 1번 풀이 - Cookie 변조 (Burp Suite를 통한 Cookie 변조)</p>
<blockquote>
<p><a href="https://dosnipe.tistory.com/66">https://dosnipe.tistory.com/66</a></p>
</blockquote>
</li>
<li><p>[EditThisCookie] 크롬에서 쿠키 수정 하기 (EditThisCookie를 통한 쿠키 변조)</p>
<blockquote>
<p><a href="https://minimin2.tistory.com/169">https://minimin2.tistory.com/169</a></p>
</blockquote>
</li>
<li><p>세션 (컴퓨터 과학) - Wikipedia</p>
<blockquote>
<p><a href="https://ko.wikipedia.org/wiki/%EC%84%B8%EC%85%98_(%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99)">https://ko.wikipedia.org/wiki/%EC%84%B8%EC%85%98_(%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99)</a>)</p>
</blockquote>
</li>
<li><p>Session vs Token Based Authentication - geeksforgeeks.org</p>
<blockquote>
<p><a href="https://www.geeksforgeeks.org/session-vs-token-based-authentication/">https://www.geeksforgeeks.org/session-vs-token-based-authentication/</a></p>
</blockquote>
</li>
<li><p>토큰 기반 인증이란? 인증 토큰의 종류와 JWT의 이점</p>
<blockquote>
<p><a href="https://www.okta.com/kr/identity-101/what-is-token-based-authentication/">https://www.okta.com/kr/identity-101/what-is-token-based-authentication/</a></p>
</blockquote>
</li>
<li><p>Introduction to JSON Web Tokens - jwt.io</p>
<blockquote>
<p><a href="https://jwt.io/introduction">https://jwt.io/introduction</a></p>
</blockquote>
</li>
<li><p>JWT 설명 및 구조</p>
<blockquote>
<p><a href="https://doqtqu.tistory.com/275">https://doqtqu.tistory.com/275</a></p>
</blockquote>
</li>
<li><p>JWT란 무엇인가?</p>
<blockquote>
<p><a href="https://etloveguitar.tistory.com/101">https://etloveguitar.tistory.com/101</a> </p>
</blockquote>
</li>
<li><p>JWT (JSON Web Token) Security</p>
<blockquote>
<p><a href="https://www.hahwul.com/cullinan/jwt/">https://www.hahwul.com/cullinan/jwt/</a></p>
</blockquote>
</li>
<li><p>JWT 해킹 사례를 통한 취약점 분석하기</p>
<blockquote>
<p><a href="https://anpigon.tistory.com/138">https://anpigon.tistory.com/138</a></p>
</blockquote>
</li>
<li><p>토큰(token)은 어떻게 탈취 당하는가?</p>
<blockquote>
<p><a href="https://velog.io/@ckdwns9121/%ED%86%A0%ED%81%B0token%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%83%88%EC%B7%A8-%EB%8B%B9%ED%95%98%EB%8A%94%EA%B0%80">https://velog.io/@ckdwns9121/%ED%86%A0%ED%81%B0token%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%83%88%EC%B7%A8-%EB%8B%B9%ED%95%98%EB%8A%94%EA%B0%80</a></p>
</blockquote>
</li>
<li><p>토큰(token)의 탈취를 최대한 예방하기</p>
<blockquote>
<p><a href="https://dkswnkk.tistory.com/684">https://dkswnkk.tistory.com/684</a></p>
</blockquote>
</li>
<li><p>JWT에서 Refresh Token은 왜 필요한가?</p>
<blockquote>
<p><a href="https://velog.io/@park2348190/JWT%EC%97%90%EC%84%9C-Refresh-Token%EC%9D%80-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80">https://velog.io/@park2348190/JWT%EC%97%90%EC%84%9C-Refresh-Token%EC%9D%80-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80</a></p>
</blockquote>
</li>
<li><p>OAuth -  Wiki</p>
<blockquote>
<p><a href="https://ko.wikipedia.org/wiki/OAuth">https://ko.wikipedia.org/wiki/OAuth</a></p>
</blockquote>
</li>
<li><p>OAuth 2.0 소개 - Google Cloud</p>
<blockquote>
<p><a href="https://showerbugs.github.io/2017-11-16/OAuth-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C">https://showerbugs.github.io/2017-11-16/OAuth-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</a></p>
</blockquote>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[5. Vue 화면 디자인]]></title>
            <link>https://velog.io/@comet_strike/5.-Vue-%ED%99%94%EB%A9%B4-%EB%94%94%EC%9E%90%EC%9D%B8</link>
            <guid>https://velog.io/@comet_strike/5.-Vue-%ED%99%94%EB%A9%B4-%EB%94%94%EC%9E%90%EC%9D%B8</guid>
            <pubDate>Tue, 08 Aug 2023 13:18:07 GMT</pubDate>
            <description><![CDATA[<h1 id="layout-application-bar">Layout Application bar</h1>
<ul>
<li><p>Layout Application bar에 Veutify MENU : Activator Slot1, 2 추가 (HelloWorld.vue)</p>
<blockquote>
<p><a href="https://vuetifyjs.com/en/components/menus/">https://vuetifyjs.com/en/components/menus/</a></p>
</blockquote>
</li>
<li><p>Application bar 부분인 <v-app-bar></v-app-bar> 사이에 작성</p>
</li>
<li><p>Activator  Slot에 Items로 Data가 Binding 되어 있으므로, 2개의 Activation Slot 생성을 위해 Data bind Id 변경</p>
</li>
</ul>
<pre><code class="language-javascript">&lt;template&gt;
  &lt;v-layout class=&quot;rounded rounded-md&quot;&gt;
    &lt;v-navigation-drawer color=&quot;grey-darken-2&quot; permanent&gt;&lt;/v-navigation-drawer&gt;

    &lt;v-app-bar
      :order=&quot;order&quot;
      color=&quot;grey-lighten-2&quot;
      flat
      title=&quot;Application bar&quot;
    &gt;
      &lt;template v-slot:append&gt;
        &lt;v-switch
          v-model=&quot;order&quot;
          hide-details
          inset
          label=&quot;Toggle order&quot;
          true-value=&quot;-1&quot;
          false-value=&quot;0&quot;
        &gt;&lt;/v-switch&gt;
      &lt;/template&gt;

      &lt;!-- Activator Slot Start --&gt;
      &lt;v-menu&gt;
        &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
          &lt;v-btn
            color=&quot;primary&quot;
            v-bind=&quot;props&quot;
          &gt;
            Activator slot
          &lt;/v-btn&gt;
        &lt;/template&gt;
        &lt;v-list&gt;
          &lt;v-list-item
            v-for=&quot;(item, index) in items&quot;
            :key=&quot;index&quot;
            :value=&quot;index&quot;
          &gt;
            &lt;v-list-item-title&gt;{{ item.title }}&lt;/v-list-item-title&gt;
          &lt;/v-list-item&gt;
        &lt;/v-list&gt;
      &lt;/v-menu&gt;
      &lt;!-- Activator Slot End --&gt;

      &lt;!-- Activator2 Slot Start --&gt;
      &lt;v-menu&gt;
        &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
          &lt;v-btn
            color=&quot;primary&quot;
            v-bind=&quot;props&quot;
          &gt;
            Activator2 slot
          &lt;/v-btn&gt;
        &lt;/template&gt;
        &lt;v-list&gt;
          &lt;v-list-item
            v-for=&quot;(item, index) in items2&quot;
            :key=&quot;index&quot;
            :value=&quot;index&quot;
          &gt;
            &lt;v-list-item-title&gt;{{ item.title }}&lt;/v-list-item-title&gt;
          &lt;/v-list-item&gt;
        &lt;/v-list&gt;
      &lt;/v-menu&gt;
      &lt;!-- Activator2 Slot End --&gt;

    &lt;/v-app-bar&gt;

    &lt;v-main class=&quot;d-flex align-center justify-center&quot; style=&quot;min-height: 500px;&quot;&gt;
      Main Content
    &lt;/v-main&gt;
  &lt;/v-layout&gt;

&lt;/template&gt;

&lt;script&gt;

export default {
  name: &#39;HelloWorld&#39;,

  data: () =&gt; ({
    order: 0,
    items: [
        { title: &#39;Item1 Click Me 1&#39; },
        { title: &#39;Item1 Click Me 2&#39; },
        { title: &#39;Item1 Click Me 3&#39; },
        { title: &#39;Item1 Click Me 4&#39; },
      ],
    items2: [
        { title: &#39;Item2 Click Me 1&#39; },
        { title: &#39;Item2 Click Me 2&#39; },
        { title: &#39;Item2 Click Me 3&#39; },
        { title: &#39;Item2 Click Me 4&#39; },
      ],
  }),
}
&lt;/script&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/3830cc4b-aa83-44c9-850e-3855cdb695e6/image.png" alt=""></p>
<h1 id="vuetify-wireframes">Vuetify Wireframes</h1>
<h2 id="inbox-navigation-var">Inbox Navigation var</h2>
<ul>
<li>Wireframes를 이용해서 화면을 구성할 예정, Inbox의 Navigation var와 현재 페이지의 Toggle 기능을 살려둔 채로
main에는 System Bar를 적용한다.
<img src="https://velog.velcdn.com/images/comet_strike/post/4cc0926c-81d9-4a39-a7cb-694e247bd69f/image.png" alt=""></li>
</ul>
<ul>
<li><p>inbox의 v-navigation-drawer만 적용 (HelloWorld.vue)</p>
<pre><code class="language-javascript">&lt;template&gt;
&lt;v-layout class=&quot;rounded rounded-md&quot;&gt;
  &lt;v-navigation-drawer v-model=&quot;drawer&quot;&gt;
    &lt;v-sheet
      color=&quot;grey-lighten-4&quot;
      class=&quot;pa-4&quot;
    &gt;
      &lt;v-avatar
        class=&quot;mb-4&quot;
        color=&quot;grey-darken-1&quot;
        size=&quot;64&quot;
      &gt;&lt;/v-avatar&gt;

      &lt;div&gt;john@google.com&lt;/div&gt;
    &lt;/v-sheet&gt;

    &lt;v-divider&gt;&lt;/v-divider&gt;

    &lt;v-list&gt;
      &lt;v-list-item
        v-for=&quot;[icon, text] in links&quot;
        :key=&quot;icon&quot;
        link
      &gt;
        &lt;template v-slot:prepend&gt;
          &lt;v-icon&gt;{{ icon }}&lt;/v-icon&gt;
        &lt;/template&gt;

        &lt;v-list-item-title&gt;{{ text }}&lt;/v-list-item-title&gt;
      &lt;/v-list-item&gt;
    &lt;/v-list&gt;
  &lt;/v-navigation-drawer&gt;

  &lt;v-app-bar
    :order=&quot;order&quot;
    color=&quot;grey-lighten-2&quot;
    flat
    title=&quot;Application bar&quot;
  &gt;
    &lt;template v-slot:append&gt;
      &lt;v-switch
        v-model=&quot;order&quot;
        hide-details
        inset
        label=&quot;Toggle order&quot;
        true-value=&quot;-1&quot;
        false-value=&quot;0&quot;
      &gt;&lt;/v-switch&gt;
    &lt;/template&gt;

    &lt;!-- Activator Slot Start --&gt;
    &lt;v-menu&gt;
      &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
        &lt;v-btn
          color=&quot;primary&quot;
          v-bind=&quot;props&quot;
        &gt;
          Activator slot
        &lt;/v-btn&gt;
      &lt;/template&gt;
      &lt;v-list&gt;
        &lt;v-list-item
          v-for=&quot;(item, index) in items&quot;
          :key=&quot;index&quot;
          :value=&quot;index&quot;
        &gt;
          &lt;v-list-item-title&gt;{{ item.title }}&lt;/v-list-item-title&gt;
        &lt;/v-list-item&gt;
      &lt;/v-list&gt;
    &lt;/v-menu&gt;
    &lt;!-- Activator Slot End --&gt;

    &lt;!-- Activator2 Slot Start --&gt;
    &lt;v-menu&gt;
      &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
        &lt;v-btn
          color=&quot;primary&quot;
          v-bind=&quot;props&quot;
        &gt;
          Activator2 slot
        &lt;/v-btn&gt;
      &lt;/template&gt;
      &lt;v-list&gt;
        &lt;v-list-item
          v-for=&quot;(item, index) in items2&quot;
          :key=&quot;index&quot;
          :value=&quot;index&quot;
        &gt;
          &lt;v-list-item-title&gt;{{ item.title }}&lt;/v-list-item-title&gt;
        &lt;/v-list-item&gt;
      &lt;/v-list&gt;
    &lt;/v-menu&gt;
    &lt;!-- Activator2 Slot End --&gt;

  &lt;/v-app-bar&gt;

  &lt;v-main class=&quot;d-flex align-center justify-center&quot; style=&quot;min-height: 500px;&quot;&gt;
    Main Content
  &lt;/v-main&gt;
&lt;/v-layout&gt;
</code></pre>
</li>
</ul>
</template>

<script>

export default {
  name: 'HelloWorld',

  data: () => ({
    order: 0,
    items: [
        { title: 'Item1 Click Me 1' },
        { title: 'Item1 Click Me 2' },
        { title: 'Item1 Click Me 3' },
        { title: 'Item1 Click Me 4' },
      ],
    items2: [
        { title: 'Item2 Click Me 1' },
        { title: 'Item2 Click Me 2' },
        { title: 'Item2 Click Me 3' },
        { title: 'Item2 Click Me 4' },
      ],
  }),
}
</script>
<pre><code>
* 적용하니 문제 발생, 처음 화면에서 제대로 반영이 안되고 Toogle 하면 생성된다.
![](https://velog.velcdn.com/images/comet_strike/post/aa43e381-691c-45fe-b4c1-b25428c62f71/image.png)

* Toggle order 클릭 시 반영 됨

![](https://velog.velcdn.com/images/comet_strike/post/7830c073-7ae7-418a-9e92-f8c8769dc19f/image.png)

![](https://velog.velcdn.com/images/comet_strike/post/d447656d-4dc3-4939-a200-63fa8ae75669/image.png)


* script에 drawer이 생략되어 발생 (HelloWorld.vue), 추가 후 정상반영 확인
``` javascript
&lt;script&gt;

export default {
  name: &#39;HelloWorld&#39;,

  data: () =&gt; ({
    order: 0,
    drawer: null,
    items: [
        { title: &#39;Item1 Click Me 1&#39; },
        { title: &#39;Item1 Click Me 2&#39; },
        { title: &#39;Item1 Click Me 3&#39; },
        { title: &#39;Item1 Click Me 4&#39; },
      ],
    items2: [
        { title: &#39;Item2 Click Me 1&#39; },
        { title: &#39;Item2 Click Me 2&#39; },
        { title: &#39;Item2 Click Me 3&#39; },
        { title: &#39;Item2 Click Me 4&#39; },
      ],
      links: [
        [&#39;mdi-inbox-arrow-down&#39;, &#39;Inbox&#39;],
        [&#39;mdi-send&#39;, &#39;Send&#39;],
        [&#39;mdi-delete&#39;, &#39;Trash&#39;],
        [&#39;mdi-alert-octagon&#39;, &#39;Spam&#39;],
      ],
  }),
}
&lt;/script&gt;</code></pre><p><img src="https://velog.velcdn.com/images/comet_strike/post/0cfb9e9f-6e3b-498e-a179-8d517386a3b5/image.png" alt=""></p>
<h2 id="system-bar---main">System bar - Main</h2>
<ul>
<li><p>v-main 내용만 복사해서 적용 완료.</p>
<pre><code class="language-javascript">&lt;template&gt;
&lt;v-layout class=&quot;rounded rounded-md&quot;&gt;
  &lt;v-navigation-drawer v-model=&quot;drawer&quot;&gt;
    &lt;v-sheet
      color=&quot;grey-lighten-4&quot;
      class=&quot;pa-4&quot;
    &gt;
      &lt;v-avatar
        class=&quot;mb-4&quot;
        color=&quot;grey-darken-1&quot;
        size=&quot;64&quot;
      &gt;&lt;/v-avatar&gt;

      &lt;div&gt;john@google.com&lt;/div&gt;
    &lt;/v-sheet&gt;

    &lt;v-divider&gt;&lt;/v-divider&gt;

    &lt;v-list&gt;
      &lt;v-list-item
        v-for=&quot;[icon, text] in links&quot;
        :key=&quot;icon&quot;
        link
      &gt;
        &lt;template v-slot:prepend&gt;
          &lt;v-icon&gt;{{ icon }}&lt;/v-icon&gt;
        &lt;/template&gt;

        &lt;v-list-item-title&gt;{{ text }}&lt;/v-list-item-title&gt;
      &lt;/v-list-item&gt;
    &lt;/v-list&gt;
  &lt;/v-navigation-drawer&gt;

  &lt;v-app-bar
    :order=&quot;order&quot;
    color=&quot;grey-lighten-2&quot;
    flat
    title=&quot;Application bar&quot;
  &gt;
    &lt;template v-slot:append&gt;
      &lt;v-switch
        v-model=&quot;order&quot;
        hide-details
        inset
        label=&quot;Toggle order&quot;
        true-value=&quot;-1&quot;
        false-value=&quot;0&quot;
      &gt;&lt;/v-switch&gt;
    &lt;/template&gt;

    &lt;!-- Activator Slot Start --&gt;
    &lt;v-menu&gt;
      &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
        &lt;v-btn
          color=&quot;primary&quot;
          v-bind=&quot;props&quot;
        &gt;
          Activator slot
        &lt;/v-btn&gt;
      &lt;/template&gt;
      &lt;v-list&gt;
        &lt;v-list-item
          v-for=&quot;(item, index) in items&quot;
          :key=&quot;index&quot;
          :value=&quot;index&quot;
        &gt;
          &lt;v-list-item-title&gt;{{ item.title }}&lt;/v-list-item-title&gt;
        &lt;/v-list-item&gt;
      &lt;/v-list&gt;
    &lt;/v-menu&gt;
    &lt;!-- Activator Slot End --&gt;

    &lt;!-- Activator2 Slot Start --&gt;
    &lt;v-menu&gt;
      &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
        &lt;v-btn
          color=&quot;primary&quot;
          v-bind=&quot;props&quot;
        &gt;
          Activator2 slot
        &lt;/v-btn&gt;
      &lt;/template&gt;
      &lt;v-list&gt;
        &lt;v-list-item
          v-for=&quot;(item, index) in items2&quot;
          :key=&quot;index&quot;
          :value=&quot;index&quot;
        &gt;
          &lt;v-list-item-title&gt;{{ item.title }}&lt;/v-list-item-title&gt;
        &lt;/v-list-item&gt;
      &lt;/v-list&gt;
    &lt;/v-menu&gt;
    &lt;!-- Activator2 Slot End --&gt;

  &lt;/v-app-bar&gt;

  &lt;v-main class=&quot;bg-grey-lighten-2&quot;&gt;
    &lt;v-container&gt;
      &lt;v-row&gt;
        &lt;template v-for=&quot;n in 4&quot; :key=&quot;n&quot;&gt;
          &lt;v-col
            class=&quot;mt-2&quot;
            cols=&quot;12&quot;
          &gt;
            &lt;strong&gt;Category {{ n }}&lt;/strong&gt;
          &lt;/v-col&gt;

          &lt;v-col
            v-for=&quot;j in 6&quot;
            :key=&quot;`${n}${j}`&quot;
            cols=&quot;6&quot;
            md=&quot;2&quot;
          &gt;
            &lt;v-sheet height=&quot;150&quot;&gt;&lt;/v-sheet&gt;
          &lt;/v-col&gt;
        &lt;/template&gt;
      &lt;/v-row&gt;
    &lt;/v-container&gt;
  &lt;/v-main&gt;
&lt;/v-layout&gt;
</code></pre>
</li>
</ul>
</template>

<script>

export default {
  name: 'HelloWorld',

  data: () => ({
    order: 0,
    drawer: null,
    items: [
        { title: 'Item1 Click Me 1' },
        { title: 'Item1 Click Me 2' },
        { title: 'Item1 Click Me 3' },
        { title: 'Item1 Click Me 4' },
      ],
    items2: [
        { title: 'Item2 Click Me 1' },
        { title: 'Item2 Click Me 2' },
        { title: 'Item2 Click Me 3' },
        { title: 'Item2 Click Me 4' },
      ],
      links: [
        ['mdi-inbox-arrow-down', 'Inbox'],
        ['mdi-send', 'Send'],
        ['mdi-delete', 'Trash'],
        ['mdi-alert-octagon', 'Spam'],
      ],
  }),
}
</script>
<pre><code>
![](https://velog.velcdn.com/images/comet_strike/post/1ae726cb-300b-4990-9339-9d2447b38c62/image.png)


* Application bar 부분과 Main 부분의 영역이 구분이 안되므로 색 변경
``` javascript
=== 기존
&lt;v-app-bar
      :order=&quot;order&quot;
      color=&quot;grey-lighten-2&quot;
      flat
      title=&quot;Application bar&quot;
    &gt;
=== 변경
&lt;v-app-bar
      :order=&quot;order&quot;
      flat
      title=&quot;Application bar&quot;
    &gt;</code></pre><p><img src="https://velog.velcdn.com/images/comet_strike/post/bf7468f2-219c-49a2-8016-38ed772104be/image.png" alt=""></p>
<ul>
<li>현재 v-layout으로 구성되어 있어, 화면 전체에 적용이 안되고 있어 v-layout을 v-app으로 변경
<img src="https://velog.velcdn.com/images/comet_strike/post/2d8fbcf7-fd78-436a-ba97-610aca96821a/image.png" alt="">
화면 전체에 적용된 걸 확인할 수 있다.</li>
</ul>
<p>전체 코드</p>
<pre><code class="language-javascript">&lt;template&gt;
  &lt;v-app id=&quot;inspire&quot;&gt;
    &lt;v-navigation-drawer v-model=&quot;drawer&quot;&gt;
      &lt;v-sheet
        color=&quot;grey-lighten-4&quot;
        class=&quot;pa-4&quot;
      &gt;
        &lt;v-avatar
          class=&quot;mb-4&quot;
          color=&quot;grey-darken-1&quot;
          size=&quot;64&quot;
        &gt;&lt;/v-avatar&gt;

        &lt;div&gt;john@google.com&lt;/div&gt;
      &lt;/v-sheet&gt;

      &lt;v-divider&gt;&lt;/v-divider&gt;

      &lt;v-list&gt;
        &lt;v-list-item
          v-for=&quot;[icon, text] in links&quot;
          :key=&quot;icon&quot;
          link
        &gt;
          &lt;template v-slot:prepend&gt;
            &lt;v-icon&gt;{{ icon }}&lt;/v-icon&gt;
          &lt;/template&gt;

          &lt;v-list-item-title&gt;{{ text }}&lt;/v-list-item-title&gt;
        &lt;/v-list-item&gt;
      &lt;/v-list&gt;
    &lt;/v-navigation-drawer&gt;

    &lt;v-app-bar
      :order=&quot;order&quot;
      flat
      title=&quot;Application bar&quot;
    &gt;
      &lt;template v-slot:append&gt;
        &lt;v-switch
          v-model=&quot;order&quot;
          hide-details
          inset
          label=&quot;Toggle order&quot;
          true-value=&quot;-1&quot;
          false-value=&quot;0&quot;
        &gt;&lt;/v-switch&gt;
      &lt;/template&gt;

      &lt;!-- Activator Slot Start --&gt;
      &lt;v-menu&gt;
        &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
          &lt;v-btn
            color=&quot;primary&quot;
            v-bind=&quot;props&quot;
          &gt;
            Activator slot
          &lt;/v-btn&gt;
        &lt;/template&gt;
        &lt;v-list&gt;
          &lt;v-list-item
            v-for=&quot;(item, index) in items&quot;
            :key=&quot;index&quot;
            :value=&quot;index&quot;
          &gt;
            &lt;v-list-item-title&gt;{{ item.title }}&lt;/v-list-item-title&gt;
          &lt;/v-list-item&gt;
        &lt;/v-list&gt;
      &lt;/v-menu&gt;
      &lt;!-- Activator Slot End --&gt;

      &lt;!-- Activator2 Slot Start --&gt;
      &lt;v-menu&gt;
        &lt;template v-slot:activator=&quot;{ props }&quot;&gt;
          &lt;v-btn
            color=&quot;primary&quot;
            v-bind=&quot;props&quot;
          &gt;
            Activator2 slot
          &lt;/v-btn&gt;
        &lt;/template&gt;
        &lt;v-list&gt;
          &lt;v-list-item
            v-for=&quot;(item, index) in items2&quot;
            :key=&quot;index&quot;
            :value=&quot;index&quot;
          &gt;
            &lt;v-list-item-title&gt;{{ item.title }}&lt;/v-list-item-title&gt;
          &lt;/v-list-item&gt;
        &lt;/v-list&gt;
      &lt;/v-menu&gt;
      &lt;!-- Activator2 Slot End --&gt;

    &lt;/v-app-bar&gt;

    &lt;v-main class=&quot;bg-grey-lighten-2&quot;&gt;
      &lt;v-container&gt;
        &lt;v-row&gt;
          &lt;template v-for=&quot;n in 4&quot; :key=&quot;n&quot;&gt;
            &lt;v-col
              class=&quot;mt-2&quot;
              cols=&quot;12&quot;
            &gt;
              &lt;strong&gt;Category {{ n }}&lt;/strong&gt;
            &lt;/v-col&gt;

            &lt;v-col
              v-for=&quot;j in 6&quot;
              :key=&quot;`${n}${j}`&quot;
              cols=&quot;6&quot;
              md=&quot;2&quot;
            &gt;
              &lt;v-sheet height=&quot;150&quot;&gt;&lt;/v-sheet&gt;
            &lt;/v-col&gt;
          &lt;/template&gt;
        &lt;/v-row&gt;
      &lt;/v-container&gt;
    &lt;/v-main&gt;
  &lt;/v-app&gt;

&lt;/template&gt;

&lt;script&gt;

export default {
  name: &#39;HelloWorld&#39;,

  data: () =&gt; ({
    order: 0,
    drawer: null,
    items: [
        { title: &#39;Item1 Click Me 1&#39; },
        { title: &#39;Item1 Click Me 2&#39; },
        { title: &#39;Item1 Click Me 3&#39; },
        { title: &#39;Item1 Click Me 4&#39; },
      ],
    items2: [
        { title: &#39;Item2 Click Me 1&#39; },
        { title: &#39;Item2 Click Me 2&#39; },
        { title: &#39;Item2 Click Me 3&#39; },
        { title: &#39;Item2 Click Me 4&#39; },
      ],
      links: [
        [&#39;mdi-inbox-arrow-down&#39;, &#39;Inbox&#39;],
        [&#39;mdi-send&#39;, &#39;Send&#39;],
        [&#39;mdi-delete&#39;, &#39;Trash&#39;],
        [&#39;mdi-alert-octagon&#39;, &#39;Spam&#39;],
      ],
  }),
}
&lt;/script&gt;</code></pre>
<h1 id="git-push">git push</h1>
<ul>
<li><code>git add .</code> 명령어로 작업한 파일 전부 add</li>
<li><code>git commit -m</code> 명령어로 커밋 메시지 작성<pre><code class="language-bash">git commit -m &#39;초기 화면 구성 완료
&gt; Vuetify Wireframes를 이용한 화면 구성
&gt; 1. Wireframes - Inbox : v-navigation-drawer
&gt; 2. Application Bar : Toogle 적용, Activator Bar 생성
&gt; 3. Wireframes - System bar : v-main 구현 완료&#39;</code></pre>
</li>
<li><code>git push</code><pre><code class="language-bash">$ git push
Enumerating objects: 13, done.
Counting objects: 100% (13/13), done.
Delta compression using up to 12 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (7/7), 1.60 KiB | 1.60 MiB/s, done.
Total 7 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To https://github.com/Fortuna3Co/Toy-Project.git
 34a513c..2fd3135  feature -&gt; feature</code></pre>
</li>
<li>정상적으로 반영 완료</li>
</ul>
<h1 id="github">GitHub</h1>
<p><a href="https://github.com/Fortuna3Co/Toy-Project/tree/feature">https://github.com/Fortuna3Co/Toy-Project/tree/feature</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[4. Vue 초기 화면 구성, Git Push]]></title>
            <link>https://velog.io/@comet_strike/4.-Vue-%EC%B4%88%EA%B8%B0-%ED%99%94%EB%A9%B4-%EA%B5%AC%EC%84%B1-Git-Push</link>
            <guid>https://velog.io/@comet_strike/4.-Vue-%EC%B4%88%EA%B8%B0-%ED%99%94%EB%A9%B4-%EA%B5%AC%EC%84%B1-Git-Push</guid>
            <pubDate>Mon, 07 Aug 2023 12:24:08 GMT</pubDate>
            <description><![CDATA[<h1 id="vue-초기-화면-구성">Vue 초기 화면 구성</h1>
<ul>
<li>Vuetify를 적용한 Vue 초기 화면</li>
</ul>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/9d392191-525d-4a60-8fe1-9cc8e7e328e9/image.png" alt=""></p>
<ul>
<li>HelloWorld.vue에 Vuetify의 Application Layout을 적용한다.
<img src="https://velog.velcdn.com/images/comet_strike/post/efe61715-ce5a-467e-b8fd-31941cd89612/image.png" alt=""></li>
</ul>
<pre><code class="language-vue.js">&lt;template&gt;
  &lt;v-layout class=&quot;rounded rounded-md&quot;&gt;
    &lt;v-app-bar title=&quot;Application bar&quot;&gt;&lt;/v-app-bar&gt;

    &lt;v-navigation-drawer&gt;
      &lt;v-list&gt;
        &lt;v-list-item title=&quot;Navigation drawer&quot;&gt;&lt;/v-list-item&gt;
      &lt;/v-list&gt;
    &lt;/v-navigation-drawer&gt;

    &lt;v-main class=&quot;d-flex align-center justify-center&quot; style=&quot;min-height: 300px;&quot;&gt;
      Main Content
    &lt;/v-main&gt;
  &lt;/v-layout&gt;
&lt;/template&gt;</code></pre>
<ul>
<li>필요없는 코드를 삭제하고 레이아웃만 추가한다. HelloWorld.vue
<img src="https://velog.velcdn.com/images/comet_strike/post/b6ec4cb0-e670-481c-80b6-e2a966018ef3/image.png" alt=""></li>
</ul>
<pre><code>&lt;template&gt;
  &lt;v-layout class=&quot;rounded rounded-md&quot;&gt;
    &lt;v-app-bar title=&quot;Application bar&quot;&gt;

    &lt;/v-app-bar&gt;

    &lt;v-navigation-drawer&gt;
      &lt;v-list&gt;
        &lt;v-list-item title=&quot;Navigation drawer&quot;&gt;&lt;/v-list-item&gt;
      &lt;/v-list&gt;
    &lt;/v-navigation-drawer&gt;

    &lt;v-main class=&quot;d-flex align-center justify-center&quot; style=&quot;min-height: 300px;&quot;&gt;
      Main Content
    &lt;/v-main&gt;
  &lt;/v-layout&gt;

&lt;/template&gt;

&lt;script&gt;

export default {
  name: &#39;HelloWorld&#39;,

  data: () =&gt; ({
  }),
}
&lt;/script&gt;</code></pre><ul>
<li>Application bar 왼쪽 서식을 확인한다.</li>
</ul>
<blockquote>
<p>flex justify : <a href="https://vuetifyjs.com/en/styles/flex/">https://vuetifyjs.com/en/styles/flex/</a></p>
</blockquote>
<pre><code class="language-vue.js">&lt;template&gt;
  &lt;v-layout class=&quot;rounded rounded-md&quot;&gt;
    &lt;v-app-bar title=&quot;Application bar&quot;&gt;
      &lt;div class=&quot;d-flex justify-end mb-6 bg-surface-variant&quot;&gt;
      &lt;v-sheet
        v-for=&quot;n in 3&quot;
        :key=&quot;n&quot;
        class=&quot;ma-2 pa-2&quot;
      &gt;
        justify-end
      &lt;/v-sheet&gt;
    &lt;/div&gt;
    &lt;/v-app-bar&gt;

    &lt;v-navigation-drawer&gt;
      &lt;v-list&gt;
        &lt;v-list-item title=&quot;Navigation drawer&quot;&gt;&lt;/v-list-item&gt;
      &lt;/v-list&gt;
    &lt;/v-navigation-drawer&gt;

    &lt;v-main class=&quot;d-flex align-center justify-center&quot; style=&quot;min-height: 300px;&quot;&gt;
      Main Content
    &lt;/v-main&gt;
  &lt;/v-layout&gt;

&lt;/template&gt;

&lt;script&gt;

export default {
  name: &#39;HelloWorld&#39;,

  data: () =&gt; ({
  }),
}
&lt;/script&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/bcf688e0-4396-4ace-b6eb-38a1198edeeb/image.png" alt=""></p>
<h1 id="git-push">Git push</h1>
<ul>
<li>환경 설정이 완료되었으니  git push를 실시
<img src="https://velog.velcdn.com/images/comet_strike/post/b080058d-bd1d-4e4e-b613-97836305bf1f/image.png" alt=""></li>
</ul>
<ul>
<li>git add 실행
<img src="https://velog.velcdn.com/images/comet_strike/post/7e9f73c7-aea7-48e1-b434-048a03ae1f2f/image.png" alt=""><ul>
<li>LF will be replaced by CRLF the next time Git touches it Error 발생 *</li>
<li>CRLF : 윈도우 줄 바꿈, LF : Mac, Linux 줄 바꿈 *</li>
<li>윈도우  OS를 사용하고 있으므로 <code>git config --global core.autocrlf true</code>를 설정*</li>
</ul>
</li>
</ul>
<blockquote>
<p><a href="https://dabo-dev.tistory.com/13">https://dabo-dev.tistory.com/13</a></p>
</blockquote>
<ul>
<li><p>git commit 실행, commit 메시지는 자신 또는 다른 사람이 알아 볼 수 있도록 작성
<img src="https://velog.velcdn.com/images/comet_strike/post/a3667bfd-e1d1-4fb8-8f64-721af66662a8/image.png" alt=""></p>
<pre><code class="language-bash">kang@DESKTOP-P0G8MJ1 MINGW64 ~/Desktop/Toy Project/Toy-Project (feature)
$ git commit -m &quot;초기 환경설정 구성 완료
&gt;  Back : Java17, SpringBoot
&gt;  Front : Vue3, Vuetify
&gt;  Front  Vuetify layout 적용&quot;</code></pre>
</li>
<li><p>git push</p>
</li>
<li><p>feature 구성 완료</p>
</li>
<li><p>feature 브랜치 develop으로 반영 (Compare &amp; pull request)
<img src="https://velog.velcdn.com/images/comet_strike/post/09549bbb-fe5c-49a3-8dc6-cc9078b0de8c/image.png" alt=""></p>
</li>
<li><p>Create pull request, Write에는 Commit 메세지를 적어준다.
<img src="https://velog.velcdn.com/images/comet_strike/post/aaa340d1-b68d-47b7-ad38-b0c2c1cd5201/image.png" alt=""></p>
</li>
<li><p>Merge pull request
<img src="https://velog.velcdn.com/images/comet_strike/post/5c4ebb97-4811-4dda-aab6-11a6baf4d7da/image.png" alt=""></p>
</li>
<li><p>Merge 확인
<img src="https://velog.velcdn.com/images/comet_strike/post/81541ac5-6f0f-4689-943a-969ad3a18b62/image.png" alt=""></p>
</li>
</ul>
<ul>
<li>1인 개발이므로 충돌 걱정은 없지만, 추후 팀프로젝트가 될 수도 있으므로, git flow 양식을 최대한 지킨다.
기능 개발은 develop에서 새  feature 브랜치를 파서 기능을 구현하고, 구현이 완료되면 develop 브랜치에 merge 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[3. Vue Router]]></title>
            <link>https://velog.io/@comet_strike/Vue-Router</link>
            <guid>https://velog.io/@comet_strike/Vue-Router</guid>
            <pubDate>Thu, 03 Aug 2023 13:11:06 GMT</pubDate>
            <description><![CDATA[<h1 id="vue-router-생성">Vue Router 생성</h1>
<ul>
<li><code>npm install vue-router</code> 를 vscode에서 터미널에 입력<pre><code class="language-bash">&gt; npm install vue-router
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.3.2 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.3.2: wanted {&quot;os&quot;:&quot;darwin&quot;,&quot;arch&quot;:&quot;any&quot;} (current: {&quot;os&quot;:&quot;win32&quot;,&quot;arch&quot;:&quot;x64&quot;})
</code></pre>
</li>
</ul>
<ul>
<li><a href="mailto:vue-router@4.2.4">vue-router@4.2.4</a>
added 2 packages from 1 contributor and audited 966 packages in 3.411s</li>
</ul>
<p>108 packages are looking for funding
  run <code>npm fund</code> for details</p>
<p>found 0 vulnerabilities</p>
<pre><code>* `vue add router` 실행, 만약 아래 사진과 같은 에러 발생시 터미널을 Command Propmpt로 변경
![](https://velog.velcdn.com/images/comet_strike/post/ee9bb654-2650-44a2-9498-070ae8d1507e/image.png)

* (Y/N) 물을 때 모두 Y로 선택하고 나면 router 폴더와 index.js, views폴더와 AboutView.vue, HomeView.vue 파일이 생성된다. 
* main.js 파일에도 router가 자동으로 import 된 것을 확인할 수 있다. App.vue 파일에도 router-link 코드가 생성된걸 확인할 수 있다.
</code></pre><pre><code>&lt;router-link to=&quot;/&quot;&gt;Home&lt;/router-link&gt; |
&lt;router-link to=&quot;/about&quot;&gt;About&lt;/router-link&gt;</code></pre><pre><code>
![](https://velog.velcdn.com/images/comet_strike/post/9462283d-ce0b-422f-b95b-afb78f9ec858/image.png)
![](https://velog.velcdn.com/images/comet_strike/post/ecdf7f75-69ed-49e1-bb31-e5bf2e2926b2/image.png)
![](https://velog.velcdn.com/images/comet_strike/post/f2bfbf5b-f1c7-4840-9128-687f33b83106/image.png)



# Vuetify 적용
* 디자인을 위해 Vuetify를 사용한다. 설치 `vue add vuetify` / `npm install vuetify --save`
![](https://velog.velcdn.com/images/comet_strike/post/ae1a0956-95b0-4389-be4c-d5b7514258e2/image.png)

* 설치 후 main.js가 vuetify를 사용하도록 바뀐것을 알 수 있다.
``` javascript
import { createApp } from &#39;vue&#39;
import App from &#39;./App.vue&#39;
import router from &#39;./router&#39;
import vuetify from &#39;./plugins/vuetify&#39;
import { loadFonts } from &#39;./plugins/webfontloader&#39;

loadFonts()

createApp(App)
  .use(router)
  .use(vuetify)
  .mount(&#39;#app&#39;)</code></pre><ul>
<li>HelloWorld.vue의 코드도 바뀌어서 실행하면 아래 화면처럼 Vuetify가 정상적으로 적용된 걸 볼 수 있다.
<img src="https://velog.velcdn.com/images/comet_strike/post/7c10fd10-4399-4ae0-93bb-70e47b2478e8/image.png" alt=""></li>
</ul>
<blockquote>
<p><a href="https://v15.vuetifyjs.com/ko/getting-started/quick-start/">https://v15.vuetifyjs.com/ko/getting-started/quick-start/</a></p>
</blockquote>
<p>이로써 환경 구성이 끝났다...
이제 개발 시작...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2. Vue (Header, Footer)]]></title>
            <link>https://velog.io/@comet_strike/Vue</link>
            <guid>https://velog.io/@comet_strike/Vue</guid>
            <pubDate>Wed, 02 Aug 2023 13:49:44 GMT</pubDate>
            <description><![CDATA[<h1 id="레이아웃">레이아웃</h1>
<ul>
<li>header / contents / footer로 구성</li>
<li>src 폴더 밑에 common 폴더를 만든후 header와 footer 생성
<img src="https://velog.velcdn.com/images/comet_strike/post/37317f38-0ad6-49d2-80b9-122900136212/image.png" alt=""></li>
<li>header 작성<pre><code class="language-javascript">&lt;template&gt;
  &lt;header&gt;
      &lt;div class=&quot;headerMenuWrap&quot;&gt;
          &lt;ul class=&quot;headerMenuUl&quot;&gt;
              &lt;li&gt;메뉴1&lt;/li&gt;
              &lt;li&gt;메뉴2&lt;/li&gt;
              &lt;li&gt;메뉴3&lt;/li&gt;
          &lt;/ul&gt;
      &lt;/div&gt;
  &lt;/header&gt;
</code></pre>
</li>
</ul>
</template>
<script>
export default{

<p>}</p>
<p></script></p>
<style>

</style>
<pre><code>* footer 작성
``` javascript
&lt;template&gt;
    &lt;footer&gt;
        &lt;p&gt;https://velog.io/@comet_strike/series/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8&lt;/p&gt;
    &lt;/footer&gt;

&lt;/template&gt;
&lt;script&gt;
export default{

}

&lt;/script&gt;

&lt;style&gt;

&lt;/style&gt;</code></pre><ul>
<li>App.vue 수정, Script에 import와 components 추가<pre><code class="language-javascript">&lt;template&gt;
&lt;div&gt;
  &lt;Header&gt;&lt;/Header&gt;
  &lt;HelloWorld msg=&quot;Welcome to Your Vue.js App&quot;/&gt;
  &lt;Footer&gt;&lt;/Footer&gt;
&lt;/div&gt;
&lt;/template&gt;
</code></pre>
</li>
</ul>
<script>
import HelloWorld from './components/HelloWorld.vue'
import Header from './components/common/Header.vue'
import Footer from './components/common/Header.vue'

export default {
  name: 'App',
  components: {
    HelloWorld,
    Header,
    Footer
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
<pre><code>* 실행 시 Error 발생, Header, Footer를 Multi-Word로 지정하지 않아서 발생, AppHeader, AppFooter로 변경
![](https://velog.velcdn.com/images/comet_strike/post/45f1e75c-a3a8-44bf-a7ac-dbabf4a7bd80/image.png)

* App.vue 수정 코드
``` javascript
&lt;template&gt;
  &lt;div&gt;
    &lt;AppHeader&gt;&lt;/AppHeader&gt;
    &lt;HelloWorld msg=&quot;Welcome to Your Vue.js App&quot;/&gt;
    &lt;AppFooter&gt;&lt;/AppFooter&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import HelloWorld from &#39;./components/HelloWorld.vue&#39;
import AppHeader from &#39;./common/AppHeader.vue&#39;
import AppFooter from &#39;./common/AppFooter.vue&#39;

export default {
  name: &#39;App&#39;,
  components: {
    HelloWorld,
    AppHeader,
    AppFooter
  }
}
&lt;/script&gt;

&lt;style&gt;
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
&lt;/style&gt;</code></pre><blockquote>
<p><a href="https://onethejay.tistory.com/65">https://onethejay.tistory.com/65</a>
<a href="https://blog.naver.com/PostView.naver?blogId=masichyun77&amp;logNo=222440885802">https://blog.naver.com/PostView.naver?blogId=masichyun77&amp;logNo=222440885802</a></p>
</blockquote>
<h1 id="레이아웃-구성-완료">레이아웃 구성 완료</h1>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/0edc8611-2e74-4e6a-aec3-e702f499ce9f/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1. toy-project 환경 구성]]></title>
            <link>https://velog.io/@comet_strike/toy-project-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1</link>
            <guid>https://velog.io/@comet_strike/toy-project-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1</guid>
            <pubDate>Wed, 02 Aug 2023 13:07:33 GMT</pubDate>
            <description><![CDATA[<p>SSAFY에서 2번의 프로젝트를 하면서 기술 부채가 쌓이는 것을 느껴서 개인적인 사이드 프로젝트를 진행하면서 하나하나 되짚어볼 생각이다.</p>
<p>당장 아이디어가 생각나지 않아 우선은 게시판 만들기부터 수행하면서 아이디어를 차근차근 해결 할 생각이다.</p>
<h1 id="project-구성">Project 구성</h1>
<h3 id="기술-스택-선정">기술 스택 선정</h3>
<ul>
<li>Learning curve를 줄이고 기술 부채를 갚기 위함이므로 새로 기술 스택을 배우는 것 보다는 기존에 알고 있는 기술을 사용하는 것을 선택했다.</li>
<li>DB의 경우에는 MySQL, MSSQL, MariaDB, Oracle 중에서 선택할 예정. 가장 많이 사용하고 경험이 없는 Oracle DB를 사용할지, 익숙한 Maria DB를 사용할 지 고민을 좀 더 해봐야 겠다.
Back :Java JPA SpringBoot
Data: Oracle 개발자 무료, Redis
Arch : Apache Kafka
Front : Vue</li>
</ul>
<h3 id="1-git-hub">1. Git Hub</h3>
<blockquote>
<p><a href="https://github.com/Fortuna3Co/Toy-Project">https://github.com/Fortuna3Co/Toy-Project</a></p>
</blockquote>
<ul>
<li>git hub에 repository 생성</li>
<li>git flow 형태로 구성 </li>
</ul>
<h3 id="폴더-구성">폴더 구성</h3>
<ul>
<li>feature에 push
<img src="https://velog.velcdn.com/images/comet_strike/post/3df00162-793e-40f9-abd6-b022d2a3d757/image.png" alt=""></li>
</ul>
<h3 id="vue-project-생성">Vue Project 생성</h3>
<ul>
<li><p>Front 폴더 안에서 CMD 실행</p>
</li>
<li><p><code>vue create toy-project</code> 프로젝트 생성</p>
<ul>
<li>Vue 3로 생성<blockquote>
<p>참고 :  <a href="https://rosedaily101.tistory.com/31">https://rosedaily101.tistory.com/31</a></p>
</blockquote>
</li>
</ul>
</li>
<li><p>생성한 프로젝트에서 VScode 실행</p>
</li>
<li><p>Ctrl + ` 키를 눌러 터미널 실행
<img src="https://velog.velcdn.com/images/comet_strike/post/479bc067-14dc-40ef-9c16-3bc72a056762/image.png" alt=""></p>
</li>
<li><p>터미널 창에 <code>npm run serve</code> 입력 후 url에 <code>ctrl + click</code>
<img src="https://velog.velcdn.com/images/comet_strike/post/d883ff77-b407-4076-b7ab-68e3b1d519c3/image.png" alt=""></p>
</li>
<li><p>Vue 프로젝트 생성 확인 완료</p>
</li>
</ul>
<h3 id="spring-boot-project-생성">Spring Boot Project 생성</h3>
<ul>
<li>start.spring.io 사이트를 이용해서 프로젝트 생성<blockquote>
<p><a href="https://start.spring.io/">https://start.spring.io/</a></p>
</blockquote>
</li>
<li>구성은 아래 사진과 같다. Dependecies의 경우 필요한 만큼 추가<blockquote>
<p>Dependecies 설명 : <a href="https://appleg1226.tistory.com/11">https://appleg1226.tistory.com/11</a>
<img src="https://velog.velcdn.com/images/comet_strike/post/b0dd9e33-b9e4-4c35-bf9c-03f0a13daae0/image.png" alt=""></p>
</blockquote>
</li>
<li>설정 후 Generate를 클릭해 다운로드, 해당 파일 Back 폴더로 이동 후 압축 풀기
<img src="https://velog.velcdn.com/images/comet_strike/post/51eca540-2f56-4cce-a2d5-8b91fbf89d7b/image.png" alt=""></li>
<li>toy-project 폴더에 들어가서 프로젝트 실행, 빌드되고 있는 것을 알 수 있다.
<img src="https://velog.velcdn.com/images/comet_strike/post/5bc6c9ad-17d7-4898-9a12-03ad9b89a67a/image.png" alt=""></li>
<li>FAILURE 발생
<img src="https://velog.velcdn.com/images/comet_strike/post/b8d03b13-aadb-46b7-a9ca-19f71872709b/image.png" alt=""></li>
<li>구글링한 결과 Spring 3.X 버전 부터는 Java 17이 필수라 되어 있어서 Java 17 설치<blockquote>
<p><a href="https://jojoldu.tistory.com/698">https://jojoldu.tistory.com/698</a>
java 17 : <a href="https://www.oracle.com/java/technologies/downloads/#jdk17-windows">https://www.oracle.com/java/technologies/downloads/#jdk17-windows</a></p>
</blockquote>
</li>
<li>Intellij 17버전으로 Gradle 변경
<img src="https://velog.velcdn.com/images/comet_strike/post/ed9efe73-03b4-4115-ac68-83ba8176bb5f/image.png" alt=""></li>
<li>Project Structure &gt; project 변경
<img src="https://velog.velcdn.com/images/comet_strike/post/aa343e75-a9c6-462b-8067-42fbe04d716f/image.png" alt=""></li>
<li>Project Structure &gt; Modules 변경
<img src="https://velog.velcdn.com/images/comet_strike/post/4b1ac323-fc38-4e3c-925d-4c85ff54a5ab/image.png" alt=""></li>
<li>Rebuild 수행 결과 Build Success
<img src="https://velog.velcdn.com/images/comet_strike/post/7c1ee496-b108-46f4-b9f4-f9acae8687f4/image.png" alt=""></li>
<li>Application 실행, Tomcat started on port(s): 8080 (http) 확인 완료
<img src="https://velog.velcdn.com/images/comet_strike/post/28699bdf-2d3b-4622-a67f-eb6fcf867ad1/image.png" alt=""></li>
</ul>
<h1 id="project-환경">Project 환경</h1>
<ul>
<li><p>Vue 3.3.4</p>
</li>
<li><p>node 14.21.1</p>
</li>
<li><p>npm 6.14.17</p>
</li>
<li><p>Java 17 </p>
</li>
<li><p>Spring Boot 3.1.2</p>
<ul>
<li>Dependencies</li>
<li>Spring Web</li>
<li>Lombok</li>
<li>Spring configuration Processor</li>
<li>Spring Security</li>
<li>OAuth2 Client</li>
<li>OAuth2 Resource Server</li>
<li>Spring Data JPA</li>
<li>H2 Database</li>
<li>Spring for Apache Kafka</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Apache Kafka 기술 공유]]></title>
            <link>https://velog.io/@comet_strike/Apache-Kafka-%EA%B8%B0%EC%88%A0-%EA%B3%B5%EC%9C%A0</link>
            <guid>https://velog.io/@comet_strike/Apache-Kafka-%EA%B8%B0%EC%88%A0-%EA%B3%B5%EC%9C%A0</guid>
            <pubDate>Wed, 26 Apr 2023 08:17:22 GMT</pubDate>
            <description><![CDATA[<h1 id="apache-kafka">Apache Kafka</h1>
<ul>
<li>Spring Boot Project에서 활용되는 Apache Kafka</li>
<li>APache Kafka에 대한 개요를 설명한다.</li>
<li>이후 Messaging System에 대해서 간략하게 짚고,</li>
<li>프로젝트에서 어떻게 활용했는지 설명한다.</li>
<li>해당 이해들을 바탕으로 Apache Kafka에 대해서 자세하게 설명한다.</li>
</ul>
<h1 id="apache-kafka란">Apache Kafka란?</h1>
<ul>
<li>아파치 카프카(Apache Kafka)는 아파치 소프트웨어 재단이 스칼라로 개발한 오픈 소스 메시지 브로커 프로젝트이다.</li>
<li>오픈 소스 분산 이벤트 스트리밍 플랫폼</li>
</ul>
<p>참고자료 </p>
<blockquote>
<p><a href="https://ko.wikipedia.org/wiki/%EC%95%84%ED%8C%8C%EC%B9%98_%EC%B9%B4%ED%94%84%EC%B9%B4">https://ko.wikipedia.org/wiki/%EC%95%84%ED%8C%8C%EC%B9%98_%EC%B9%B4%ED%94%84%EC%B9%B4</a>
<a href="https://kafka.apache.org/">https://kafka.apache.org/</a></p>
</blockquote>
<h1 id="kafka-탄생">Kafka 탄생</h1>
<ul>
<li>카프카는 2011년 미국 링크드인에서 출발</li>
<li>카프카는 링크드인 웹사이트에서 생성되는 로그를 처리하여 웹사이트 활동을 추적하는 것을 목적으로 개발</li>
<li>웹에서 생성되는 대량의 로그를 분석하여 사용자가 웹에서 하는 활동을 모니터링하고 서비스 개선에 활용하는 목적</li>
<li>링크드인의 실현 목적 ( 요구 사항)<pre><code>➊ 높은 처리량으로 실시간 처리한다.
➋ 임의의 타이밍에서 데이터를 읽는다.
➌ 다양한 제품과 시스템에 쉽게 연동한다.
➍ 메시지를 잃지 않는다.</code></pre></li>
<li>실현 수단<pre><code>➊ 메시징 모델과 스케일 아웃형 아키텍처
➋ 디스크로의 데이터 영속화
➌ 이해하기 쉬운 API 제공
➍ 전달 보증</code></pre></li>
</ul>
<p>참고 자료</p>
<blockquote>
<p><a href="https://www.hanbit.co.kr/media/channel/view.html?cms_code=CMS9400468504">https://www.hanbit.co.kr/media/channel/view.html?cms_code=CMS9400468504</a></p>
</blockquote>
<ul>
<li>요약 : 확장 가능하고 안정성이 있으며 호환성이 좋은 메시징 모델이 없어서 Kafka를 개발했다.</li>
</ul>
<h1 id="apache-kafka-이해하기">Apache Kafka 이해하기</h1>
<ul>
<li>Apache Kafka는 분산 스트리밍 플랫폼이다.</li>
<li>데이터 파이프 라인을 만들 때 주로 사용되는 오픈소스 솔루션이다.</li>
<li>대용량의 실시간 로그처리에 특화되어 있는 솔루션이다.</li>
<li>데이터를 유실없이 안전하게 전달하는 것이 주목적인 메세지 시스템에서 Fault-Tolerant한 안정적인 아키텍처와 빠른 퍼포먼스로 데이터를 처리한다.</li>
</ul>
<p>=&gt; Kafka는 분산 데이터 처리 환경에 적합하다. 실시간 로그 처리에 적합하다. 메시징 시스템이며, 빠른 퍼포먼스로 데이터를 처리한다.</p>
<h2 id="메시징-시스템-messaging-system-이란">메시징 시스템, Messaging System 이란?</h2>
<ul>
<li>메시징 시스템으로는 Kafka, RabbitMQ, Active MQ, AWS SQS, Java JMS 등이 있다.</li>
<li>메시지를 문자, 이메일이라 생각하면 안됨.</li>
<li>여기서 메시지는 로그 데이터, 데이터, 이벤트 메시지 등 API로 호출할 때 보내는 데이터드르을 처리하는 시스템.</li>
</ul>
<p>메시징 시스템 참고자료</p>
<blockquote>
<p><a href="https://victorydntmd.tistory.com/343">https://victorydntmd.tistory.com/343</a></p>
</blockquote>
<p>메시지, 메시징에 대한 참고자료</p>
<blockquote>
<p><a href="https://smallake.kr/?p=2139">https://smallake.kr/?p=2139</a></p>
</blockquote>
<ul>
<li>메시징 시스템은 서로 다른 프로그램 끼리 정보를 교환하기 위해 통합채널로 활용한다.</li>
</ul>
<blockquote>
<p><a href="https://velog.io/@pood/%EB%A9%94%EC%8B%9C%EC%A7%95-%EC%8B%9C%EC%8A%A4%ED%85%9C">https://velog.io/@pood/%EB%A9%94%EC%8B%9C%EC%A7%95-%EC%8B%9C%EC%8A%A4%ED%85%9C</a></p>
</blockquote>
<h2 id="zzalu-project에서-kafka가-사용된-방식">Zzalu Project에서 Kafka가 사용된 방식</h2>
<ul>
<li>실제 진행한 Zzalu Project에서 Kafka가 어떤 방식으로 사용되었는지 설명한다.</li>
</ul>
<p>프로젝트 Git Hub</p>
<blockquote>
<p><a href="https://github.com/Zzalu/Zzalu">https://github.com/Zzalu/Zzalu</a></p>
</blockquote>
<p>프로젝트 Git Hub - 프로젝트 구조 / 시스템 흐름</p>
<blockquote>
<p><a href="https://github.com/Zzalu/Zzalu/tree/main/exec">https://github.com/Zzalu/Zzalu/tree/main/exec</a></p>
</blockquote>
<h3 id="프로젝트-apache-kafka-사용-이유">프로젝트 Apache Kafka 사용 이유</h3>
<ul>
<li>우선 해당 프로젝트에서는 채팅 시스템을 개발하기 위해서 Apache Kafka를 사용했다.</li>
<li>채팅 시스템을 구현하기 위해서, <code>Web Socket/STOMP</code>, <code>Redis</code>, <code>Apache Kafka</code>를 사용했다.</li>
<li>채팅 시스템에서 Kafka를 사용하는 이유는 안정성과 확장성이다.</li>
</ul>
<ol>
<li>채팅 메세지 순서 보장</li>
<li>모든 내역을 로그로 기록</li>
<li>서버에 장애가 생길 경우 Kafka가 해당 메세지를 다른 서버로 보내거나, 가지고 있다가 복구되는데로 처리한다.</li>
</ol>
<p>자세한 설명이 필요하면 아래 자료 참고</p>
<blockquote>
<p><a href="https://velog.io/@comet_strike/Kafka-Redis-Web-Socket-Stomp-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%B1%84%ED%8C%85-%EC%84%9C%EB%B2%84-%ED%9A%8C%EA%B3%A0">https://velog.io/@comet_strike/Kafka-Redis-Web-Socket-Stomp-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%B1%84%ED%8C%85-%EC%84%9C%EB%B2%84-%ED%9A%8C%EA%B3%A0</a></p>
</blockquote>
<h2 id="프로젝트에서-실제-처리-과정">프로젝트에서 실제 처리 과정</h2>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/8d920ecc-0198-410c-b4b2-0db453d54610/image.png" alt=""></p>
<ul>
<li>사용자가 특정 Event를 발생시킬 경우 처리 과정을 설명한다.</li>
<li>해당 프로젝트에서는 채팅 메세지를 보낼경우 Kafka를 이용해 처리하므로 채팅 메세지를 보냈을 경우에 처리 방식을 설명한다.</li>
</ul>
<ol>
<li>사용자가 채팅방에 입장할 때, Server와 Client는 Web Socket으로 연결되어 있다.</li>
<li>사용자가 채팅 메세지를 보낼 경우, Front에서 특정 URL로 메세지를 보낸다.</li>
<li>Spring Boot Server에서 해당 메세지를 수신한다.</li>
<li>수신한 메세지를 Kafka Producer를 이용해서 특정 Topic으로 발행한다.</li>
<li>Kafka Server는 이를 수신하고 Kafka Consumer에 전달한다.
(Kafka Consumer는 Spring Boot Server에서 @KafaListner 어노테이션을 이용해서 구현한다.)</li>
<li>Kafka Consumer는 받은 메세지를 Redis로 발행한다.</li>
<li>Redis SUB에서는 STOMP를 통해 채팅 메세지를 수신한다.</li>
</ol>
<h2 id="kafka-사용하는-서비스">Kafka 사용하는 서비스</h2>
<ul>
<li><p>Line</p>
<blockquote>
<p> <a href="https://engineering.linecorp.com/ko/blog/how-to-use-kafka-in-line-1">https://engineering.linecorp.com/ko/blog/how-to-use-kafka-in-line-1</a></p>
</blockquote>
</li>
<li><p>RIDI</p>
<blockquote>
<p><a href="https://ridicorp.com/story/how-to-use-kafka-in-ridi/">https://ridicorp.com/story/how-to-use-kafka-in-ridi/</a></p>
</blockquote>
</li>
<li><p>Kakao Alex</p>
<blockquote>
<p><a href="https://tech.kakao.com/2020/06/08/websocket-part1/">https://tech.kakao.com/2020/06/08/websocket-part1/</a></p>
</blockquote>
</li>
</ul>
<h3 id="line에서-kafka">Line에서 Kafka</h3>
<ul>
<li>LINE에서 Kafka를 사용하는 방법은 크게 두 가지</li>
</ul>
<ol>
<li>단순하게 분산 큐잉 시스템으로 사용</li>
<li>데이터 허브로 사용</li>
</ol>
<h3 id="ridi에서-kafka">RIDI에서 Kafka</h3>
<ul>
<li>리디에서는 다양한 사업적 요구를 분석하기 위해서 여러 종류의 분석 플랫폼들을 활용하고 있는데, 리디 서비스와 이러한 플랫폼들 간의 데이터 통로로도 Kafka를 활용</li>
</ul>
<h3 id="kakao-alex에서-kafka">Kakao Alex에서 Kafka</h3>
<ul>
<li>메시지 브로커로 카프카를 사용</li>
</ul>
<h2 id="kafka-설명-요약">Kafka 설명 요약</h2>
<ul>
<li>Kafka는 링크드인에서 개발한 Messaging System이다.</li>
<li>Kafka는 여러 방식으로 사용된다.</li>
<li>Kafka를 데이터의 중간 통로로 이용한다.</li>
</ul>
<h1 id="kafka-구성">Kafka 구성</h1>
<p>참고자료 </p>
<blockquote>
<p><a href="https://hoing.io/archives/5108">https://hoing.io/archives/5108</a></p>
</blockquote>
<h1 id="실제-spring-boot-프로젝트에-적용하는-방법">실제 Spring Boot 프로젝트에 적용하는 방법</h1>
<h2 id="1-docker를-이용한-kafka-설치">1. Docker를 이용한 Kafka 설치</h2>
<ul>
<li><p>Kafka, Zookeeper 설치</p>
</li>
<li><p>Git Clone <code>https://github.com/wurstmeister/kafka-docker.git</code></p>
</li>
<li><p>클론한 폴더에서 docker-compose.yml 파일 수정</p>
<pre><code>version: &#39;2&#39;
services:
zookeeper:
  image: wurstmeister/zookeeper
  container_name: zookeeper
  ports:
    - &quot;2181:2181&quot;
  restart: unless-stopped

kafka:
  build: .
  image: wurstmeister/kafka
  container_name: kafka
  ports:
    - &quot;9092:9092&quot;
  environment:
    DOCKER_API_VERSION: 1.22
    KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1
    KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
    KAFKA_MESSAGE_MAX_BYTES: 10000000
    KAFKA_AUTO_CREATE_TOPICS_ENABLE: &#39;true&#39;
    KAFKA_DELETE_TOPIC_ENABLE: &#39;true&#39;
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock
  restart: unless-stopped
</code></pre></li>
</ul>
<pre><code>* 해당 폴더에서 터미널 실행 `docker-compose up -d`



## 2. Spring Boot와 연결
* application.properties에 kafka 내용 추가</code></pre><p>spring.kafka.consumer.bootstrap-servers: localhost:9092
spring.kafka.consumer.group-id: foo
spring.kafka.consumer.auto-offset-reset: earliest
spring.kafka.consumer.key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.producer.bootstrap-servers: localhost:9092
spring.kafka.producer.key-serializer: org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer: org.apache.kafka.common.serialization.StringSerializer</p>
<pre><code>
참고자료
&gt; https://velog.io/@taehodot/SpringBoot-%EC%B9%B4%ED%94%84%EC%B9%B4%EC%99%80-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%97%B0%EB%8F%99


## 3. 실제 사용 코드

1. 사용자가 채팅 메세지를 보낼경우 수신 받은 후, Kafkao Producer로 발행
``` java
    @MessageMapping(&quot;/chat/message&quot;)
    public void message(ChatMessageDto message) {

        // 토큰 검사 =&gt; 에외 발생 시 Exception
        Member requestMember = jwtTokenProvider.getMember(message.getSender());
        message.setSender(requestMember.getNickname());
        message.setMemberName(requestMember.getUsername());
        message.setProfilePath(requestMember.getProfilePath());
        message.setMemberId(requestMember.getId());
        message.setSendDate(LocalDateTime.now());

        if (ChatMessageDto.MessageType.ENTER.equals(message.getType())) {
            chatRoomRedisRepository.enterChatRoom(message.getRoomId());
        }

        // kafka topic 발행
        kafkaProducer.sendMessage(message);
        // 입장이 아닐때만 저장
        if (!ChatMessageDto.MessageType.ENTER.equals(message.getType())) {
            chatRoomRedisRepository.setChatMessage(message, message.getRoomId());
        }
//        redisPublisher.publish(chatRoomRepository.getTopic(message.getRoomId()), message);
    }</code></pre><p>아래 Kafka Producer 코드 참고</p>
<pre><code class="language-java">@Service
public class KafkaProducer {

    private static final String TOPIC = &quot;exam&quot;;
    private final KafkaTemplate&lt;String, Object&gt; kafkaTemplate;

    @Autowired
    public KafkaProducer(KafkaTemplate kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void sendMessage(Object message){
        this.kafkaTemplate.send(TOPIC, message);
    }

}</code></pre>
<ol start="2">
<li>Kafka Consumer / Redis Publisher 설정</li>
</ol>
<pre><code class="language-java">    @KafkaListener(topics=&quot;exam&quot;, groupId = &quot;foo&quot;)
    public void kafkaListener(String message) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        ChatMessageDto chatMessageDto = objectMapper.readValue(message, ChatMessageDto.class);
        System.out.println(&quot;publish : &quot; + chatMessageDto.toString());
        redisTemplate.convertAndSend(((ChannelTopic) chatRoomRedisRepository.getTopic(chatMessageDto.getRoomId())).getTopic(), chatMessageDto);
    }
</code></pre>
<ul>
<li>@KafkaListener 어노테이션을 이용해서 SUB 처리</li>
<li>redisTemplate.convertAndSend로 PUB 처리</li>
</ul>
<ol start="3">
<li>Redis Subscribe 설정 (Topic에 따라 다르게 처리), STOMP PUB 처리</li>
</ol>
<pre><code class="language-java"> @Override
    public void onMessage(Message message, byte[] pattern) {
        try {

            String topic = (String) redisTemplate.getStringSerializer().deserialize(message.getChannel());
            String publishMessage = (String) redisTemplate.getStringSerializer().deserialize(message.getBody());

            if(topic.equals(&quot;comments&quot;)){
                System.out.println(&quot;comments&quot;);
                CommentResponse commentResponse =  objectMapper.readValue(publishMessage, CommentResponse.class);
                messagingTemplate.convertAndSend(&quot;/sub/title-hakwon/comments/&quot;, commentResponse);
            }else if (topic.equals(&quot;likes&quot;)){
                LikeResponse likeResponse = objectMapper.readValue(publishMessage, LikeResponse.class);
                messagingTemplate.convertAndSend(&quot;/sub/title-hakwon/comments/likes&quot;, likeResponse);
            }else{
                ChatMessageDto roomMessage = objectMapper.readValue(publishMessage, ChatMessageDto.class);
                messagingTemplate.convertAndSend(&quot;/sub/chat/room/&quot; + roomMessage.getRoomId(), roomMessage);
            }

        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }</code></pre>
<p>순서 요약 :
메세지 수신 시, 메세지 처리 후 Kafka Producer를 통해 Kafka Server로 전달.
이후 Consumer를 통해 메세지 처리. Consumer는 Redis Publish 수헹.
Redis Subscribe에서 해당 메세지를 받은 후, STOMP Publish 수행.
STOMP Subscribe는 Client이므로 메세지가 Client에 정상적으로 전달.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[EC2, Jenkins를 활용한 CI/CD 구축 기술공유]]></title>
            <link>https://velog.io/@comet_strike/CICD-Apache-Kafka-Redis-%EA%B8%B0%EC%88%A0-%EA%B3%B5%EC%9C%A0</link>
            <guid>https://velog.io/@comet_strike/CICD-Apache-Kafka-Redis-%EA%B8%B0%EC%88%A0-%EA%B3%B5%EC%9C%A0</guid>
            <pubDate>Wed, 26 Apr 2023 06:01:11 GMT</pubDate>
            <description><![CDATA[<h1 id="cicd">CI/CD</h1>
<ul>
<li>AWS EC2, Git Lab, Jenkins 환경에서 CI/CD 구성방법.</li>
</ul>
<h2 id="cicd란">CI/CD란?</h2>
<ul>
<li>CI : Continuous Integration 지속적인 통합</li>
<li>CD : Continous Delivery 지속적인 제공 (Deployment로 사용하는 경우가 있다.)</li>
<li>CI/CD는 애플리케이션 개발 단계 부터 배포 단계 까지를 자동화하여 애플리케이션을 더욱 짧은 주기로 고객에게 제공하는 방법</li>
</ul>
<p>CI/CD 개념 참고자료</p>
<blockquote>
<p><a href="https://www.redhat.com/ko/topics/devops/what-is-ci-cd">https://www.redhat.com/ko/topics/devops/what-is-ci-cd</a>
<a href="https://www.youtube.com/watch?v=0Emq5FypiMM&amp;t=60s">https://www.youtube.com/watch?v=0Emq5FypiMM&amp;t=60s</a></p>
</blockquote>
<p>CI/CD 구축 방법 참고자료</p>
<blockquote>
<p> <a href="https://dramatic-armchair-97f.notion.site/e85a109829c94c9ea5c841ca8e852ac5">https://dramatic-armchair-97f.notion.site/e85a109829c94c9ea5c841ca8e852ac5</a></p>
</blockquote>
<h1 id="aws에-cicd-구축-하는-방법">AWS에 CI/CD 구축 하는 방법</h1>
<ul>
<li>순서는 다음과 같다.<ol>
<li>EC2 Instance에 Jenkins 설치</li>
<li>Jenkins에 접속해 Git Lab과 Web Hook 연동</li>
<li>Jenkins에 Pipeline Script를 이용해 자동 빌드 및 배포 구현</li>
</ol>
</li>
</ul>
<h2 id="span-stylecolor-crimson1-ec2-instance에-jenkins-설치-span"><span style="color: crimson">1. EC2 Instance에 Jenkins 설치 </span></h2>
<ul>
<li>Docker를 이용해서 설치한다.</li>
</ul>
<ol>
<li>Docker 설치는 공식 Docs 참고 (Install Docker Engine on Ubuntu)<blockquote>
<p><a href="https://docs.docker.com/engine/install/ubuntu/">https://docs.docker.com/engine/install/ubuntu/</a></p>
</blockquote>
</li>
<li>EC2에 접속한 후 Docker를 이용해 Jenkins를 설치한다.
포트의 경우에는 기본으로 8081로 설정했지만, 프로젝트 환경에 맞게 구성하면 된다.<pre><code class="language-bash">sudo docker run --name jenkins -d -p 8081:8080 -p 50000:50000 jenkins/jenkins:lts</code></pre>
</li>
</ol>
<h2 id="span-stylecolor-crimson2-jenkins에-접속해-git-lab과-web-hook-연동span"><span style="color: crimson">2. Jenkins에 접속해 Git Lab과 Web Hook 연동</span></h2>
<ul>
<li>젠킨스로 접속한다. 웹 브라우저에 <code>http://[IP]:8081</code>를 입력해서 접속 가능하다.</li>
<li>접속 Password는 EC2에 <code>sudo docker logs jenkins</code>를 입력해서 찾을 수 있다.</li>
<li>추천 pulgin을 설치한다.</li>
</ul>
<h3 id="jenkins-플러그인-설치">Jenkins 플러그인 설치</h3>
<ul>
<li>Plugin Manager를 통해서 Avaliable pugin을 설치한다.<ol>
<li>Gitlab</li>
<li>Publish Over SSH</li>
<li>Mattermost Notification(MM으로 알림 보낼 경우만 설치)</li>
<li>docker</li>
<li>node.js</li>
</ol>
</li>
</ul>
<h3 id="jenkins-publish-over-ssh-설정">Jenkins Publish Over SSH 설정</h3>
<ul>
<li>Jenkins와 EC2의 SSH 연결을 설정한다.</li>
<li>젠킨스 관리 &gt; Configure System 으로 들어간다.</li>
<li>Publish Over SSH에 .pem키를 붙여넣는다.</li>
<li>SSH Server를 설정한다.</li>
<li>Remote Dircetory의 경우에는 연결 대상에 미리 존재해야한다.
(아래의 경우에은 /home/ubuntu/jenkins_build가 remote directory이므로 EC2에 /home/ubuntu/jenkins_build가 존재해야 한다. EC2에서 mkdir 명령어를 통해서 디렉토리 생성 가능하다.)<pre><code>Name : EC2_Instance
Hostname : [EC2 domain] 
Username : ubuntu
Remote Directory : /home/ubuntu/jenkins_build </code></pre></li>
</ul>
<h3 id="ec2-설정">EC2 설정</h3>
<h4 id="ssh-연결을-설정한다">SSH 연결을 설정한다.</h4>
<ul>
<li>/etc/ssh/sshd_config에 vim을 통해 아래 2줄 추가한다.
<code>sudo vim /etc/ssh/sshd_config</code><pre><code>  PubkeyAuthentication yes
  PubkeyAcceptedKeyTypes +ssh-rsa</code></pre></li>
<li><code>sudo service sshd restart</code>를 입력해서 ssh 서비스를 재시작한다.</li>
</ul>
<h4 id="ssh-연결에-사용할-directory를-생성한다">SSH 연결에 사용할 Directory를 생성한다.</h4>
<pre><code>mkdir ~/jenkins_build</code></pre><h4 id="연결-확인">연결 확인</h4>
<ul>
<li>Jenkins, EC2 모두 설정한 후, Jenkins에 Publish Over SSH 설정 화면에서 오른쪽 하단 Test Configuration 클릭한다.</li>
</ul>
<h2 id="span-stylecolor-crimson3-jenkins에-pipeline-script를-이용해-자동-빌드-및-배포-구현span"><span style="color: crimson">3. Jenkins에 Pipeline Script를 이용해 자동 빌드 및 배포 구현</span></h2>
<ul>
<li>Jenkiins에 Script를 설정해 Build 및 Deploy를 수행한다. </li>
<li>Jenkins에서 Build하기 위해 Java와 NodeJS를 설치한다.</li>
<li>Build된 결과를 EC2에 전달하고, 원격으로 해당 결과를 실행시킨다.</li>
<li>빌드되는 순서는 다음과 같다.<ol>
<li>Jenkins에서 Vue 프로젝트를 clone해 온다.</li>
<li>npm install 명령어를 통해 빌드한다.</li>
<li>Spring Boot 프로젝트를 clone해 온다.</li>
<li>생성된 결과를 Spring Boot 프로젝트에 옮긴다.</li>
<li>Spring Boot 프로젝트를 빌드한다.</li>
<li>빌드한 결과를 EC2에 전달한다.</li>
<li>기존 서버를 없앤 후, 새로 빌드한 프로젝트를 구동한다. </li>
</ol>
</li>
</ul>
<h3 id="span-stylecolor-green1-jenkins-환경-구성span"><span style="color: green">1. Jenkins 환경 구성</span></h3>
<ul>
<li>npm을 사용하기 위해 nodeJS를 설치한다.</li>
<li>Spring Project를 빌드하기 위해 Java를 설치한다.</li>
</ul>
<h4 id="1-환경-설정">1. 환경 설정</h4>
<ol>
<li><p>apt 업데이트, sudo, vim 설치</p>
<pre><code>apt-get update
apt-get install sudo
apt-get install vim</code></pre></li>
<li><p>sudo 설정, 설정 파일 열기
<code>sudo visudo</code></p>
</li>
<li><p>해당 파일에 아래 내용 추가
<code>jenkins ALL=(ALL) NOPASSWD: ALL</code></p>
</li>
</ol>
<h4 id="2-nodejs-설치">2. NodeJS 설치</h4>
<ol>
<li><p>EC2에 접속한 후, jenkins에 접속하는 명령어를 수행한다.
<code>sudo docker exec -it -u root jenkins /bin/bash</code></p>
</li>
<li><p>Jenkins에 NVM을 설치한다.
<code>curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash</code></p>
</li>
<li><p>NVM을 활성화한다. <code>. ~/.nvm/nvm.sh</code></p>
</li>
<li><p>14버전을 설치한다. (프로젝트에 맞는 버전으로 설치)
<code>nvm install 14</code></p>
</li>
<li><p>모든유저에게 적용한다.
<code>n=$(which node);n=${n%/bin/node}; chmod -R 755 $n/bin/*; cp -r $n/{bin,lib,share} /usr/local</code></p>
</li>
</ol>
<h4 id="3-java-설치">3. Java 설치</h4>
<ol>
<li>EC2에 접속한 상태라 가정</li>
<li>java를 설치하는 명령어 실행
<code>sudo apt-get install openjdk-11-jdk</code></li>
</ol>
<h3 id="span-stylecolor-green2--jenkins-pipeline-생성span"><span style="color: green">2 . Jenkins Pipeline 생성</span></h3>
<ul>
<li>참고 노션 [7. 젠킨스 파이프라인 생성] 참고<blockquote>
<p><a href="https://dramatic-armchair-97f.notion.site/e85a109829c94c9ea5c841ca8e852ac5">https://dramatic-armchair-97f.notion.site/e85a109829c94c9ea5c841ca8e852ac5</a></p>
</blockquote>
</li>
</ul>
<h3 id="span-stylecolor-green3-webhook-생성span"><span style="color: green">3. Webhook 생성</span></h3>
<p>참고자료</p>
<blockquote>
<p><a href="https://junhyunny.github.io/information/jenkins/github/jenkins-github-webhook/">https://junhyunny.github.io/information/jenkins/github/jenkins-github-webhook/</a></p>
</blockquote>
<ol>
<li>Gitlab Webhook 설정, Token 생성</li>
<li>Jenkins 빌드 트리거 설정, Webhook 설정</li>
</ol>
<h3 id="span-stylecolor-green4-script-작성span"><span style="color: green">4. Script 작성</span></h3>
<ul>
<li><p>참고 노션 [8. script 작성] 참고</p>
<blockquote>
<p><a href="https://dramatic-armchair-97f.notion.site/e85a109829c94c9ea5c841ca8e852ac5">https://dramatic-armchair-97f.notion.site/e85a109829c94c9ea5c841ca8e852ac5</a></p>
</blockquote>
</li>
<li><p>실제 작성한 스크립트를 기반으로 설명할 예정.</p>
</li>
<li><p>전체 스크립트는 아래 참고</p>
<pre><code>node {  

  stage(&#39;Build&#39;) { 
      checkout scmGit(branches: [[name: &#39;*/dev&#39;]], extensions: [], userRemoteConfigs: [[credentialsId: &#39;comet&#39;, url: &#39;https://lab.ssafy.com/s08-webmobile2-sub2/S08P12C109&#39;]])
     dir(&quot;front&quot;) {
         sh &quot;pwd&quot;
         sh &quot;sudo npm install&quot;
         sh &quot;sudo npm run build&quot;
         sh &quot;sudo cp -r /var/jenkins_home/workspace/zzalu_ci_cd_test/front/dist/* /var/jenkins_home/workspace/zzalu_ci_cd_test/back/src/main/resources/static&quot;
     }
     dir(&quot;back&quot;) {
         sh &quot;pwd&quot;
         sh &quot;sudo chmod +x gradlew &quot;
         sh &quot;sudo ./gradlew clean&quot;
         sh &quot;sudo ./gradlew build&quot;
     }

  }
  stage(&#39;Deploy&#39;) {
      dir(&quot;back/build/libs&quot;) {
          sshPublisher(publishers: [sshPublisherDesc(configName: &#39;zzalu_deploy_server_test&#39;, transfers: [sshTransfer(cleanRemote: false, excludes: &#39;&#39;, execCommand: &#39;&#39;&#39;cd /home/ubuntu/jenkins_build
          kill -9 `cat save_pid.txt`
          rm save_pid.txt
          sudo nohup java -jar zzalu-0.0.1-SNAPSHOT.jar &gt; logs/zzalu.log 2&gt;&amp;1 &amp; echo $! &gt; save_pid.txt&#39;&#39;&#39;, execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: &#39;[, ]+&#39;, remoteDirectory: &#39;&#39;, remoteDirectorySDF: false, removePrefix: &#39;&#39;, sourceFiles: &#39;*.jar&#39;)], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
          mattermostSend color: &#39;#32a852&#39;, message: &quot;Dev Branch Deploy End! (${env.JOB_NAME}) #(${env.BUILD_NUMBER}) (&lt;${env.BUILD_URL}|Open&gt;) \n See the (&lt;${env.BUILD_URL}console|console&gt;)&quot;
      }
  }
}</code></pre></li>
</ul>
<ol>
<li><p><code>checkout scmGit(branches: [[name: &#39;*/dev&#39;]], extensions: [], userRemoteConfigs: [[credentialsId: &#39;comet&#39;, url: &#39;https://lab.ssafy.com/xxxxxxxxxx/xxxxxx&#39;]])</code></p>
<ul>
<li>gitlab으로 clone하는 부분</li>
<li>branches: [[name: &#39;*/dev&#39;]] : dev 브랜치를 클론한다.</li>
<li>credentialsId에는 사용할 계정을 선택한다.</li>
<li>url로는 프로젝트의 gitlab 주소를 사용한다.</li>
</ul>
</li>
<li><p>dir(&quot;front&quot;) {</p>
<pre><code>    sh &quot;pwd&quot;
    sh &quot;sudo npm install&quot;
    sh &quot;sudo npm run build&quot;
    sh &quot;sudo cp -r /var/jenkins_home/workspace/zzalu_ci_cd_test/front/dist/* /var/jenkins_home/workspace/zzalu_ci_cd_test/back/src/main/resources/static&quot;
}
* front 빌드 시 수행할 영역
* dir은 해당 dir로 들어가서 수행한다는 의미
* 현재 프로젝트는 아래 캡처처럼 프로젝트가 구성되어 있다.
* dir(&quot;front&quot;) 를 통해서 front 디렉토리 안으로 들어간다는 의미
* front 폴더 안에 들어가서 npm install, build를 수행한다.
* 이후 빌드된 결과를 back/src/main/resorces/static에 복사한다.</code></pre></li>
</ol>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/69c60da6-b813-4992-bf82-e0be77b01c6c/image.png" alt=""></p>
<ol start="3">
<li><p>dir(&quot;back&quot;) {</p>
<pre><code>   sh &quot;pwd&quot;
   sh &quot;sudo chmod +x gradlew &quot;
   sh &quot;sudo ./gradlew clean&quot;
   sh &quot;sudo ./gradlew build&quot;</code></pre><p>   }</p>
<ul>
<li>dir(&quot;back&quot;)을 통해서 back 디렉토리로 들어간다.</li>
<li>이후 gradle을 이용해서 빌드한다.</li>
</ul>
</li>
<li><p>dir(&quot;back/build/libs&quot;)</p>
<ul>
<li>빌드 된 결과는 back/build/libs에 있으므로, 해당 디렉토리로 진입한다.</li>
</ul>
</li>
<li><p>sshPublisher</p>
<pre><code>sshPublisher(publishers: [sshPublisherDesc(configName: &#39;zzalu_deploy_server_test&#39;, transfers: [sshTransfer(cleanRemote: false, excludes: &#39;&#39;, execCommand: &#39;&#39;&#39;cd /home/ubuntu/jenkins_build
         kill -9 `cat save_pid.txt`
         rm save_pid.txt
         sudo nohup java -jar zzalu-0.0.1-SNAPSHOT.jar &gt; logs/zzalu.log 2&gt;&amp;1 &amp; echo $! &gt; save_pid.txt&#39;&#39;&#39;, execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: &#39;[, ]+&#39;, remoteDirectory: &#39;&#39;, remoteDirectorySDF: false, removePrefix: &#39;&#39;, sourceFiles: &#39;*.jar&#39;)], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
         mattermostSend color: &#39;#32a852&#39;, message: &quot;Dev Branch Deploy End! (${env.JOB_NAME}) #(${env.BUILD_NUMBER}) (&lt;${env.BUILD_URL}|Open&gt;) \n See the (&lt;${env.BUILD_URL}console|console&gt;)&quot;</code></pre></li>
</ol>
<ul>
<li>sshPublisherDesc(configName: &#39;zzalu_deploy_server_test&#39; EC2와 연결할 때 사용할 SSH, Publish On SSH에서 설정한 설정 이름을 사용  </li>
<li><code>execCommand:</code> 전송한 후 수행할 명령어들을 작성한다.<pre><code>위의 경우에서 전송후 특정 디렉토리로 이동 (cd), 
기존 프로세스를 종료(kill -9)한 후 
전송한 jar 파일을 실행(nohup)한다.</code></pre></li>
</ul>
<ol start="6">
<li>remoteDirectorySDF: false, removePrefix: &#39;&#39;, sourceFiles: &#39;*.jar&#39;)]</li>
</ol>
<ul>
<li>.jar인 파일을 모두 전송한다.</li>
</ul>
<ol start="7">
<li>mattermostSend color: &#39;#32a852&#39;, message: &quot;Dev Branch Deploy End! (${env.JOB_NAME}) #(${env.BUILD_NUMBER}) (&lt;${env.BUILD_URL}|Open&gt;) \n See the (&lt;${env.BUILD_URL}console|console&gt;)&quot;</li>
</ol>
<ul>
<li>mattermost로 특정 메시지를 전달한다.</li>
</ul>
<h1 id="trouble-shooting">Trouble Shooting</h1>
<ul>
<li>트러블 슈팅 하는 방법에 대해서 설명한다.</li>
</ul>
<h2 id="1-프로젝트가-정상-빌드-되었는지-확인하는-법">1. 프로젝트가 정상 빌드 되었는지 확인하는 법</h2>
<ul>
<li>프로젝트가 정상 반영되지 않았을 때, 확인하는 방법</li>
</ul>
<ol>
<li>Jenkins 웹에 접속해서 빌드 결과가 Success인지 확인한다.
=&gt; Fail일 경우 발생한 Error를 보고 판단한다.</li>
<li>EC2에 접속해서 Top 명령어를 친 후 jar 파일 구동 시간이 빌드한 시간과 일치하는 지 확인한다.
=&gt; 일치하지 않을 경우 프로세스가 정상적으로 종료되지 않았을 경우일 확률이 높다. Jenkins 스크립트의 kill -9 명령어가 정상적으로 작동되는지 확인한다.</li>
<li>Jenkins cli로 접속해서 jar 파일 변경시간이 올바른지 확인한다.
=&gt; 변경시간이 올바르지 않을 경우, Git에서 제대로 가져오는지 확인한다. 이후 Build에서 문제가 생기는지 CLI에서 직접 빌드해본다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[2023-03-17]]></title>
            <link>https://velog.io/@comet_strike/2023-03-17</link>
            <guid>https://velog.io/@comet_strike/2023-03-17</guid>
            <pubDate>Thu, 20 Apr 2023 02:50:11 GMT</pubDate>
            <description><![CDATA[<ul>
<li><p>docker-compose</p>
<blockquote>
<p><a href="https://github.com/ManduTheCat/docker-hadoop-spark">https://github.com/ManduTheCat/docker-hadoop-spark</a></p>
</blockquote>
</li>
<li><p>참고 페이지</p>
<blockquote>
<p><a href="https://github.com/dbusteed/kafka-spark-streaming-example">https://github.com/dbusteed/kafka-spark-streaming-example</a>
<a href="https://www.youtube.com/watch?v=9D7-BZnPiTY&amp;t=756s">https://www.youtube.com/watch?v=9D7-BZnPiTY&amp;t=756s</a></p>
</blockquote>
</li>
<li><p>window local 환경에서 진행</p>
</li>
<li><p>이후 AWS EC2에서 진행할 예정</p>
</li>
</ul>
<h1 id="kafka">kafka</h1>
<ol>
<li><p>kafka 진입
<code>docker exec -it kafka bash</code></p>
</li>
<li><p>apt 최신화 <code>apt-get update</code>, sudo 설치 <code>apt-get install sudo</code></p>
<ul>
<li>초기 진입 시 root이기 때문에 sudo는 필요없지만 sudo 명령어를 복사해서 바로 쓰기 위해서 sudo 설치함</li>
</ul>
</li>
<li><p>zookeeper 실행<code>/opt/kafka_2.13-2.8.1/bin/zookeeper-server-start.sh /opt/kafka_2.13-2.8.1/config/zookeeper.properties</code> =&gt; tab 이름 ZK</p>
</li>
<li><p>kafka 토픽 확인. 최초 확인 시 Topic 없음. tweets 토픽 생성 후 topic 확인 시 topci 확인 가능</p>
<pre><code>root@53642356f28f:/# /opt/kafka_2.13-2.8.1/bin/kafka-topics.sh --list --bootstrap-server localhost:9092
</code></pre></li>
</ol>
<p>root@53642356f28f:/# /opt/kafka_2.13-2.8.1/bin/kafka-topics.sh --create --bootstrap-server localhost:9092 \</p>
<blockquote>
<p>--replication-factor 1 <br>--partitions 1 <br>--topic tweets
Created topic tweets.
root@53642356f28f:/# /opt/kafka_2.13-2.8.1/bin/kafka-topics.sh --list --bootstrap-server localhost:9092
tweets
root@53642356f28f:/#</p>
</blockquote>
<pre><code>
## kafka producer / consumer 확인

* 윈도우 터미널 탭 2개로 진행 
* Producer Tab에 다음 명령어 수행
`/opt/kafka_2.13-2.8.1/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic tweets`
* consumber Tab에 다음 명령어 수행
`/opt/kafka_2.13-2.8.1/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic tweets`

* Producer Tab에 `hello` 입력
* Consumer Tab에 `hello` 출력 확인


## Python - Kafak 
* kafka 도커에서 수행

1. python3 설치 `sudo apt install python3`
2. python3-pip 설치 `sudo apt install python3-pip`
3. kafka-python 설치 `pip3 install kafka-python`
    * kafka-python 설치 확인
    * 1. python3 실행 `python3`
    * 2. import kafka 실행
    * 에러가 없으면 정상 설치 완료
4. git 설치 `sudo apt-get install git`
5. git clone `git clone https://github.com/dbusteed/kafka-spark-streaming-example`
6. 실행 권한 추가
    * `chmod +x fake_tweet_stream.py`
    * `chmod +x transformer.py`
    * `chmod +x tweet_stream.py`
7. words 파일 생성 `vim words` (경로 pwd 명령어로 사용 : /kafka-spark-streaming-example/files)</code></pre><h1 id="파일-내용">파일 내용</h1>
<p>sdafjlsdhaflksadjfhkasdjfhl
sadfjhasdfksdhafkjsadfh
dsfhksadfjhsadkfjhsadkf
sdafkjsadfhkjsadfhkjsghiuwehkjvzxcj
weafuvbkcjzkcxjvb</p>
<pre><code>8. `vim fake_tweet_stream.py` word 파일 경로 수정</code></pre><h1 id="파일-내용-변경">파일 내용 변경</h1>
<p>WORD_FILE = &#39;/kafka-spark-streaming-example/files/words&#39;</p>
<pre><code>
9 server.properties - advertiesd.host.name 수정 `vim /opt/kafka_2.13-2.8.1/config/server.properties`</code></pre><h1 id="아래-내용-추가수정">아래 내용 추가/수정</h1>
<p>...
listeners=PLAINTEXT://172.19.0.15:9092
...
advertiesd.host.name=자신의호스트주소</p>
<pre><code>

# Spark

1. spark-zeppelin 진입 `docker exec -it spark-zeppelin bahs`
2. `pip3 install pyspark` 파이스파크 설치
3. `pyspark` 실행 (`ctrl + d` 로 exit)
4. pyspark 버전에 맞게 설정 `pip3 install pyspark==3.0.0`
5. python-kafka 설치 `pip3 install kafka-python`

</code></pre><p>AnalysisException: Failed to find data source: kafka. Please deploy the application as per the deployment section of &quot;Structured Streaming + Kafka Integration Guide&quot;.;
오류 발생시 아래 명령어 실행</p>
<p>spark-submit --packages org.apache.spark:spark-sql-kafka-0-10_2.12:3.0.0 my_kafka_spark_app.py
또는
spark-shell --packages org.apache.spark:spark-sql-kafka-0-10_2.12:3.0.0</p>
<p>```</p>
<p>`</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[시스템 아키텍처 설계]]></title>
            <link>https://velog.io/@comet_strike/%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@comet_strike/%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Thu, 20 Apr 2023 02:48:51 GMT</pubDate>
            <description><![CDATA[<ul>
<li>현재 진행하는 프로젝트에서 시스템 아키텍처 설계 역할을 맡았다.</li>
<li>또한, 금주 일어지는 프로젝트 기획 발표에서 구체적인 수치를 언급하고 싶다는 요구사항을 받아, 어떻게든 구체적인 수치를 넣을 수 있는 방법을 생각 해 작성했다.</li>
<li>cloudcraft를 이용해서 제작했다. <code>https://www.cloudcraft.co/</code></li>
</ul>
<h1 id="시스템-아키텍처">시스템 아키텍처</h1>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/18593c98-ef3f-4ba2-b44c-c3caa46adcee/image.png" alt=""></p>
<h2 id="시스템-아키텍처-사용자-관점">시스템 아키텍처 사용자 관점</h2>
<ol>
<li><p>사용자가 웹 사이트에 접근하게 되면 Front Deploy Server에서 Nginx를 통해 Build된 React를 받게 된다.</p>
</li>
<li><p>사용자가 서버에서 처리해야 하는 요청을 보낼 경우, 해당 API는 Nginx를 통해서 Load Balancer에 전달된다.</p>
</li>
<li><p>Load Balancer는 2개의 WAS의 상태를 확인 한 후 가용할 수 있는 서버에 해당 요청을 분배한다.</p>
</li>
<li><p>각 각의 WAS는 사용자의 요청을 처리하며 분산 처리가 필요한 경우에는 Apache Kafka 메시징 시스템을 이용해 분산 처리가 실행된다.</p>
</li>
<li><p>또한, 각  WAS들은 DB와 연결되어 Data를 처리한다.</p>
</li>
</ol>
<h2 id="장점">장점</h2>
<ul>
<li><p>확장성: 로드 밸런서와 Apache Kafka를 사용하여 서비스의 부하 분산과 확장성을 향상시킬 수 있습니다. WAS를 추가하거나 삭제하면서 시스템의 처리량을 유연하게 조절할 수 있습니다.</p>
</li>
<li><p>가용성: 로드 밸런서를 사용하여 여러 대의 WAS에 요청을 분산시키므로, 하나의 WAS가 다운되더라도 서비스 전체에 영향을 미치지 않습니다. 이는 시스템의 가용성을 높이는 데 기여합니다.</p>
</li>
<li><p>성능: Nginx를 통해 정적 리소스를 캐싱하고 효율적으로 전달함으로써, 성능을 향상시킬 수 있습니다. 또한, Apache Kafka를 사용하여 WAS 간의 통신을 효율적으로 처리하므로, 서비스의 응답 시간을 단축시킬 수 있습니다.</p>
<ul>
<li>비용 효율성: WAS를 여러 대 사용하여 부하를 분산시키면서, 하드웨어 리소스를 효율적으로 활용할 수 있습니다. 또한, AWS EC2와 같은 클라우드 서비스를 사용하여 인프라를 구축함으로써, 운영 비용을 절감할 수 있습니다.</li>
</ul>
</li>
</ul>
<h2 id="구체적인-수치">구체적인 수치</h2>
<ul>
<li><p>가용성 : 서비스할 수 있는 WAS가 2개이기 때문에, WAS에 문제가 발생할 가능성은 단순 산술 계산으로 1/2로 줄어들 수 있다. 또한, 로드 밸런서를 사용하여 요청을 분산시키면, 하나의 WAS가 처리할 수 있는 요청 수를 초과해도 시스템 전체에 영향을 미치지 않아 실제로는 문제가 발생할 경우는 더 적다.</p>
</li>
<li><p>성능: Nginx를 통해 정적 리소스를 캐싱하고 효율적으로 전달함으로써, 요청 처리 속도를 높일 수 있습니다. 또한, Apache Kafka를 사용하여 WAS 간의 통신을 효율적으로 처리함으로써, 응답 시간을 단축할 수 있습니다. 대부분의 요청에 대해 100ms 이하의 응답 시간을 달성할 수 있다.</p>
</li>
<li><p>비용 효율성: 클라우드 서비스를 사용하여 인프라를 구축함으로써, 운영 비용을 절감할 수 있습니다. 또한, WAS를 여러 대 사용하여 하드웨어 리소스를 효율적으로 활용함으로써, 적은 비용으로 높은 처리량을 달성할 수 있습니다.
전체  시스템을 구성하는데 약 80만원 정도가 들었지만, 성능을 2배로 높이기 위해서 필요한 비용은 약 30만원이다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2023-04-18]]></title>
            <link>https://velog.io/@comet_strike/Spring-Boot-Controller-Service-%EA%B5%AC%EB%B6%84</link>
            <guid>https://velog.io/@comet_strike/Spring-Boot-Controller-Service-%EA%B5%AC%EB%B6%84</guid>
            <pubDate>Tue, 18 Apr 2023 00:16:11 GMT</pubDate>
            <description><![CDATA[<h1 id="jpql-native-query">JPQL, Native Query</h1>
<ul>
<li>JPA를 이용할 떄, 쿼리를 이용하기 위한 방법은 JPQL과 Native Query가  있다.</li>
<li>JPQL을 사용하게 될 경우 SQL을 추상화 하기 때문에 데이터 베이스 의존도가 낮아진다.</li>
<li>반면 Native Query를 사용하게 될 경우 데이터 베이스의 의존도가 높아진다.</li>
<li>이식성을 고려한다면 JPQL을 사용하는게 좋고, JPQL로 처리할 수 없는 경우 Native Query를 사용하는게 옳다고 생각한다.</li>
</ul>
<h1 id="error-처리">Error 처리</h1>
<ul>
<li>error에 대해서 얼마나 세세하게 할 건지?</li>
</ul>
<h1 id="입력의-신뢰">입력의 신뢰</h1>
<ul>
<li>Front에서 전달하는 입력을 얼마나 신뢰할 수 있는지?</li>
<li>어느 선 까지 신뢰해야 하는지</li>
<li>입력의 검증을 위한 IO 증가 vs 신뢰성 보장</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 제약 조건 수정하기]]></title>
            <link>https://velog.io/@comet_strike/JPA-%EC%A0%9C%EC%95%BD-%EC%A1%B0%EA%B1%B4-%EC%88%98%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@comet_strike/JPA-%EC%A0%9C%EC%95%BD-%EC%A1%B0%EA%B1%B4-%EC%88%98%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 18 Apr 2023 00:15:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://velog.io/@joshuara7235/ISSUEs-JPA%EB%A1%9C-%EC%83%9D%EC%84%B1%ED%95%9C-entity-column%EC%97%90%EC%84%9C-unique-%EC%A0%9C%EC%95%BD%EC%A1%B0%EA%B1%B4-%EC%82%AD%EC%A0%9C%ED%95%98%EA%B8%B0">https://velog.io/@joshuara7235/ISSUEs-JPA%EB%A1%9C-%EC%83%9D%EC%84%B1%ED%95%9C-entity-column%EC%97%90%EC%84%9C-unique-%EC%A0%9C%EC%95%BD%EC%A1%B0%EA%B1%B4-%EC%82%AD%EC%A0%9C%ED%95%98%EA%B8%B0</a></p>
</blockquote>
<ul>
<li>JPA 설정이 다음과 같이 되어있을 경우, <code>spring.jap.hibernate.ddl-auto : update</code></li>
<li>테이블 관련 제약 조건들을 수정하기 위해서는 DB에 직접 수정해야 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Returnz 부하테스트 2]]></title>
            <link>https://velog.io/@comet_strike/Returnz-%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8-2</link>
            <guid>https://velog.io/@comet_strike/Returnz-%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8-2</guid>
            <pubDate>Thu, 06 Apr 2023 12:21:57 GMT</pubDate>
            <description><![CDATA[<pre><code>import time
from locust import HttpUser, task, between
import json
import random


class QuickstartUser(HttpUser):
    wait_time = between(3, 5)

    # 닉네임 조회 Task
    @task
    def chat_room(self):
        self.client.get(&quot;/&quot;)
        self.client.post(&quot;/api/members/login&quot;, json={&quot;username&quot;:&quot;moon5@naver.com&quot;, &quot;password&quot;:&quot;ssafy123!&quot;})

    # 게임 생성 Task
    @task
    def game_create(self):
        self.client.get(&quot;/&quot;)
        self.client.post(&quot;/api/games/init&quot;, json={&quot;theme&quot;:&quot;COVID&quot;,
        &quot;turnPerTime&quot;:&quot;DAY&quot;,
        &quot;startTime&quot;:0,
        &quot;totalTurn&quot;:10,
        &quot;memberIdList&quot;:[1]})


    # 게임 진행 Task
    @task
    def game_progress(self):
        list_index = random.randrange(0, len(game_room_list))
        self.client.get(&quot;/&quot;)
        self.client.post(&quot;/api/games/game&quot;, json={
        &quot;roomId&quot; : &quot;16cd5b51-789d-4723-80eb-b76b1e7a0dd1&quot;,
        &quot;gamerId&quot; : 3021})</code></pre><ul>
<li>게임 진행 시 턴을 넘기지 않도록 설정하고, 부하테스트를 진행했다.</li>
<li>분산 저장을 위해 사용했던 hadoop, spark, zeppeline 등을 stop 시킨 후 진행했다.</li>
</ul>
<h1 id="aws-ec2-성능">AWS EC2 성능</h1>
<ul>
<li>xeon 2676v3 Siblings : 4</li>
</ul>
<h1 id="결과">결과</h1>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/2f79a214-8f32-49c7-b31b-2e21c86f9fb2/image.png" alt=""></p>
<ul>
<li>RPS는 22</li>
<li>95% 4500ms되는 구간의 유저수는 99명</li>
<li>게임의 원활한 진행을 위한 시간이 5000ms라고 가정한다면, 최대 103명 까지 가능하다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Returnz 부하 테스트]]></title>
            <link>https://velog.io/@comet_strike/Returnz-%EB%B6%80%ED%95%98-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@comet_strike/Returnz-%EB%B6%80%ED%95%98-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Wed, 05 Apr 2023 07:38:47 GMT</pubDate>
            <description><![CDATA[<h1 id="부하-테스트를-진행한다">부하 테스트를 진행한다.</h1>
<h2 id="locust-file">locust file</h2>
<pre><code class="language-python">import time
from locust import HttpUser, task, between
import json
import random

game_room_list = [
    {
        &quot;roomId&quot; : &quot;e47b2a75-003f-4420-bdaa-eed369f121b4&quot;,
        &quot;gamerId&quot; : 1329
    },
    {
        &quot;roomId&quot;: &quot;71cf2f67-0dbd-4540-b1bb-dcfe7468e2c3&quot;,
        &quot;gamerId&quot;: 1330
    },
    {
        &quot;roomId&quot;: &quot;fd7906a9-1c6c-414a-9ae6-cdd1130bd60b&quot;,
        &quot;gamerId&quot;: 1331
    }, 
    {
        &quot;roomId&quot;: &quot;941e133b-536d-4d45-817b-3fd1152dce5e&quot;,
        &quot;gamerId&quot;: 1332
    },
    {
        &quot;roomId&quot;: &quot;33609155-5960-4686-98b0-36ecde8ba1e4&quot;,
        &quot;gamerId&quot;: 1333
    },
    {
        &quot;roomId&quot;: &quot;706bd11b-a91f-469b-af7c-86872e8e2123&quot;,
        &quot;gamerId&quot;: 1334
    },
    {
        &quot;roomId&quot;: &quot;13819427-6f64-45f0-9218-655604194b73&quot;,
        &quot;gamerId&quot;: 1335
    },
    {
        &quot;roomId&quot;: &quot;62b7d4bd-4fe0-4ebc-adc3-9fb8ca20bf2e&quot;,
        &quot;gamerId&quot;: 1336
    },
    {
        &quot;roomId&quot;: &quot;b0795412-4675-4b9b-8b0b-f51ed38892f7&quot;,
        &quot;gamerId&quot;: 1337
    },
    {
        &quot;roomId&quot;: &quot;be8fe367-f0ae-4f57-8abd-f635f9def5da&quot;,
        &quot;gamerId&quot;: 1338
    },
    {
        &quot;roomId&quot;: &quot;63eedc5e-4b42-49d4-942a-66188a7303b7&quot;,
        &quot;gamerId&quot;: 1339
    },
    {
        &quot;roomId&quot;: &quot;7c0ca297-1305-4cb2-9788-780fffb18dab&quot;,
        &quot;gamerId&quot;: 1340
    },
    {
        &quot;roomId&quot;: &quot;10c9a2d2-7355-4146-b5d4-5add9d617f41&quot;,
        &quot;gamerId&quot;: 1341
    },
    {
        &quot;roomId&quot;: &quot;906fa5b2-233d-45f7-a5c4-083feafb8023&quot;,
        &quot;gamerId&quot;: 1342
    },
    {
        &quot;roomId&quot;: &quot;0066ffb9-4862-45e0-9de2-2de1177a709d&quot;,
        &quot;gamerId&quot;: 1343
    },
    {
        &quot;roomId&quot;: &quot;159c630a-f1ee-46f1-907f-b1313238e576&quot;,
        &quot;gamerId&quot;: 1344
    }

]

class QuickstartUser(HttpUser):
    wait_time = between(3, 5)

    # 닉네임 조회 Task
    @task
    def chat_room(self):
        self.client.get(&quot;/&quot;)
        self.client.post(&quot;/api/members/login&quot;, json={&quot;username&quot;:&quot;moon5@naver.com&quot;, &quot;password&quot;:&quot;ssafy123!&quot;})

    # 게임 생성 Task
    @task
    def game_create(self):
        self.client.get(&quot;/&quot;)
        self.client.post(&quot;/api/games/init&quot;, json={&quot;theme&quot;:&quot;COVID&quot;,
        &quot;turnPerTime&quot;:&quot;DAY&quot;,
        &quot;startTime&quot;:0,
        &quot;totalTurn&quot;:10,
        &quot;memberIdList&quot;:[1]})


    # 게임 진행 Task
    @task
    def game_progress(self):
        list_index = random.randrange(0, len(game_room_list))
        self.client.get(&quot;/&quot;)
        self.client.post(&quot;/api/games/game&quot;, json={json.dumps(game_room_list[list_index])})</code></pre>
<ul>
<li><p>게임방을 미리 생성해둔 채로 진행.</p>
</li>
<li><p>닉네임 조회, 게임 생성, 게임 조회를 수행하도록 함.</p>
</li>
<li><p>게임방 생성을 계속해서 해줘야 하므로, 한번의 테스트로 Insight를 얻을 수 있도록 Number of Users, spawn rate를 조정</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/ca5fc4b0-0f16-40eb-bbf6-8b573c6b9336/image.png" alt=""></p>
<ul>
<li>게임 방 진행시 가져오는 Data가 많고, DB에서 탐색해야 하는 데이터도 많기 때문에 RPS가 낮고, 응답 시간 또한 낮을 거라 판단.</li>
<li>게이방 처음 진입 시, 대량의 데이터를 한번에 가져오기 때문에, 초기 Response Times 95%가 비선형적으로 증가한다고 판단.</li>
</ul>
<h2 id="부하-테스트-결과">부하 테스트 결과</h2>
<p><img src="https://velog.velcdn.com/images/comet_strike/post/f10da8f7-c5ca-45b3-8b91-90c49c43d998/image.png" alt=""></p>
<ul>
<li>Total Request per Second를 분석하면 제일 작은 RPS는 10.3 이였다.</li>
<li>최대 RSP는 약 15이다.</li>
<li>Failure는 닉네임 조회, 로그인, 메인 화면 진입시에 발생했다.</li>
<li>Response time 50th percentile은 16 ~ 18사이로 균등하다</li>
<li>Response time 95th percentile은 꾸준히 증가한다. (3200ms ~ 34000ms)</li>
<li>Response Time 95th percentile이 줄어들다 증가하는 구간의Number of Users는 33이다.</li>
<li>해당 경우의 Response Time 95th percentile은 4600ms이다.</li>
</ul>
<h2 id="분석-결과">분석 결과</h2>
<ul>
<li>게임 진행 시 처리해야 하는 데이터가 많기 때문에 응답시간이 어느정도 낮다는 걸 고려한 결과 최대 처리할 수 있는 유저의 수는 33명이라 가정한다.</li>
<li>locust에서는 3, 5초 사이로 Task를 수행하도록 설정했지만, 실제 게임에서는 이보다 긴 1분 이내에서 Task가 수행되기때문에 테스트 보다 안정적일 것이라 생각한다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>