[2024.08.05 TIL] 내일배움캠프 78일차 (최종 팀프로젝트, 공연 수정 프론트엔드 구현, CI/CD 에러 해결 시도)

My_Code·2024년 8월 5일
0

TIL

목록 보기
93/113
post-thumbnail

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


💻 TIL(Today I Learned)

📌 Today I Done

✏️ 공연 수정 프론트엔드 구현

  • 공연 수정 프론트엔드는 기존에 공연 정보가 미리 Input에 넣어진 상태로 수정되어야 함

  • 처음에는 공연 상세보기 페이지에서 공연 정보를 Session Storage에 담아서 가져왔음

  • 하지만 만약 '/shows/3/edit' URL에서 상세 페이지에서 공연 수정 버튼을 통해서 오지 않으면 Session Storage에 공연 정보를 담아서 가져갈 수 없음

  • 그래서 그냥 공연 수정 페이지에 오면 path parameter에 있는 공연 ID를 가져와서 그 때마다 공연 정보를 가져오는 방식을 채택함

  • 아래가 공연 수정 페이지에서 사용하는 JS 전체 코드임

document.addEventListener('DOMContentLoaded', async () => {
  // 공연 ID 추출
  const urlArray = window.location.href.split('/');
  const showId = urlArray[urlArray.length - 2];

  let show; // 전역으로 사용할 공연 데이터 변수
  const token = window.localStorage.getItem('accessToken');

  try {
    // 공연 ID로 공연 데이터 가져오기
    const response = await axios.get(`/shows/${showId}`);
    show = response.data.date;
    console.log(show);
  } catch (err) {
    console.log(err);
    alert(err.response.data.message);
    window.location.href = '/views';
  }

  // session storage에서 공연 정보 가져오기
  const { title, content, category, runtime, location, price, imageUrl } = show;

  const titleInput = document.getElementById('title');
  const contentInput = document.getElementById('content');
  const categoryInput = document.getElementById('category');
  const runtimeInput = document.getElementById('runtime');
  const locationInput = document.getElementById('location');
  const priceInput = document.getElementById('price');
  const dropdownItems = document.querySelectorAll('.dropdown-item');

  // 기존의 공연 정보 각 Input에 넣기
  titleInput.value = title;
  contentInput.value = content;
  categoryInput.innerHTML = category;
  runtimeInput.value = runtime;
  locationInput.value = location;
  priceInput.value = price;

  // 드롭다운 항목 클릭하면 해당 항목으로 텍스트 변경 이벤트
  dropdownItems.forEach((item) => {
    item.addEventListener('click', () => {
      categoryInput.innerHTML = item.innerText;
    });
  });

  // 수정된 이미지 URL 배열
  let modifiedImageUrls = [...imageUrl];

  // 미리보기 이미지 생성
  const createImagePreview = (url) => {
    const imageDiv = document.createElement('div');
    imageDiv.className = 'position-relative me-2';

    const img = document.createElement('img');
    img.src = url; //기존 이미지 URL 또는 새 이미지 URL
    img.className = 'preview-img';
    img.style.width = '120px';

    // 삭제 버튼
    const deleteImageBtn = document.createElement('button');
    deleteImageBtn.className = 'btn btn-danger position-absolute top-0 end-0';
    deleteImageBtn.innerText = 'X';
    deleteImageBtn.onclick = () => {
      // 미리보기 삭제
      imagePreview.removeChild(imageDiv);
      // 삭제된 이미지 URL을 modifiedImageUrls에서 제거
      modifiedImageUrls = modifiedImageUrls.filter((imageUrl) => imageUrl !== url);
    };

    // 미리보기 이미지와 삭제 버튼 imageDiv에 추가
    imageDiv.appendChild(img);
    imageDiv.appendChild(deleteImageBtn);
    return imageDiv;
  };

  // 기존 이미지 미리보기 생성
  const imagePreview = document.getElementById('imagePreview');
  imageUrl.forEach((url) => {
    imagePreview.appendChild(createImagePreview(url));
  });

  // 파일 업로더에 변화가 있으면 이벤트 추가
  document.getElementById('formFileMultiple').addEventListener('change', (e) => {
    const images = e.target.files;
    imagePreview.innerHTML = ''; // 기존 미리보기를 초기화
    modifiedImageUrls = [...imageUrl]; // 수정된 이미지 배열 초기화

    // 기존 이미지 미리보기 생성
    imageUrl.forEach((url) => {
      imagePreview.appendChild(createImagePreview(url));
    });

    Array.from(images).forEach((image) => {
      const reader = new FileReader();
      reader.onload = async (e) => {
        console.log(e.target);
        imagePreview.appendChild(createImagePreview(e.target.result));
        modifiedImageUrls.push(e.target.result);
      };

      // 파일을 Data URL로 읽기
      reader.readAsDataURL(image);
    });
  });

  // 업데이트 버튼에 이벤트 추가
  document.getElementById('updateShow').addEventListener('click', async () => {
    const formData = new FormData();

    // 선택한 이미지 파일을 formData에 추가
    const fileInput = document.getElementById('formFileMultiple');
    const files = fileInput.files;
    for (let i = 0; i < files.length; i++) {
      formData.append('image', files[i]); // 'image'는 백엔드에서 기대하는 필드 이름입니다.
    }

    // 최대 이미지 수를 추가 (필요에 따라 수정)
    formData.append('maxImageLength', 5);

    try {
      const response = await axios.post('/images', formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
          Authorization: `Bearer ${token}`,
        },
      });

      console.log(response);
    } catch (err) {
      console.log(err);
      // alert(err.response.data.message);
      // window.location.href = '/views';
    }
  });
});


