[2024.08.01 TIL] 내일배움캠프 76일차 (최종 팀프로젝트, ejs를 이용해 프론트엔드 예제 구현)

My_Code·2024년 8월 1일
0

TIL

목록 보기
91/112
post-thumbnail

본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.


💻 TIL(Today I Learned)

📌 Today I Done

✏️ ejs를 이용해 프론트엔드 예제 구현

  • 필요한 패키지 설치 (pull 받으면 자동으로 받아짐)

    $ npm install axios
    
    $ npm install ejs
  • ejs를 위한 디렉토리 구성 (파일 이름은 임시로 지정함)

    📦views
     ┣ 📂auth
     ┃ ┣ 📜auth.view.controller.ts
     ┃ ┗ 📜sign.view.ejs
     ┣ 📂common
     ┃ ┗ 📜header.view.ejs
     ┣ 📂users
     ┣ 📜index.view.controller.ts
     ┗ 📜index.view.ejs
    📦public
     ┣ 📂css
     ┃ ┣ 📂auth
     ┃ ┃ ┗ 📜sign.css
     ┃ ┣ 📂common
     ┃ ┃ ┗ 📜header.css
     ┃ ┗ 📜index.css
     ┗ 📂js
     ┃ ┣ 📂auth
     ┃ ┃ ┗ 📜sign.js
     ┃ ┗ 📂common
     ┃ ┃ ┗ 📜header.js
    • controller 파일은 해당 ejs와 URL 주소를 연결하기 위한 파일

    • ejs 파일은 실제로 사용자에게 보여질 HTML 파일

    • views 폴더에 해당 기능마다 폴더를 생성해서 controller와 ejs 파일을 만들어서 작업 진행


  • 실행할 ejs에 필요한 경로 설정
    // main.ts
    
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { ConfigService } from '@nestjs/config';
    import { ValidationPipe } from '@nestjs/common';
    import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
    import { NestExpressApplication } from '@nestjs/platform-express';
    import { resolve } from 'path';
    
    async function bootstrap() {
      // NestExpressApplication는 Express 기능을 Nest.js에서 활용할 수 있게 해줌
      const app = await NestFactory.create<NestExpressApplication>(AppModule);
    
      const configService = app.get(ConfigService);
      const port = configService.get<number>('SERVER_PORT');
      app.setGlobalPrefix('/');
      app.useGlobalPipes(
        new ValidationPipe({
          transform: true,
        })
      );
    
      const config = new DocumentBuilder()
        .setTitle('티켓 예매 및 중고 거래 서비스')
        .setVersion('1.0')
        .addTag('Ticketing')
        .addBearerAuth({ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' })
        .build();
      const document = SwaggerModule.createDocument(app, config);
      SwaggerModule.setup('/', app, document, {
        swaggerOptions: {
          persistAuthorization: true, // 새로고침 시에도 JWT 유지하기
          tagsSorter: 'alpha', // API 그룹 정렬을 알파벳 순으로
          operationsSorter: 'alpha', // API 그룹 내 정렬을 알파벳 순으로
        },
      });
    
      // ejs 설정
      app.useStaticAssets(resolve('./src/public')); // 정적 파일 경로 설정 (js, css, img)
      app.setBaseViewsDir(resolve('./src/views')); // 클라이언트에 보여질 파일들의 경로 설정
      app.setViewEngine('ejs'); // 클라이언트엑세 보여질 템플릿 엔진 설정
    
      await app.listen(port);
    }
    bootstrap();

  • URL 상에서 해당 View 컨트롤러를 사용하기 위해서 app.module에 등록
    • 나중에 View 컨트롤러가 많아지면 View를 위한 모듈을 생각 중…

      // app.module.ts
      
      import { MiddlewareConsumer, Module } from '@nestjs/common';
      import { AppController } from './app.controller';
      import { AppService } from './app.service';
      import { ConfigModule, ConfigService } from '@nestjs/config';
      import { TypeOrmModule } from '@nestjs/typeorm';
      import { configModuleValidationSchema } from 'src/configs/env-validation.config';
      import { typeOrmModuleOptions } from 'src/configs/database.config';
      import { AuthModule } from './modules/auth/auth.module';
      import { UsersModule } from './modules/users/users.module';
      import { ShowsModule } from './modules/shows/shows.module';
      import { TradesModule } from './modules/trades/trades.module';
      import { ImagesModule } from './modules/images/images.module';
      import { RedisModule } from './modules/redis/redis.module';
      import { BullModule } from '@nestjs/bullmq';
      import { ViewsController } from './views/index.view.controller';
      import { AuthViewsController } from './views/auth/auth.view.controller';
      
      @Module({
        imports: [
          ConfigModule.forRoot({
            isGlobal: true,
            validationSchema: configModuleValidationSchema,
          }),
      
          BullModule.forRootAsync({
            imports: [ConfigModule],
            inject: [ConfigService],
            useFactory: (configService: ConfigService) => ({
              connection: {
                host: configService.get<string>('REDIS_HOST'),
                port: configService.get<number>('REDIS_PORT'),
                password: configService.get<string>('REDIS_PASSWORD'),
              },
            }),
          }),
          TypeOrmModule.forRootAsync(typeOrmModuleOptions),
          AuthModule,
          UsersModule,
          ShowsModule,
          TradesModule,
          ImagesModule,
          RedisModule,
        ],
        controllers: [AppController, ViewsController, AuthViewsController],
        providers: [AppService],
      })
      export class AppModule {}

  • View 컨트롤러 예제
    • index.view 를 메인 view 파일로 지정

    • 사실 메인 페이지의 주소를 지정하기 위해서 index.view.controller.ts를 사용

      // index.view.controller.ts
      
      import { Controller, Get, Render } from '@nestjs/common';
      
      @Controller('views')
      export class ViewsController {
        @Get()
        @Render('index.view.ejs')
        moveToHome() {}
      }
      
    • @Render() 데코레이터를 통해서 어떤 파일을 통해 클라이언트에게 보여질지 설정

    • 즉, localhost:3000/views 경로로 들어가면 index.view.ejs 파일이 랜더링 된다는 의미

    • 원래 ejs 자체가 서버 사이드 랜더링(SSR)를 위해 주로 사용된다고 하지만 이 프로젝트에서는 클라이언트 사이드 랜더링(CSR)를 위해서 컨트롤러는 그저 URL 설정을 위한 용도로만 사용

    • 서버 사이드 랜더링(SSR)은 서버에서 HTML을 생성하여 클라이언트에게 전달하는 방식

    • 클라이언트 사이드 랜더링(CSR)은 클라이언트(브라우저)에서 JavaScript를 통해 HTML을 생성하는 방식

    • 이번 프로젝트에서 필요한 페이지를 기준으로 컨트롤러에 주소와 ejs 파일을 설정하면 됨


  • View ejs 예제
    • <head> 테그에서 필요한 JS, CSS를 설정함

    • <%- include() %>를 사용하면 공통적으로 사용하는 ejs 형식을 불러다 사용 가능함

    • 그 밖의 내용은 대부분 캠프 초반에 학습한 HTML 코드임

      <!-- index.view.ejs -->
      
      <!DOCTYPE html>
      <html lang="en">
      
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>Nest & EJS ❤</title>
      
          <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
              integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
          <link rel="stylesheet" href="/css/index.css">
          <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"
              integrity="sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3"
              crossorigin="anonymous"></script>
          <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js"
              integrity="sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V"
              crossorigin="anonymous"></script>
      
      </head>
      
      <body>
          <!-- include 안에는 상대 경로 작성 -->
          <%- include('./common/header.view.ejs') %>
              <main>
                  <div id="carouselExampleIndicators" class="carousel slide" data-bs-ride="true">
                      <div class="carousel-indicators">
                          <button type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide-to="0"
                              class="active" aria-current="true" aria-label="Slide 1"></button>
                          <button type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide-to="1"
                              aria-label="Slide 2"></button>
                          <button type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide-to="2"
                              aria-label="Slide 3"></button>
                      </div>
                      <div class="carousel-inner">
                          <div class="carousel-item active">
                              <img src="https://velog.velcdn.com/images/my_code/post/33437564-f78e-42f0-9b6a-d6dd2e950e40/image.png"
                                  class="d-block w-100" alt="...">
                          </div>
                          <div class="carousel-item">
                              <img src="https://velog.velcdn.com/images/my_code/post/33437564-f78e-42f0-9b6a-d6dd2e950e40/image.png"
                                  class="d-block w-100" alt="...">
                          </div>
                          <div class="carousel-item">
                              <img src="https://velog.velcdn.com/images/my_code/post/33437564-f78e-42f0-9b6a-d6dd2e950e40/image.png"
                                  class="d-block w-100" alt="...">
                          </div>
                      </div>
                      <div>
                          <button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleIndicators"
                              data-bs-slide="prev">
                              <span class="carousel-control-prev-icon" aria-hidden="true"></span>
                              <span class="visually-hidden">Previous</span>
                          </button>
                          <button class="carousel-control-next" type="button" data-bs-target="#carouselExampleIndicators"
                              data-bs-slide="next">
                              <span class="carousel-control-next-icon" aria-hidden="true"></span>
                              <span class="visually-hidden">Next</span>
                          </button>
                      </div>
                  </div>
              </main>
              <footer>
              </footer>
      
      </body>
      
      </html>


  • 작동 순서 간단 정리
    1. localhost:3000/views 경로로 접속
    2. inde.view.controller.ts 를 통해서 해당하는 경로에 맞는 ejs 파일을 랜더링 함
    3. 이 때, 필요한 JS와 CSS가 동작함
    4. 크게 나누면 View 컨트롤러, View ejs, JS, CSS로 나눌 수 있음

✏️ ejs를 이용한 로그인 프론트엔드 예제

  • 로그인 동작 순서

    1. 사용자가 이메일과 비밀번호를 입력하고 로그인 버튼을 클릭함

    2. 해당 버튼에 설정된 이벤트 동작

    3. 입력한 이메일과 비밀번호를 JS 코드로 가져옴

    4. JS 코드에서 axios를 통해서 백엔드와 통신

    5. 이 때, 사용자가 입력한 이메일과 비밀번호를 같이 넘김

    6. 만약 잘못된 이메일이나 비밀번호를 넘길 경우 에러를 출력함

    7. 성공한다면 백엔드의 로그인 API는 해당 사용자의 토큰을 반환함

    8. 프론트엔트 측에서 반환받은 토큰을 localStorage에 저장 (보안은 취약하지만 일단 임시로 사용)

    9. 혹시나 사용자 인가를 위해 토큰이 필요하다면 해당 API를 axios로 요청할 때 headers에 토큰을 넣어서 전달


  • 로그인 기능 ejs 및 JS 예제 (Auth)

  • View 컨트롤러 예제

    • localhost:3000/views/auth/sign 에 접속하면 auth/sign.view.ejs 파일을 랜더링 함

      import { Controller, Get, Render } from '@nestjs/common';
      
      @Controller('views/auth')
      export class AuthViewsController {
        @Get('/sign')
        @Render('auth/sign.view.ejs')
        signIn() {}
      }

  • View ejs 예제
    • 아직은 기본적인 틀인 예제

    • 여기는 클라이언트에게 보여줄 HTML 파일이라고 생각해도 됨

      <!-- sign.view.ejs -->
      
      <!DOCTYPE html>
      <html lang="en">
      
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>Nest & EJS ❤</title>
      
          <!-- 부트스트랩 CSS CDN-->
          <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
              integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
      
          <!-- 부트스트랩 JS CDN -->
          <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"
              integrity="sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3"
              crossorigin="anonymous"></script>
          <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js"
              integrity="sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V"
              crossorigin="anonymous"></script>
      
          <!-- 로그인 페이지에 필요한 CSS 및 JS -->
          <link rel="stylesheet" href="/css/auth/sign.css">
          <script type="module" src="/js/auth/sign.js"></script>
      </head>
      
      <body>
          <!-- include 안에는 상대 경로 작성 -->
          <%- include('../common/header.view.ejs') %>
              <main>
                  <div class="container">
                      <!-- Heading -->
                      <h1>SIGN IN</h1>
      
                      <!-- Links -->
                      <ul class="links">
                          <li>
                              <a href="#" id="signin">SIGN IN</a>
                          </li>
                          <li>
                              <a href="#" id="signup">SIGN UP</a>
                          </li>
                          <li>
                              <a href="#" id="reset">RESET</a>
                          </li>
                      </ul>
      
                      <!-- Form -->
                      <form class="sign-form">
                          <!-- email input -->
                          <div class="first-input input__block first-input__block">
                              <input type="email" placeholder="Email" class="input" id="email" />
                          </div>
                          <!-- password input -->
                          <div class="input__block">
                              <input type="password" placeholder="Password" class="input" id="password" />
                          </div>
                          <!-- repeat password input -->
                          <div class="input__block">
                              <input type="password" placeholder="Repeat password" class="input repeat__password"
                                  id="repeat__password" />
                          </div>
                          <!-- sign in button -->
                          <button class="signin__btn">
                              Sign in
                          </button>
                      </form>
                      <!-- separator -->
                      <div class="separator">
                          <p>OR</p>
                      </div>
                      <!-- google button -->
                      <button class="google__btn">
                          <i class="fa fa-google"></i>
                          Sign in with Google
                      </button>
                  </div>
              </main>
              <footer>
              </footer>
      
      </body>
      
      </html>

  • 백엔드 로그인 API를 호출 및 결과값 처리를 위한 JS
    document.addEventListener('DOMContentLoaded', function () {
      const signUp = document.querySelector('#signup');
      const signIn = document.querySelector('#signin');
      const reset = document.querySelector('#reset');
      const firstInput = document.querySelector('.first-input');
      const hiddenInput = document.querySelector('#repeat__password');
      const signInBtn = document.querySelector('.signin__btn');
    
      // 이미 로그인 했는지 확인
      if (window.localStorage.getItem('accessToken')) {
        alert('이미 로그인한 사용자입니다.');
        window.location.href = '/views';
      }
    
      //----------- sign up ---------------------
      signUp.addEventListener('click', function (e) {
        e.preventDefault();
        const h1 = signUp.closest('li').parentNode.previousElementSibling; // h1 요소 찾기
        h1.textContent = 'SIGN UP';
        signUp.parentElement.style.opacity = '1';
        Array.from(signUp.parentElement.parentElement.children).forEach(function (sibling) {
          if (sibling !== signUp.parentElement) sibling.style.opacity = '.6';
        });
        firstInput.classList.remove('first-input__block');
        firstInput.classList.add('signup-input__block');
        hiddenInput.style.opacity = '1';
        hiddenInput.style.display = 'block';
        signInBtn.textContent = 'Sign up';
    
        // 회원 가입 이벤트 연결
      });
    
      //----------- sign in ---------------------
      signIn.addEventListener('click', function (e) {
        e.preventDefault();
        const h1 = signIn.closest('li').parentNode.previousElementSibling; // h1 요소 찾기
        h1.textContent = 'SIGN IN';
        signIn.parentElement.style.opacity = '1';
        Array.from(signIn.parentElement.parentElement.children).forEach(function (sibling) {
          if (sibling !== signIn.parentElement) sibling.style.opacity = '.6';
        });
        firstInput.classList.add('first-input__block');
        firstInput.classList.remove('signup-input__block');
        hiddenInput.style.opacity = '0';
        hiddenInput.style.display = 'none';
        signInBtn.textContent = 'Sign in';
      });
    
      // 로그인 이벤트 연결
      signInBtn.addEventListener('click', async (e) => {
        e.preventDefault(); // 기본 이벤트 동작을 막기 위한 부분
    
        // 로그인 API에게 보낼 사용자 입력 DTO 객체
        const signInDto = {
          email: document.getElementById('email').value,
          password: document.getElementById('password').value,
        };
    
        try {
          // 백엔드 로그인 API 호출
          const result = await axios.post('/auth/sign-in', signInDto);
    
          // 반환된 토큰을 localStorage에 저장
          window.localStorage.setItem('accessToken', result.data.accessToken);
          window.localStorage.setItem('refreshToken', result.data.refreshToken);
    
          // 로그인 완료 후 메인 페이지로 이동
          window.location.href = '/views';
        } catch (err) {
          // 로그인 실패 시 에러 처리 (에러 메세지 출력)
          console.log(err.response.data);
          const errorMessage = err.response.data.message;
          alert(errorMessage);
        }
      });
    
      //----------- reset ---------------------
      reset.addEventListener('click', function (e) {
        e.preventDefault();
        const inputs = document.querySelectorAll('.input__block .input');
        inputs.forEach(function (input) {
          input.value = '';
        });
      });
    });
    



📌 Tomorrow's Goal

✏️ 프론트엔드 구현 방법 전파 및 개발 시작

  • 일단 내일 오전에 팀원들에게 이러한 내용을 작성했다고 전파하고 사용방법을 간단하게 이야기할 예정

  • 그리고 기능 구현이 끝난 사람부터 본인이 맡았던 기능 구현에 대한 프론트엔드를 개발할 예정

  • 프론트엔드를 빠르게 시작해야 프론트엔드 개발을 통해 알게 되는 백엔드 에러를 수정할 수 있기 때문에 서둘러 진행할 예정



📌 Today's Goal I Done

✔️ ejs를 이용해 프론트엔드 예제 구현

  • 오늘은 ejs를 이용해 간단한 프론트앤드 예제를 구현함

  • 기본적인 파일 구조와 메인 페이지, 로그인 페이지를 구현함

  • 그리고 axios를 이용해서 백엔드와의 통신해 실제 로그인되는 과정을 구현함

  • 이 프론트엔드 예제를 만드는 이유는 기능 구현이 빨리 끝나서 미리 기본적인 세팅을 진행하고 팀원들에게 이를 전파하기 위해서 구현함

  • 팀원들이 이 내용을 보고 조금이라도 더 빨리 개발에 들어가는 것이 목적임


profile
조금씩 정리하자!!!

0개의 댓글