Front-End
Back-End
AI
Front-End
정원석, 최우현, 정유진
Back-End
허창원, 정재훈, 이은석
AI
정원석
userRouter.post('/register', RegisterValidationRules, registerValidate, userController.register);
// 로그인
userRouter.post('/login', loginValidationRules, loginValidate, userController.login);
// 로그인 확인
userRouter.use(loginRequired);
// 로그인 검증
userRouter.get('/isLogin', userController.isLogin);
// 유저 정보 불러오기
userRouter.get('/:userId', userParamsValidate, userController.getOtherUserInfo);
// 현재 로그인한 유저 정보 불러오기
userRouter.get('/mypage', userController.getCurrentUserInfo);
// 유저 정보 수정하기(별명, 설명)
userRouter.put('/mypage', userController.update);
// 유저 정보 삭제하기
userRouter.delete('/mypage', userController.delete);
// 유저 비밀번호 확인, 변경
userRouter.put('/mypage/password', setPasswordValidationRules, setPasswordValidate, userController.updatePassword);
// 현재 로그인한 유저가 작성한 게시글 모두 불러오기
userRouter.get('/mypage/posts', userController.getCurrentUserPosts);
// 현재 로그인한 유저의 참여한 게시글 모두 불러오기
userRouter.get('/mypage/participants', userController.getCurrentUserParticipants);
// 오늘의 낙낙(네트워크)페이지 - 랜덤으로 6명 유저 정보 불러오기
userRouter.get('/network', userController.getRandomUsersInfo);
유저 라우터를 작성할 때, /mypage
, /network
라우터가 모두 유저 정보 불러오기/:userId
로 흘러들어갔습니다.
이 해결방안으로는 아래와 같이 고정적인 주소에 대한 라우터를 먼저 작성하고, 그 후 params와 같은 가변적인 주소에 대한 라우터를 후반부에 작성합니다.
userRouter.post('/register', RegisterValidationRules, registerValidate, userController.register);
// 로그인
userRouter.post('/login', loginValidationRules, loginValidate, userController.login);
// 로그인 확인
userRouter.use(loginRequired);
// 로그인 검증
userRouter.get('/isLogin', userController.isLogin);
// 현재 로그인한 유저 정보 불러오기
userRouter.get('/mypage', userController.getCurrentUserInfo);
// 유저 정보 수정하기(별명, 설명)
userRouter.put('/mypage', userController.update);
// 유저 정보 삭제하기
userRouter.delete('/mypage', userController.delete);
// 유저 비밀번호 확인, 변경
userRouter.put('/mypage/password', setPasswordValidationRules, setPasswordValidate, userController.updatePassword);
// 현재 로그인한 유저가 작성한 게시글 모두 불러오기
userRouter.get('/mypage/posts', userController.getCurrentUserPosts);
// 현재 로그인한 유저의 참여한 게시글 모두 불러오기
userRouter.get('/mypage/participants', userController.getCurrentUserParticipants);
// 오늘의 낙낙(네트워크)페이지 - 랜덤으로 6명 유저 정보 불러오기
userRouter.get('/network', userController.getRandomUsersInfo);
// 유저 정보 불러오기
userRouter.get('/:userId', userParamsValidate, userController.getOtherUserInfo);
import 'dotenv/config';
import multer from 'multer';
import multerS3 from 'multer-s3';
import AWS from 'aws-sdk';
const s3 = new AWS.S3({
region: process.env.AWS_BUCKET_REGION,
accessKeyId: process.env.AWS_ACCESS_KEY,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
});
const upload = multer({
storage: multerS3({
s3: s3,
bucket: process.env.AWS_BUCKET_NAME,
acl: 'public-read',
metadata: function (req, file, cb) {
cb(null, { fieldName: file.fieldname });
},
key: function (req, file, cb) {
const date = new Date().toISOString().replace(/:/g, '-');
const fileExtension = file.originalname.split('.').pop();
const filename = `image-${date}.${fileExtension}`;
cb(null, filename);
},
}),
});
export { upload };
S3는 깊이 공부할 시간이 없어 public으로 사용했습니다. 실무에서는 절대로 public으로 사용해서는 안된다는 것을 알게되었고 추후 고도화 할때 presigned를 적용하여 보안을 강화할 것입니다.
이번 프로젝트에서 모임 게시글에 대해 간단히 설명하면, 신청자가 게시글에 참여 신청하고 게시자가 수락했을 때, 신청자는 '대기중'에서 '수락됨'으로 변경되고 게시글은 참가자가 +1명으로 상태가 변합니다.
이때, 참여신청 과정과 수락 과정 중 하나라도 실행에 실패하면 신청자는 대기중인데 참가자가 +1명이 되거나 반대 상황이 발생하여 데이터베이스의 일관성이 깨지게 됩니다.
이를 해결하기 위해 트랜잭션을 도입했습니다.
그러나 트랜잭션을 도입했음에도 작동하지 않았습니다.
Before
// participantService.js
const transaction = await db.sequelize.transaction({ autocommit: false }); // 트랜잭션 생성
try {
...
await ParticipantModel.update({ participantId, updateField: 'status', newValue: 'accepted' });
await PostModel.update({ postId, fieldToUpdate, newValue });
if (isCompleted) {
await PostModel.update({ postId, fieldToUpdate: 'isCompleted', newValue: true });
}
await transaction.commit();
return {
...
};
} catch (error) {
await transaction.rollback();
...
}
// ParticipantModel.js
update: async ({ participantId, updateField, newValue }) => {
await db.Participant.update(
{ [updateField]: newValue },
{
where: { participantId },
},
);
},
// PostModel.js
update: async ({ postId, fieldToUpdate, newValue }) => {
const updatePost = await db.Post.update(
{ [fieldToUpdate]: newValue },
{
where: { postId },
},
);
return updatePost;
},
트랜잭션이 작동하지 않았던 이유는 DB와 상호작용하는 ParticipantModel.js
, PostModel.js
에서 직접 transaction을 설정해주지 않았지 때문입니다. 어떤 하나의 작업이 하나의 transaction임을 선언해주어야 한다는 것을 깨닫게 되었습니다.
After
// participantService.js
const transaction = await db.sequelize.transaction({ autocommit: false }); // 트랜잭션 생성
try {
...
await ParticipantModel.update({ transaction, participantId, updateField: 'status', newValue: 'accepted' });
await PostModel.update({ transaction, postId, fieldToUpdate, newValue });
if (isCompleted) {
await PostModel.update({ transaction, postId, fieldToUpdate: 'isCompleted', newValue: true });
}
await transaction.commit();
return {
...
};
} catch (error) {
await transaction.rollback();
...
}
// ParticipantModel.js
update: async ({ transaction, participantId, updateField, newValue }) => {
await db.Participant.update(
{ [updateField]: newValue },
{
where: { participantId },
transaction,
},
);
},
// PostModel.js
update: async ({ transaction, postId, fieldToUpdate, newValue }) => {
const updatePost = await db.Post.update(
{ [fieldToUpdate]: newValue },
{
where: { postId },
transaction,
},
);
return updatePost;
},
사용자들의 취미, 이상형, 성격 등에 대한 태그 데이터를 데이터베이스에 저장하여 연인 매칭 기능을 구현하려는 계획을 세웠습니다. 처음에는 태그 데이터를 배열로 저장하는 방식을 고려했으나, MySQL은 배열 저장을 지원하지 않아 이 방법은 실행할 수 없었습니다. 더불어 배열 형태로 데이터를 저장하게 되면 사용자 간의 관계 파악이 어려워져 태그 데이터 조회에도 문제가 발생하는 상황이었습니다.
그래서 문제를 해결하기 위해, 매핑 테이블을 도입했습니다. 이 매핑 테이블은 사용자 ID, 태그 카테고리 ID, 그리고 태그 ID를 저장하고 있습니다. 회원 테이블, 회원-태그(매핑 테이블), 그리고 태그 테이블 - 이 세 개의 테이블을 활용하여 각 엔티티 간의 관계를 명확하게 정립했습니다. 이런 방식으로 설계된 ERD는 사용자가 선택한 태그의 수가 다르더라도 데이터 조회가 용이하며, 추가적인 태그 카테고리나 태그 삽입 등 시스템 확장에도 유연하게 대응할 수 있는 구조로 만들었습니다.
댓글 페이지네이션은 어떻게할지 고민하였고 페이지네이션에는 두 종류가 있다는 것을 알게 되었습니다. 오프셋 페이지네이션과 커서 페이지네이션의 장단점을 비교한 후, 이 프로젝트에서는 댓글을 무한 스크롤로 구현을 할 것이기 때문에 커서 페이지네이션이 더 적합하다고 결론을 내렸습니다.
데이터를 약 260만개를 넣고 두 페이지네이션 방법의 조회 속도를 비교해 보았습니다.
커서 페이지네이션
오프셋 페이지네이션
엘리스에서 우수상을 수상 받았습니다. 기능 수는 적지만 처음 기획했던 기능을 모두 구현했고 팀원들과 마지막까지 버그를 수정하여 얻은 결과라고 생각합니다.
커뮤니케이션에서 중요한 요소 중 하나는 기록이라고 생각합니다. 우리는 노션에 프로젝트 페이지를 생성하여 회의록을 작성하고, 팀원들과 소통하며 개발 방향성을 정할 수 있었습니다. 이를 통해 개발 진행 상황을 공유하고, 프론트엔드의 진행 상황을 파악하여 백엔드에서 도움이 필요한 부분에 대해 지원할 수 있었습니다. 이번 프로젝트를 통해 개발 흐름을 파악하고, 이전 프로젝트에서 부족한 부분을 보완하여 적용할 수 있는 기회가 되었습니다. 덕분에 MVC 패턴으로 파일 구조화하는 방법, ERD 설계의 확장성 고려, REST API 작성 등 백엔드 개발자로서 고려해야 할 기본 사항들에 대해 깊이 생각할 수 있는 시간이 되었습니다.
추후에는 Javascript를 Typescript로 변경하여 프로젝트를 고도화해보고 싶고 Jenkins, Docker 등을 추가로 사용해보려고 합니다. 이번에 JWT 토큰으로 로그인을 설계했지만 만료시간 등을 설정하지 않아 부족한 보안을 보완할 계획입니다.