📌 Tomorrow's Goal

✏️ 프론트엔드 마무리 및 디자인

  • 내일은 발표까지 얼마 남지 않았기 때문에 프론트엔드를 마무리 할 예정

  • 그리고 어느정도의 디자인도 되어야 하기 때문에 간단한 디자인도 진행할 예정

  • 수요일에는 튜터님께 간단한 PPT 틀을 보여드려야 하기 때문에 전체적인 테스트도 진행할 예정

  • PPT를 위한 기능 도식화도 필요



📌 Today's Goal I Done

✔️ 공연 수정 프론트엔드 구현

  • 오늘은 공연 수정 ejs 프론트엔드를 구현함

  • 간단할 줄 알았지만 이미지 업로드하는 곳에서 생각보다 오래 걸림

  • 사실 아직 완벽하게 완성하진 않았고 90%정도 완성했음

  • 그리고 업로드한 이미지를 미리보기로 보여줘야 하기 때문에 JS에서 HTML 태그를 만들어서 배치해 줘야 했음

  • 특히, 공연 수정이기 때문에 기존에 있던 이미지 URL를 가져와서 똑같이 미리보기로 보여주고 추가되거나 삭제되는 부분도 신경써야 해서 더욱 어려웠음



⚠️ 구현 시 발생한 문제

✔️ CD workflow 멈춤 현상 (리소스 100% 사용)

  • 오늘도 마찬가지로 CD에서 멈춤 현상이 발생함

  • 그래서 오늘은 EC2 인스턴스 서버에서 top 명령어를 통해서 프로세서 점유율을 체크함

  • 여러번 실행한 결과 CPU가 거의 100%가 찍힌 뒤 서버가 멈춰 버림

  • 일단 kswapd0에서 CPU를 대부분 사용하기 때문에 구글링을 시도함

  • 찾은 내용 중 하나는 npm ci를 할 때 옵션을 통해서 빠르게 진행되도록 할 수 있다고 함

  • https://github.com/actions/setup-node/pull/103#issuecomment-890361838

  • 그리고 혹시 CPU가 넉넉하면 괜찮을까 하고 t2.micro에서 인스턴스를 t3로 업드레이드 했음

  • https://velog.io/@3210439/aws-t2-t3-%EC%B0%A8%EC%9D%98%EC%A0%90-%EC%A0%95%EB%A6%AC

  • 결과적으로 둘 다 소용이 없었음

  • 하는 수 없이 튜터님께 도움을 요청하니 튜터님께서 해당하는 pem 키와 env를 가지고 튜터님 환경에서 테스트를 진행해주신다고 하셨음

  • 결과적으로는 해결됨

  • 정확한 원인이 파악되진 않았지만 appleboy/ssh-action을 다운로드 하는 과정에서 타임아웃이 발생하고 그 과정에서 CPU 점유율이 100%로 치솟는 현상이 발생하는 게 멈춤 현상의 원인으로 보고 있음

  • 그래서 튜터님께서 알아내신 해결 방법은 해당 action을 사용하지 않고 직접 SSH를 통해서 명령을 실행하는 방법이라고 말씀 하셨음

  • 우선 Github Actions Secret에 ENV라는 이름으로 환경변수를 모두 넣음

  • cd.yml 파일은 아래와 같은 기존의 코드를

