백엔드 개발 포트폴리오 | 중고 거래 서비스
내일배움캠프 4기 웹 개발 과정 Node.js 트랙 수료생 최종 프로젝트 '냐옹상회'을 소개합니다.
Aug 16, 2023
냐옹상회 서비스 소개
가격으로만 소통하자 Try your Deal!
국내 중고거래 시장은 점점 커지고 있습니다. 커져가는 만큼 많은 불편한 점들이 생겨나며 그 중 하나는 가격협상을 하며 얻는 스트레스라고 생각을 합니다.
그래서 저희는 가격을 협상하는데 의사소통을 없애보려고 했습니다.
의사소통 없이 오직 가격으로만 결정하자 그것이 저희가 서비스를 기획하게된 계기 입니다.
테스트용 ID : test01@gmail.com
테스트용 : PW : 1234
관리자 : https://5gnunfleamarket.shop/admin/login
id : admin1
pw : admin1
아키텍처
로컬용
배포용
기술적 의사 결정
onclick 이벤트 시 서버와 통신할 도구 선택
- onclick 이벤트를 통해서 추가로 서버에 데이터를 요청 할 HTTP 통신 도구 선택 axios, ajax, fetch함수 중에 하나를 선택해서 사용하기로 결정
- ajax는 JQuery를 사용해야 하는 단점과 fetch는 json으로 data를 변환해야하는 추가 작업이 필요한 단점으로 위의 axios를 사용하기로 결정했습니다.
일반 유저와 Admin의 인증 차이
- 일반 유저는 관리자 페이지에 들어갈 수 없어야 됩니다.
- NestJS에서 Guard를 기본적으로 제공합니다. 가드는 Passport-strategy 전략을 실행해주는 역할을 해줍니다.
- 유저와 관리자가 각각 부여받는 엑세스 토큰에는 다른 정보들이 담깁니다. 유저는 리프레시 토큰 또한 부여받습니다. 따라서 유저와 관리자는 각각의 다른 Passport-strategy 전략을 취해야 합니다.
- 그 결과 일반 유저와 관리자가 인증을 거치는 Guard가 분리됩니다.
- 일반 유저는 로그인을 하더라도 관리자가 쓰는 Guard에 막혀 관리자 페이지에 들어갈 권한이 안 주어집니다.
데이터 일관성을 위해 NestJS 에서 트랜잭션 사용
- 사용자가 제품의 ‘찜하기’를 업데이트 하기 위한 접근 방식을 고민했습니다. IF문을 사용하지않고 트랜잭션을 사용한 이유
- 사용자가 ‘찜하기’를 누르면
likeRepository
에서 ‘찜하기’를 하였는지 확인합니다likeRepository
에 해당 자료가 없으면 저장 후productRepository
의 ‘likes’컬럼을 +1 update 해줍니다.ikeRepository
에 해당자료가 없으면 반대작업을 합니다.ikeRepository
,productRepository
두개의 데이터 소스가 업데이트 되기 때문에 일관성과 무결성을 보장해야됩니다.
- 고려된 옵션
IF문을 사용하여 업데이트
트랜잭션을 사용하여 업데이트
- 평가 기준
데이터 일관성 및 무결성
안전성
- 옵션 평가
- 트랜잭션 데이터 일관성과 무결성을 보장한다. 오류 발생 시 데이터 변경사항을 롤백하는 오류처리를 해준다.
- IF문 조건부 논리로 처리하는 비교적 쉬운 방법이다. 트랜잭션 수준의 안정성을 제공하지 않는다.
- 결론
다중 데이터를 처리하는 ‘찜하기’ 기능에서는 트랜잭션을 사용하는것이 좋은방법이다.
ECS
- 배포시에 이때까지 해오던 EC2 배포에 불편함을 느꼈습니다.
- 서버에 관한 세팅도 하나에서 열까지 다 세팅해주어야 하고 심지어 서버도 git clone 혹은 FTP 서버를 통해 전송해주어야 서버를 띄울 수 있었습니다.
- 저희 팀은 이 플랫폼이 서비스 된다고 가정했을 때, 이 서버가 컨테이너화 되어 트래픽에 따라 자연스럽게 오케스트레이션 되는 기능을 찾게 되었고, Github Actions, ECS Fargate CI/CD를 통해 트래픽에 따라 유연하게 AutoScaling되는 클라우드 서버를 구축하였습니다.
- 또한 ECS를 사용함으로써 AWS CodeDeploy를 통한 다양한 배포 전략(롤링, 카나리, 블루/그린)에도 대응이 가능하게 되었습니다.
Lambda
- 저희 팀의 서버에는 거래 완료시 구매자에게 거래가 성립되었다는 문자를 보내는 함수가 존재합니다.
- 이는 구매자와 판매자간 거래 성립시에만 실행되는 호출되어 코드가 실행되는데 Lambda는 서버리스 컴퓨팅 서비스로, 함수가 호출될 때만 코드를 실행시키기 때문에 특정 시간 혹은 기간에만 동작하는 서비스를 만들 때 유용합니다.
- 서버리스 컴퓨팅 환경의 개발자는 이 서버에 대한 트래픽, 개수, 사양등을 고려할 필요 없이 온전히 코드를 집중할 수 있게되는데 저희 팀은 먼저 서버리스 컴퓨팅이 어떻게 이루어지는지 체험하고자 먼저 문자 발송 로직을 AWS Lambda Function으로 구현하게 되었습니다.
트러블 슈팅
파일을 읽을 때 상대 경로 문제
- 문제 설명:
절대 경로(
"/js"
) 대신 상대 경로("js"
)를 사용했기 때문에“public”
폴더의”/js/header.js"
파일을 읽는 데 문제가 발생 해당 기능의 컨트롤러의 경로가 자꾸 앞에 붙는 문제가 발생하였습니다 문제 경로 :localhost:3000/api/orders/js/header.js
원하는 경로 :localhost:3000/js/header.js
- 재현 단계:
문제를 재현하기 위해 상대 경로("js")와 접두사 경로(
"api/orders/js/header.js"
)를 사용하여"public/js/header.js"
파일에 액세스하려고 했습니다.
- 예상 동작:
"public”
폴더의“/js/header.js"
파일에 접근할 수 있다고 생각
- 실제 행동: 상대 경로를 사용할 때 파일을 찾을 수 없었고 경로도 작동하지 않았습니다.
- 문제 해결 단계: - 모든 @Controller() 요청메소드를 어떻게 없앨까? 에 대한 고민 - 잘못된 생각을 인지하고 경로에 대해 검색 - 슬래시("/")로 시작하는 절대 경로를 사용하는 것이 파일을 참조하는 올바른 방법임을 깨달았습니다.
Docker 컨테이너에 올라간 MySQL이 무한 재시작 하는 버그
- 문제 설명:
docker compose -up을 통해 컨테이너에 올라간 MySQL이 정상 작동되지않는 문제 발견
- 원인 조사:
docker 파일에 있는 MySQL 관련 코드를 검색
- 원인 파악:
./.docker/mysql/conf.d:/etc/mysql/conf.d
코드가 문제를 일으키고 있었다.
저희 docker 파일에는 설정 내용이 없는데 설정 파일을 생성해서 생기는 문제였다.- 해결:
해당 코드를 지우고 docker compose -up을 다시 하니 해결되었다.
파일을 읽을 때 상대 경로 문제
- 문제 설명:
절대 경로(
"/js"
) 대신 상대 경로("js"
)를 사용했기 때문에“public”
폴더의”/js/header.js"
파일을 읽는 데 문제가 발생 해당 기능의 컨트롤러의 경로가 자꾸 앞에 붙는 문제가 발생하였습니다 문제 경로 :localhost:3000/api/orders/js/header.js
원하는 경로 :localhost:3000/js/header.js
- 재현 단계:
문제를 재현하기 위해 상대 경로("js")와 접두사 경로(
"api/orders/js/header.js"
)를 사용하여"public/js/header.js"
파일에 액세스하려고 했습니다.
- 예상 동작:
"public”
폴더의“/js/header.js"
파일에 접근할 수 있다고 생각
- 실제 행동: 상대 경로를 사용할 때 파일을 찾을 수 없었고 경로도 작동하지 않았습니다.
- 문제 해결 단계: - 모든 @Controller() 요청메소드를 어떻게 없앨까? 에 대한 고민 - 잘못된 생각을 인지하고 경로에 대해 검색 - 슬래시("/")로 시작하는 절대 경로를 사용하는 것이 파일을 참조하는 올바른 방법임을 깨달았습니다.
PickType 상위 객체 상속
- 문제설명:
처음에는 PickType을 쓰지 않고 DTO를 만들었었습니다.
Create,Read,Update,Delete 모두 DTO가 필요할 경우 DTO를 각각 만들어야 하는데 그렇게되면 코드가 길어지고, 타입을 재활용 할 수 없어집니다.
- 문제 해결 단계:
공식문서를 찾아보니 4개의 Mapped Types중 PickType의 존재를 알게되었고 가장 적합하다고 생각한 PickType을 선택하게 되었습니다.
동일한 엔티티를 타입으로 재활용 하고, PickType으로 지정함으로써 엔티티 변화를 빌드 시점에서 인지할 수 있어 예상치 못한 에러를 방지할 수 있게 되었습니다.
Node.js의 CACHE_MODULE
- 문제 설명:
모듈을 쓸때는 의존성 / 특징들을 고려하여 사용하여야 합니다.
처음 메일 인증번호 발송을 구현할때 옵션으로 TTL을 주지 않았었습니다.
로그에도 찍히고 정상 인증은 되는데 값이 Redis에는 담기지 않는 이슈가 있었습니다.
- 문제 해결 단계:
돌아가지 않는다고 수정하면서 여러 설정을 forRoot를 사용하여 전역으로 적용시켰었는데 전역으로 적용된 설정이 Redis를 사용하는 CACHE_MODULE이 아닌 Node.js 자체의 CACHE_MODULE을 사용하게 만들었습니다.
또한 .set 메서드를 사용할 때 옵션에 TTL을 적용시키지 않으면 자체 CACHE_MODULE을 사용한다는것을 깨달았습니다.
Validate 함수 리팩토링
- 문제 설명:
JWT Passport Strategy쪽의 Validate 함수는 처음 구현 당시 TypeORM의 fineOne 메서드를 사용하여 유저를 찾아 검증을 진행하였습니다. 하지만 요청이 잦은 미들웨어에서 굳이 db까지 엑세스를 하는게 좋은 방법일까 라는 의문을 가지게 되었습니다.
- 문제 해결 단계:
결과적으로 JWT Decode를 통해 payload에서 유저 정보를 찾아 리턴하는 방식으로 DB I/O를 줄이게 되었습니다.
Share article
Subscribe to our newsletter