서론
프로젝트 기간 : 24년 6월 10일(월) ~ 6월 21일(금)
첫 팀 프로젝트라서 어떻게 진행해야 할 지 처음엔 감이 안 잡혀서 막막했는데
오피스아워 시간에서의 코치님들의 피드백이 도움이 많이 됐고,
팀원 전부가 열심히 참여하고 서로 도와준 덕분에 어찌저찌 잘 굴러가서 만족스러운 결과가 나온 것 같다.
2주라는 짧은 시간이었지만 진행하면서 많이 공부가 됐고 성장한 기분이 들었다.✨
프로젝트를 통해 얻게 된 큼직한 경험을 요약하자면,
1. code-formatter를 이용한 코드 스타일 통일 방법
2. gitFlow 협업 방식에 대한 이해와 merge conflict 대처
3. 네이버/ 카카오 SSO 구현 (with. passport 라이브러리)
4. AWS S3로 정적 이미지 파일 업로드 (with. multer 라이브러리)
특히 Node.js와 MongoDB(mongoose)에 대한 이해도가 확실히 많이 올라왔다.
프로젝트 진행 방식
구글링과 백엔드 공부하는 친구에게 팀 프로젝트 진행 방식에 대한 조언을 듣고 아래와 같이 진행했다.
1. (FE) figma로 UI 설계 / (BE) Postman으로 API 엔드포인트 설정
첫 주 3일이라는 기간을 잡고 이 기간에 figma UI 설계를 진행하면서 동시에 Postman API 엔드포인트를 설정했다.
상세 구현 기능에 어떠한 것들이 있는 지 파악한 후 그것을 토대로 기획을 진행했다.
Postman API 문서에 Request / Response 부분은 백엔드 코드가 완성되면 채워나가기로 하고
기획 단계에서는 example과 설명을 임시로 첨부하기로 했다.
2. 코드 셋업 / 레포지토리 initial commit
엘리스에서 제공해 준 스켈레톤 코드를 최대한 재사용하는 방향으로 가기로 결정했다.
initial commit에는 백엔드 스켈레톤 코드와 babel.config.json, package.json 등등이 포함됐다.
.gitignore에 .env를 추가하는 것도 잊지 않고 포함시켰다.
/* .gitignore */
# Dependencies
node_modules
yarn.lock
.env
이후 팀원 모두와 통일된 코드 스타일을 유지하기 위해
code formatter를 쓰기로 했고,
ESLint, Prettier 중 Prettier를 사용하기로 했다.
extension을 다운로드 받은 후 .prettierrc 파일을 셋업했다.
/* .prettierrc */
{
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always"
}
3. git 컨벤션 / 기타 규칙 토론
git 협업 방식은 gitFlow 전략을 채택했다.
develop 브랜치에서 git pull을 해온 후 feature 브랜치를 만들어 하나의 작업 단위가 완성되면 develop으로 merge 하는 식으로 진행했다.
또한 merge conflict를 최대한 방지하기 위해 아래와 같은 방법을 사용하기로 했다.
이러한 방식의 장점으로는
-- 모두가 동일한 develop 버전으로 작업할 수 있다.
-- merge conflict가 발생했을 때 팀원 모두가 문제 파일을 쉽게 추적하여 해결할 수 있다.
4. (BE) 스켈레톤 코드 분석을 통해 재사용 가능한 코드 파악
백지에서 시작하는 것보다 스켈레톤 코드 로직을 이해한 후 최대한 코드를 재사용하는 방식이
더 효율적이라고 생각하여 백엔드 코드 작성 시작 전 스켈레톤 코드 분석에 집중했다.
5. 코드 작업
매일 오전 10시에 디스코드로 모여서 어떠한 작업을 분담할 것인지를 정하여
서로 진행 상황을 공유하였다.
우리 팀은 프론트엔드와 백엔드 포지션을 따로 구분하지 않고,
풀스택처럼 개발하기로 했다.
이러한 방식은 장단점이 존재했다.
장점1. 프론트와 백에 팀원 전원이 개입하므로 서로의 진행 상황을 묻는 수고를 덜었다.
장점2. 백엔드 API의 에러/정상 여부를 프론트엔드에서 바로바로 확인할 수 있다.
단점1. 프론트와 백에 대한 지식이 모두 필요하여 개인 공부를 위한 시간 할애가 어느 정도 필요했다.
단점2. 프론트/백 작업을 스위칭할 때 사고방식 전환을 위한 시간이 어느 정도 소요됐다.
프로젝트에서 배운 것들
1. Prettier 설정
/* .prettierrc */
{
"printWidth": 120, // 한 줄의 최대 길이: 120자
"tabWidth": 2, // 탭: 스페이스바 2칸
"useTabs": false, // 탭 대신 스페이스바 입력 사용
"semi": true, // 모든 문장의 끝에 세미콜론
"singleQuote": true, // 문자열 작은따옴표 처리
"jsxSingleQuote": true, // JSX 문자열 작은따옴표 처리
"trailingComma": "all", // 객체 마지막 요소에 쉼표 추가
"bracketSpacing": true, // 객체 리터럴에서 중괄호 사이에 공백 추가
"bracketSameLine": false, // 닫는 괄호를 새 줄에 배치
"arrowParens": "always" // 화살표 함수 매개변수에 괄호 항상 적용
}
2. merge conflict 대처
머지 충돌이 일어나 기존 코드 or 새로운 코드 적용의 갈림길에 섰을 때
처음에는 당황했지만 몇 번 겪어보니 이후로는 사소한 이슈였다.
팀원들과의 소통을 통해 어떤 코드를 적용할 지를 물어보고 interactive mode 또는 edit inline으로 수정해줬다.
수정을 반영한 뒤 merge request를 close하고 다시 open하여 해결했다.🔥
3. 네이버 / 카카오 SSO 구현
사용자가 카카오 로그인과 네이버 로그인을 할 수 있도록 기능을 구현했다.
사용한 라이브러리는 다음과 같다.
npm install passport
npm install passport-kakao
npm install passport-naver-v2
아래의 블로그를 참고하여 만들었다.
카카오 로그인을 구현한 과정은 다음과 같다.
https://developers.kakao.com/console/app
Kakao Developers 에 들어가 내 애플리케이션을 생성한다.
카카오 로그인 서비스를 이용하기 위한 세팅을 해준다.
중요한 것은 동의항목과 Redirect URI인데,
동의 항목을 설정하고
Redirect URI는 아래와 같이 해줬다.
이제 사이트에서 카카오 로그인 버튼을 클릭하면 해당 URI로 redirect가 된다.
카카오 애플리케이션 세팅을 마치고 코드 로직을 구현했다.
(네이버 로그인 구현도 카카오와 크게 다르지 않으므로 네이버 세팅 과정은 생략)
import { Router } from 'express';
import session from 'express-session';
import passport from 'passport';
import MongoStore from 'connect-mongo';
import { authRouter } from './auth-router';
import '../passport/kakao-strategy';
import '../passport/naver-strategy';
const userRouter = Router();
// passport 라이브러리 셋팅
userRouter.use(passport.initialize());
userRouter.use(
session({
secret: process.env.SESSION_SECRET, // 세션 id를 암호화
resave: false, // 요청 이벤트마다 사용자의 session을 갱신할 지 여부
saveUninitialized: false, // 비로그인 시에도 세션 생성해줄 지 여부
cookie: { maxAge: 24 * 60 * 60 * 1000 }, // maxAge: [ms] => 세션 만료 기한 설정
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URL,
dbName: 'test',
}),
}),
);
userRouter.use(passport.session());
// auth 라우팅
userRouter.use('/auth', authRouter);
Redirect URI가 /users/auth 이므로, authRouter에 코드 제어권이 넘어간다.
// auth-router.js
import { Router } from 'express';
import passport from 'passport';
const authRouter = Router();
authRouter.get('/kakao', passport.authenticate('kakao'));
authRouter.get(
'/kakao/callback',
// passport 로그인 전략에 의해 kakaoStrategy로 가서 카카오계정 정보와 DB를 비교해서 회원가입시키거나 로그인 처리하게 한다.
passport.authenticate('kakao', {
failureRedirect: '/', // kakaoStrategy에서 실패한다면 실행
}),
// kakaoStrategy에서 성공한다면 콜백 실행
(req, res) => {
res.redirect('/');
},
);
authRouter.get(
'/naver',
passport.authenticate('naver', {
authType: 'reprompt',
}),
);
authRouter.get(
'/naver/callback',
// passport 로그인 전략에 의해 naverStrategy로 가서 네이버계정 정보와 DB를 비교해서 회원가입시키거나 로그인 처리하게 한다.
passport.authenticate('naver', { failureRedirect: '/' }),
(req, res) => {
res.redirect('/');
},
);
export { authRouter };
auth-router에서는 사용자가 유효한 카카오/네이버 계정인 지 확인한 후
유효하다면 로그인을 시켜야 하는데
DB에 이미 있는 회원 정보라면 바로 로그인시키고,
DB에 없는 회원 정보라면 DB에 유저정보를 CREATE한 후 로그인시키도록 했다.
4. AWS S3로 이미지 파일 업로드
보여줄 이미지를 쉽게 가져오기 위해
AWS S3(파일저장용 클라우드 서비스) 에 이미지 파일을 업로드하는 방식을 사용했다.
우선 AWS 사이트에 들어가 가입 및 카드 등록을 하고
IAM에 들어가 사용자 등록을 하고 Access Key를 발급받는다.
그 다음 S3로 들어가 버킷을 생성한다.
그리고 퍼블릭 액세스를 허용해 둔 후(현재는 모두 차단해 둠)
버킷 정책을 세팅합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "1",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::버킷이름/*"
},
{
"Sid": "2",
"Effect": "Allow",
"Principal": {
"AWS": "ARN 입력"
},
"Action": [
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::버킷이름/*"
}
]
}
"Sid": "1" => 모든 유저가 GET 요청 가능
"Sid": "2" => 관리자만 PUT, DELETE 요청 가능
에 대한 설정을 "Action": "s3:[Get/Put/Delete]Object" 로 해두었다.
마지막으로 CORS 설정을 한다.
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"PUT",
"POST"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [
"ETag"
]
}
]
AllowedOrigins은 자료를 PUT/DELETE할 수 있는 사이트를 적으면 되는데
개발 중에는 "*" (모두 허용)으로 해두고 지금은 막아두었다.
AWS 세팅을 모두 마치고 코드 로직을 구현했다.
라이브러리를 설치하고
npm install multer multer-s3 @aws-sdk/client-s3
해당 라우터에서 S3 자동 업로드에 필요한 라이브러리 세팅을 했다.
import { S3Client } from '@aws-sdk/client-s3';
import multer from 'multer';
import multerS3 from 'multer-s3';
const s3 = new S3Client({
region: 'ap-northeast-2', // 지역(region) : Seoul
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID, // S3 액세스 키
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY, // S3 비밀 액세스 키
},
});
const upload = multer({
storage: multerS3({
s3: s3,
bucket: process.env.S3_BUCKET_NAME, // 버킷명
key: (req, file, cb) => {
cb(null, `${Date.now()}_${file.originalname}`); // 파일명은 원본 파일명과 타임스탬프를 결합하여 설정
},
}),
});
upload에는 S3 storage에 저장할 때 어떠한 버킷에 저장해둘 지(bucket),
이름은 어떻게 저장할 지를 콜백 함수 내에 적어준다.
나는 파일명이 겹치지 않도록, 타임스탬프 + 파일명 으로 저장되도록 했다.
그 다음 프론트엔드 코드에서 업로드할 이미지 원본을 가져온다.
<input name="image-file" class="file-input" id="imageInput" type="file" accept=".png, .jpeg, .jpg" />
모든 input tag에는 name 어트리뷰트를 지정해두고,
form 을 제출할 때 자바스크립트에서 HTML FormData API를 이용해 정보를 수집한다.
const formData = new FormData(form);
const name = formData.get('name'); // 제품 이름
const category = formData.get('category'); // 카테고리
const manufacturer = formData.get('manufacturer'); // 제조사
const description = formData.get('description'); // 제품 설명
const imageFile = formData.get('image-file'); // 제품 사진
const price = formData.get('price'); // 가격
// 입력값 검증 로직
// ...생략
// 상품등록 api 요청
try {
await fetch('/products/product', {
method: 'POST',
body: formData,
});
alert(`정상적으로 등록되었습니다.`);
// 상품리스트로 이동
window.location.href = '/productlist';
} catch (err) {
console.error(err.stack);
}
그 다음 multer 라이브러리 세팅 코드 밑에 상품등록 API를 적고
미들웨어로 upload.single('이미지 name 어트리뷰트') 를 추가해주면
자동으로 AWS S3에 원본 이미지가 저장되고 생성된 원본 사진 링크를 DB에 저장할 수 있다.
const productRouter = Router();
// 상품등록 api (아래는 /product이지만, 실제로는 /products/product로 요청해야 함. ->app.js에 경로 설정할수있음 질문환영)
// 사진 이미지 S3에 업로드할 수 있게 미들웨어로 추가
productRouter.post('/product', upload.single('image-file'), async (req, res, next) => {
try {
// Content-Type: application/json 설정을 안 한 경우, 에러를 만들도록 함.
// application/json 설정을 프론트에서 안 하면, body가 비어 있게 됨.
if (is.emptyObject(req.body)) {
throw new Error('headers의 Content-Type을 application/json으로 설정해주세요');
}
// req (request)의 body 에서 데이터 가져오기
const { name, category, manufacturer, description, price } = req.body;
// req.file.location에서 이미지 링크 가져오기
const imageUrl = req.file.location;
// 위 데이터를 유저 db에 추가하기(서비스 품목에 상품등록 서비스를 이용함)
const newProduct = await productService.addProduct({
name,
category,
manufacturer,
description,
price,
imageUrl,
});
// 추가된 유저의 db 데이터를 프론트에 다시 보내줌
// 물론 프론트에서 안 쓸 수도 있지만, 편의상 일단 보내 줌
res.status(201).json(newProduct);
} catch (error) {
next(error);
}
});
마치며
첫 팀프로젝트라 걱정이 많았지만 하다보니까 어찌저찌 잘 굴러가는(?) 게 너무 신기했다.
프로젝트하는 2주 내내 새벽 1-2시까지 기능 구현하고, 회의하고, 구글링하고,....
확실히 팀 프로젝트에 대한 두려움은 사라졌지만
앞으로 있을 2차 팀 프로젝트에서는 아래와 같은 목표를 가지고 더 좋은 결과를 내보고 싶다.
1. figma 툴 다루는 연습하기
2. CSS flex, grid 등 반응형 디자인 더 공부하기
3. 코드 가독성/유지보수 측면에 좀 더 신경쓰는 습관 기르기(기능 구현에 급급한 나머지 어떻게 짰는 지 뒤돌아보면 이해 안감)
마지막으로 최종 결과물 소개하면서 마칩니다. 읽어주셔서 감사하고 잘못된 정보가 있다면 지적 환영합니다😊