jobs:
  deploy:
    # workflow 완료 후 결과가 성공 일 때
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    # 작업을 실행 할 VM의 OS 지정
    runs-on: ubuntu-24.04
    # 작업 내의 단위 작업(step)을 정의
    steps:
      # SSH 접속 후 명령을 통해서 배포 진행
      - name: Deploy to EC2
        uses: appleboy/ssh-action@master # SSH 접속 후 명령 실행을 위해 미리 정의 된 workflow를 불러와서 사용
        with:
          host: ${{ secrets.AWS_EC2_HOST }} # EC2 IP주소
          username: ${{ secrets.AWS_EC2_USERNAME }} # EC2 사용자 (Ubuntu OS 설치 시 기본값은 ubuntu)
          key: ${{ secrets.AWS_EC2_PEM_KEY }} # EC2 접속을 위한 pem 파일의 raw data
          port: ${{ secrets.AWS_EC2_PORT }} # EC2 접속을 위한 SSH 포트
          script: |
            # node, npm, yarn 명령어 사용을 위한 설정 (.bashrc 파일에 추가되어 있는 내용)
            export NVM_DIR="$HOME/.nvm"
            [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
            [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion
            
            # yarn global 설치 한 pm2 명령을 위한 설정 (npm 사용 시 불필요)
            # export PATH="$(yarn global bin):$PATH"
            
            # 프로젝트 폴더로 이동
            cd /home/ubuntu/Give_me_the_ticket
            
            # main 브랜치로 이동
            git switch dev
            
            # 최신 소스 코드를 가져옴
            git pull
            
            # .env 파일 생성
            # ">" 는 생성 또는 덮어쓰기
            # ">>" 는 내용 덧붙이기
            echo "SERVER_PORT=${{ secrets.SERVER_PORT }}" > .env
            echo "DB_HOST=${{ secrets.DB_HOST }}" >> .env
            echo "DB_PORT=${{ secrets.DB_PORT }}" >> .env
            echo "DB_USER=${{ secrets.DB_USER }}" >> .env
            echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> .env
            echo "DB_NAME=${{ secrets.DB_NAME }}" >> .env
            echo "DB_SYNC=${{ secrets.DB_SYNC }}" >> .env
            echo "JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }}" >> .env
            echo "REFRESH_SECRET_KEY=${{ secrets.REFRESH_SECRET_KEY }}" >> .env
            echo "AWS_S3_REGION=${{ secrets.AWS_S3_REGION }}" >> .env
            echo "AWS_S3_ACCESS_KEY=${{ secrets.AWS_S3_ACCESS_KEY }}" >> .env
            echo "AWS_S3_SECRET_KEY=${{ secrets.AWS_S3_SECRET_KEY }}" >> .env
            echo "AWS_BUCKET=${{ secrets.AWS_BUCKET }}" >> .env
            echo "REDIS_HOST=${{ secrets.REDIS_HOST }}" >> .env
            echo "REDIS_PORT=${{ secrets.REDIS_PORT }}" >> .env
            echo "REDIS_USERNAME=${{ secrets.REDIS_USERNAME }}" >> .env
            echo "REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}" >> .env
            echo "REDIS_DBNAME=${{ secrets.REDIS_DBNAME }}" >> .env
            echo "KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}" >> .env
            echo "KAKAO_CLIENT_SECRET=${{ secrets.KAKAO_CLIENT_SECRET }}" >> .env
            echo "KAKAO_CALLBACK_URL=${{ secrets.KAKAO_CALLBACK_URL }}" >> .env
            echo "ELASTIC_NODE=${{ secrets.ELASTIC_NODE }}" >> .env
            echo "ELASTIC_MAX_RETRIES=${{ secrets.ELASTIC_MAX_RETRIES }}" >> .env
            echo "ELASTIC_REQUEST_TIMEOUT=${{ secrets.ELASTIC_REQUEST_TIMEOUT }}" >> .env
            echo "ELASTIC_PING_TIMEOUT=${{ secrets.ELASTIC_PING_TIMEOUT }}" >> .env
            echo "ELASTIC_USERNAME=${{ secrets.ELASTIC_USERNAME }}" >> .env
            echo "ELASTIC_PASSWORD=${{ secrets.ELASTIC_PASSWORD }}" >> .env
            
            # 의존성 설치
            # yarn --frozen-lockfile
            npm ci --prefer-offline --no-audit
            
            # 빌드 (ts 아니면 생략 가능)
            # yarn build
            npm run build
            
            # PM2로 실행 중인 서버 중지 및 삭제
            pm2 delete Ticketing
            
            # 서버를 PM2로 실행
            pm2 --name Ticketing start dist/src/main.js
            
            # PM2 설정 저장 (선택사항, startup 설정을 해놨다면)
            pm2 save
  • 아래와 같이 변경함
jobs:
  deploy:
    # workflow 완료 후 결과가 성공 일 때
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    # 작업을 실행 할 VM의 OS 지정
    runs-on: ubuntu-24.04

    # 작업 내의 단위 작업(step)을 정의
    steps:
      # SSH 접속 후 명령을 통해서 배포 진행
      - name: Configure SSH
        run: |
          mkdir -p ~/.ssh/
          echo "$SSH_KEY" > ~/.ssh/ec2.key
          chmod 400 ~/.ssh/ec2.key
          cat >>~/.ssh/config <<END
          Host ec2
            HostName $SSH_HOST
            User $SSH_USER
            IdentityFile ~/.ssh/ec2.key
            StrictHostKeyChecking no
          END
        env:
          SSH_USER: ${{ secrets.AWS_EC2_USERNAME }}
          SSH_KEY: ${{ secrets.AWS_EC2_PEM_KEY }}
          SSH_HOST: ${{ secrets.AWS_EC2_HOST }}

      - name: Deploy to EC2
        run: ssh ec2 'cd /home/ubuntu/Give_me_the_ticket && git switch dev && git pull origin dev && echo "${{ secrets.ENV }}" > .env && ./scripts/run.sh'
  • 참고로 dev에 최신 코드를 병합하고 있기 때문에 일단 EC2에서도 dev 브랜치를 사용하도록 수정함

  • 그러고 프로젝트 폴더 루트에 scripts 폴더를 생성하고 그 안에 run.sh 파일을 생성함

  • 파일을 만들고 다음 명령을 통해서 실행권한을 추가해야 함 chmod 744 run.sh

#!/bin/bash

# node, npm, yarn 명령어 사용을 위한 설정 (.bashrc 파일에 추가되어 있는 내용)
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion

# PM2로 실행 중인 서버 중지 및 삭제
pm2 delete Ticketing

# 의존성 설치
npm ci

# 빌드 (ts 아니면 생략 가능)
npm run build

# 서버를 PM2로 실행
pm2 --name Ticketing start dist/src/main.js

# PM2 설정 저장 (선택사항, startup 설정을 해놨다면)
pm2 save
  • 즉 run.sh에서 기존의 명령어 작업을 진행한다는 의미

  • 그리고 실행중인 서버에서도 리소스를 잡아 먹을 것 같아서 pm2 delete를 npm ci하기 전에 실행시킴

  • 일단 CD가 계속 살아있기 때문에 현행 유지 할 예정

profile
조금씩 정리하자!!!

0개의 댓글