본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.
공연 수정 프론트엔드는 기존에 공연 정보가 미리 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';
}
});
});
내일은 발표까지 얼마 남지 않았기 때문에 프론트엔드를 마무리 할 예정
그리고 어느정도의 디자인도 되어야 하기 때문에 간단한 디자인도 진행할 예정
수요일에는 튜터님께 간단한 PPT 틀을 보여드려야 하기 때문에 전체적인 테스트도 진행할 예정
PPT를 위한 기능 도식화도 필요
오늘은 공연 수정 ejs 프론트엔드를 구현함
간단할 줄 알았지만 이미지 업로드하는 곳에서 생각보다 오래 걸림
사실 아직 완벽하게 완성하진 않았고 90%정도 완성했음
그리고 업로드한 이미지를 미리보기로 보여줘야 하기 때문에 JS에서 HTML 태그를 만들어서 배치해 줘야 했음
특히, 공연 수정이기 때문에 기존에 있던 이미지 URL를 가져와서 똑같이 미리보기로 보여주고 추가되거나 삭제되는 부분도 신경써야 해서 더욱 어려웠음
오늘도 마찬가지로 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가 계속 살아있기 때문에 현행 유지 할 예정