1. 백엔드 개발
가. 기본설정
1) DB 연동
•
mongoose 설치
•
Git 저장소 내 basic_react_node의 todolist app의 sample 코드 활용
2) 파일 구조 생성
3) DB models 생성
나. 회원 관련 API 구현
1) /api/users/register
•
User model import(→ index.js)
•
requested 회원 정보를 DB에 저장
•
처리 결과 response
// index.js
const { User } = require('./models/User');
app.post('/api/users/register', (req, res) => {
const user = new User(req.body);
user.save((err, userInfo) => {
if (err) return res.json({ success: false, err });
return res.status(200).json({ success: true });
});
});
JavaScript
복사
•
Hashed password(feat. bcrypt)
→ bcrypt module 설치
→ pre 메소드 활용하여 DB save 전에 아래 로직 실행
→ user 스키마 내 password filed의 생성, 수정 시에만 아래 로직 실행
→ plain password를 해쉬처리하고 salt를 더해서 user.password에 저장
→ save 메소드로 이동(next())
// models/User.js
const bcrypt = require('bcrypt');
const saltRounds = 10;
userSchema.pre('save', function (next) {
let user = this;
if (user.isModified('password')) {
bcrypt.genSalt(saltRounds, function (err, salt) {
if (err) return next(err);
bcrypt.hash(user.password, salt, function (err, hash) {
if (err) return next(err);
user.password = hash;
next();
});
});
} else {
next();
}
});
JavaScript
복사
2) /api/users/login
•
User schema 부분 로직
→ comparePassword 메소드 정의
userSchema.methods.comparePassword = function (plainPassword, callback) {
// 사용자에 의해 입력된 비밀번호와 DB 내 비밀번호 간 비교
bcrypt.compare(plainPassword, this.password, function (err, isMatch) {
if (err) return callback(err);
callback(null, isMatch);
});
};
JavaScript
복사
→ generateToken 메소드 정의
userSchema.methods.generateToken = function (callback) {
let user = this;
// jsonwebtoken 활용하여 signed token 생성
let token = jwt.sign(user._id.toHexString(), 'secretToken');
user.token = token;
user.save(function (err, user) {
if (err) return callback(err);
callback(null, user);
});
};
JavaScript
복사
•
Index 부분 로직
// index.js
app.post('/api/users/login', (req, res) => {
// 1. 요청된 이메일 주소를 DB에서 검색(User schema 내 이메일 중 요청 이메일 주소 검색)
User.findOne({ email: req.body.email }, (err, userInfo) => {
// 요청한 이메일 주소가 없다면
if (!userInfo) {
return res.json({
loginSuccess: false,
message: '해당하는 이메일이 없습니다.',
});
}
// 2. 요청한 이메일과 일치하는 user가 DB 내 있다면, user 비밀번호 일치여부 확인
userInfo.comparePassword(req.body.password, (err, isMatch) => {
if (!isMatch)
return res.json({
loginSueccess: false,
message: '비밀번호가 틀렸습니다.',
});
// 3. 비밀번호 일치 시 토큰 생성
userInfo.generateToken((err, userInfo) => {
if (err) return res.status(400).send(err);
// 쿠키에 token 저장
return (
res
.cookie('x_auth', userInfo.token)
.status(200)
.json({ loginSuccess: true, userId: userInfo._id, userToken: userInfo.token })
);
});
});
});
});
JavaScript
복사
3) /api/users/auth
•
인증 기능의 필요성: client에서 페이지 이동 시, 권한 확인(일반 User or Admin) 또는 특정 기능 권한 확인(게시물 업로드 또는 삭제 등)
•
기본 로직
1.
client browser 내 cookie에 저장된 Token을 Server로 전송
2.
Token을 복호화하여 UserID 확인
3.
해당 UserID가 DB 내 있는 지 확인
4.
DB 내 User collection의 token filed와 cookie에서 가져온 token 비교하여 인증여부 확인
5.
인증 확인에 따른 유저 정보 갱신
// auth.js
const { User } = require("../models/User");
let auth = (req, res, next) => {
let token = req.cookies.x_auth;
User.findByToken(token, (err, user) => {
console.log(user);
if (err) throw err;
if (!user) return res.json({ isAuth: false, error: true });
req.token = token;
req.user = user;
next();
});
};
module.exports = { auth };
JavaScript
복사
// User.js
userSchema.statics.findByToken = function (token, callback) {
let user = this;
jwt.verify(token, "secretToken", function (err, decoded) {
user.findOne({ _id: decoded, token: token }, function (err, user) {
if (err) return callback(err);
callback(null, user);
});
});
};
JavaScript
복사
// index.js
// auth에서 cookie 처리 위해 관련 모듈 설치
const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.get("/api/users/auth", auth, (req, res) => {
res.status(200).json({
_id: req.user._id,
isAdmin: req.user.role === 0 ? false : true,
isAuth: true,
email: req.user.email,
name: req.user.name,
lastname: req.user.lastname,
role: req.user.role,
image: req.user.image,
});
});
JavaScript
복사
4) /api/users/logout
•
기본 로직
1.
로그아웃 해당 유저를 DB에서 검색
2.
DB 내 해당 유저 filed 내 token 삭제
// index.js
app.get('/api/users/logout', auth, (req, res) => {
User.findOneAndUpdate({ _id: req.user._id }, { token: '' }, (err, user) => {
if (err) return res.json({ success: false, err });
return res.status(200).send({ success: true });
});
});
JavaScript
복사
다. 작품 관리 API 구현
기본 설정) express는 사용자가 업로드한 파일을 저장하는 기능을 제공하지 않으므로 multer 모듈을 설치함(npm install --save multer)
1) Work Schema 생성
const workSchema = mongoose.Schema(
{
// 게시글 작성자
writer: {
type: Schema.Types.ObjectId,
ref: 'User',
},
// 작가 이름
author: {
type: String,
},
// 작품 이름
title: {
type: String,
maxlength: 50,
},
// 작품 설명
description: {
type: String,
},
// 작품 이미지
WorkImages: {
type: Array,
default: [],
},
// 작가 이미지
AuthorImage: {
type: Array,
default: [],
},
// 작품 가격
price: {
type: Number,
default: 0,
},
// 판매 수량
sold: {
type: Number,
maxlength: 100,
default: 0,
},
// 게시글 조회수
views: {
type: Number,
default: 0,
},
},
{ timestamps: true }
);
JavaScript
복사
2) /api/works/image
•
muter 활용하여 server 내 특정 경로에 클라이언트로부터 이미지 파일을 저장함
var storage = multer.diskStorage({
// 저장될 파일 경로
destination: (req, file, cb) => {
cb(null, 'uploads/test');
},
// 파일 이름
filename: (req, file, cb) => {
cb(null, `${Date.now()}_${file.originalname}`);
},
// 파일 확장자 기준으로 필터링
fileFilter: (req, file, cb) => {
const ext = path.extname(file.originalname);
if (ext !== '.mp4') {
return cb(res.status(400).end('only jpg, png, mp4 is allowed'), false);
}
cb(null, true);
},
});
var upload = multer({ storage: storage }).single('file');
router.post('/image', (req, res) => {
upload(req, res, (err) => {
if (err) {
return res.json({ success: false, err });
}
return res.json({
success: true,
filePath: res.req.file.path,
fileName: res.req.file.filename,
});
});
});
JavaScript
복사
3) /api/works/uploadWork
router.post('/uploadWork', (req, res) => {
const work = new Work(req.body);
work.save((err) => {
if (err) return res.status(400).json({ success: false, err });
return res.status(200).json({ success: true });
});
});
JavaScript
복사
4) /api/works/getWorks
가) 서버에서 요청을 받아서 DB 내 관련 정보 찾아서 client로 response하기
•
populate: 인자로 받은 키워드의 세부정보를 찾아줌
router.post('/getWorks', (req, res) => {
GalleryWork.find().exec((err, workInfo) => {
if (err) return res.status(400).send(err);
res.status(200).json({ success: true, workInfo });
});
});
JavaScript
복사
5) /api/works/work_by_id
나) Server side에서 특정 work에 대한 상세정보 처리
•
Get 방식 요청에 대한 상세 쿼리를 살필 때는 req.query 사용(반면 POST 방식은 req.body)
router.get('/work_by_id', (req, res) => {
let workId = req.query.id;
let workType = req.query.type;
GalleryWork.find({ _id: workId })
.populate('writer')
.exec((err, workDetailInfo) => {
if (err) return res.status(400).json({ success: false });
res.status(200).json({ success: true, workDetailInfo });
});
});
JavaScript
복사
라. 장바구니 API 구현
1) User Schema 확장
가) history, cart 필드 추가
•
cart 필드: 장바구니에 추가된 목록 관리
•
history: 결제 내역 및 재고 관리
•
상품 아이디, 상품 개수, 일자 정보를 각 필드에 삽입
2) action 함수로부터 받은 매개변수를 바탕으로 처리
1.
User Collection에서 현재 로그인한 유저의 id 찾기
2.
장바구니에 있는 모든 상품을 순회하여 클라이언트로부터 받은 상품과 중복되는 것이 있다면 duplicate 변수에 true 할당
2-1. 중복 상품이 존재한다면, 해당 유저의 중복되는 상품의 quantity 필드를 1만큼 증가하고 옵션(new: true)에 따라 그 결과를 콜백함수에 반환함
2-2. 중복 상품이 존재하지 않는다면, 해당 유저에 cart 필드를 push 한고 그 결과를 콜백함수에 반환함
router.post('/addToCart', auth, (req, res) => {
// 1. User Collection에서 해당 유저의 정보 가져오기
// 1-1. auth(middleware) 거치면서 req.user._id를 받아올 수 있음
User.findOne({ _id: req.user._id }, (err, userInfo) => {
// 2. 가져온 정보에서 장바구니에 넣으려는 상품이 존재하는지 확인
let duplicate = false;
userInfo.cart.forEach((item) => {
if (item.id === req.body.productId) {
duplicate = true;
}
});
// 2-1. 존재한다면
if (duplicate) {
User.findOneAndUpdate(
{ _id: req.user._id, 'cart.id': req.body.productId },
{ $inc: { 'cart.$.quantity': 1 } },
{ new: true },
(err, userInfo) => {
if (err) return res.status(200).json({ success: false, err });
res.status(200).send(userInfo.cart);
}
);
}
// 2-2. 존재하지 않는다면
else {
User.findOneAndUpdate(
{ _id: req.user._id },
{
$push: {
cart: {
id: req.body.productId,
quantity: 1,
date: Date.now(),
},
},
},
{ new: true },
(err, userInfo) => {
if (err) return res.status(400).json({ success: false, err });
res.status(200).send(userInfo.cart);
}
);
}
});
});
JavaScript
복사
3) auth API의 response 값 수정
•
다른 API에서 auth middleware 사용하여 유저정보를 받기 때문에 auth response 값에 cart 및 history 필드를 추가한다.
•
추가하지 않으면 장바구니 페이지에서 다른 페이지로 이동 시, redux store에서 cart 및 history 필드 값이 사라지는 것을 확인할 수 있다
4) /api/works/works_by_id API 추가
•
기존 work_by_id API 개선, 기존에는 GalleryWork collection 내 특정 데이터 검색 시 한 개의 id만을 기준으로 찾았다면, 개선 후에는 인자로 array 전달 가능
router.get('/works_by_id', (req, res) => {
let workIds = req.query.id;
let workType = req.query.type;
if (workType === 'array') {
let ids = req.query.id.split(',');
workIds = ids.map((id) => {
return id;
});
}
GalleryWork.find({ _id: { $in: workIds } })
.populate('writer')
.exec((err, workDetailInfo) => {
if (err) return res.status(400).json({ success: false });
res.status(200).json({ success: true, workDetailInfo });
});
});
JavaScript
복사
2. 프런트엔드 개발
가. 기본설정
1) React 설치(npx create-react-app .)
2) Router Dom 설정
•
종속성 추가(react-router-dom)
•
// App.js
import React from 'react';
import LandingPage from "./components/views/LandingPage/LandingPage.js";
import LoginPage from "./components/views/LoginPage/LoginPage.js";
import RegisterPage from "./components/views/RegisterPage/RegisterPage.js";
import {
BrowserRouter as Router,
Switch,
Route,
} from "react-router-dom";
function App() {
return (
<Router>
<div>
<Switch>
<Route exact path="/" component={LandingPage} />
<Route exact path="/login" component={LoginPage} />
<Route exact path="/register" component={RegisterPage} />
</Switch>
</div>
</Router>
);
}
export default App;
JavaScript
복사
3) Proxy server 설정
•
종속성 추가(ttp-proxy-middleware)
4) server
client 간 통신 테스트
•
종속성 추가(axios)
5) Antd 종속성 추가
•
종속성 추가(antd)
6) Redux 적용
•
종속성 추가(redux, react-redux, redux-promis, redux-thunk)
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { BrowserRouter } from "react-router-dom";
import 'antd/dist/antd.css'
import Reducer from "./_reducers";
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import promiseMiddleware from "redux-promise";
import ReduxThunk from "redux-thunk";
const createStoreWithMiddleware = applyMiddleware(
promiseMiddleware,
ReduxThunk
)(createStore);
ReactDOM.render(
<Provider
store={createStoreWithMiddleware(
Reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
)}
>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById("root")
);
serviceWorker.unregister();
JavaScript
복사
//_reducers/index.js
import { combineReducers } from 'redux';
// import user from './user_reducer';
const rootReducer = combineReducers({
// user,
});
export default rootReducer;
JavaScript
복사
나. 로그인/회원가입 페이지 개발
1) 로그인 페이지
•
JSX part
→ 이메일, 비밀번호, 버튼(Login) 생성 및 반환
•
event handling part
→ useState 활용하여 이메일, 비밀번호 상태 업데이트
→ form 내 submit handler 활용하여 이메일, 패스워드 post request(→ Server)
→ 서버 통신 결과(reponse 값)에 따라 로그인 성공 시 landingPage('/') 이동 및 에러 처리
props.history.push 사용할 때, withRouter 모듈을 사용할 것
// LoginPage.js
import { withRouter } from "react-router-dom";
// 생략
export default withRouter(LoginPage);
JavaScript
복사
•
state managing part
→ dispatch 함수 활용하여 action 전달(→reducers/user_reducer)
→ user_reducer에서 action.type에 따라 상태 업데이트함
→ 위에서 처리한 결과를 store에 저장함
2) 회원가입 페이지
•
JSX part
→ 이메일, 비밀번호, 버튼(Login) 생성 및 반환
•
event handling part
→ useState 활용하여 이메일, 비밀번호 상태 업데이트
→ form 내 submit handler 활용하여 이메일, 패스워드 post request(→ Server)
→ 서버 통신 결과(reponse 값)에 따라 로그인 성공 시 landingPage('/') 이동 및 에러 처리
•
state managing part
→ dispatch 함수 활용하여 action 전달(→user_reducer)
→ user_reducer에서 action.type에 따라 상태 업데이트함
→ 위에서 처리한 결과를 store에 저장함
3) 로그아웃
•
JSX part
→ Landing Page 내 로그아웃 버튼 생성
•
event handling part
→ 버튼 내 event handler 활용하여 버튼 클릭 시 get request(→ server)
→ 서버로부터 response 받아서 로그인 성공여부 확인
→ 성공 시, Login Page로 이동 및 에러 처리
•
state managing part: 없음
4) 인증
•
JSX part
→ App.js 내 Route 태그 내 Auth(HOC) 메소드 적용하여 전 페이지에 걸쳐 인증로직 적용
→ Auth 두번째 매개변수(option): null: 아무나 출입 가능, true: only 로그인 유저 출입 가능, false: 로그인 유저도 출입 X
→ Auth 세번째 매개변수(default = null): true 입력 시, 특정 페이지는 관리자만 입장 가능
import Auth from "./hoc/auth";
<Router>
<div>
<Switch>
<Route exact path="/" component={Auth(LandingPage, null)} />
<Route exact path="/login" component={Auth(LoginPage, false)} />
<Route exact path="/register" component={Auth(RegisterPage, false)} />
</Switch>
</div>
</Router>
JavaScript
복사
•
state managing part
→ AuthenticationCheck 동작 원리
// server/index.js
app.get("/api/users/auth", auth, (req, res) => {
// auth에서 인증로직 통과 후 다음의 코드 실행
res.status(200).json({
_id: req.user._id,
isAdmin: req.user.role === 0 ? false : true,
isAuth: true,
email: req.user.email,
name: req.user.name,
lastname: req.user.lastname,
role: req.user.role,
image: req.user.image,
});
});
JavaScript
복사
// client/auth.js
function AuthenticationCheck(props) {
const dispatch = useDispatch();
useEffect(() => {
dispatch(auth()).then((response) => {
console.log(response);
if (!response.payload.isAuth) {
if (option) {
props.history.push("/login");
}
} else {
if (adminRoute && !response.payload.isAdmin) {
props.history.push("/");
} else {
if (option === false) props.history.push("/");
}
}
});
}, []);
return <SpecificComponent />;
}
JavaScript
복사
◦
Auth 컴포넌트 mount 시 useEffect 함수로 인증로직 실행
→ dispatch 함수 활용하여 action 전달(→_user_reducer)
user_reducer에서 action.type에 따라 상태 업데이트함
위에서 처리한 결과를 store에 저장함
→ 리라우팅 작업
1. 인증되지 않은 유저일 경우, 로그인 페이지로 push
2. 관리자 권한이 없는 유저일 경우, 시작 페이지로 push
3. 로그인 유저가 출입할 필요 없는 페이지일 경우(ex 로그인 페이지), 시작 페이지로 push
// auth.js
useEffect(() => {
dispatch(auth()).then((response) => {
console.log(response);
if (!response.payload.isAuth) {
if (option) {
props.history.push("/login");
}
} else {
if (adminRoute && !response.payload.isAdmin) {
props.history.push("/");
} else {
if (option === false) props.history.push("/");
}
}
});
}, []);
JavaScript
복사
다. 작품 업로드 페이지 개발
1) 기본설정(페이지 생성. 라우팅 처리, 종속성 설치(react-dropzone))
2) Work Schema 생성
3) JSX 부분
•
UploadWorkPage, 기본 input 값 처리
<Form>
<label>작가 이미지</label>
<FileUpload refreshFunction={updateAuthorImage} />
<br />
<br />
<label>작품 이미지</label>
<FileUpload refreshFunction={updateWorkImages} />
<br />
<br />
<label>작가 이름</label>
<Input onChange={handleChangeAuthor} value={Author} />
<br />
<br />
<label>작품 이름</label>
<Input onChange={handleChangeWorkTitle} value={WorkTitle} />
<br />
<br />
<label>작품 설명</label>
<TextArea onChange={handleChangeDecsription} value={Description} />
<br />
<br />
<label>가격($)</label>
<Input type="number" onChange={handleChangePrice} value={Price} />
<br />
<br />
<select onChange={handleChangeGenre} value={Genre}>
{Genres.map((genre) => (
// Genre State 값이 숫자이므로 value에 genre.key 삽입
<option key={genre.key} value={genre.key}>
{genre.value}
</option>
))}
</select>
<br />
<br />
<Button onClick={onSubmit}>확인</Button>
</Form>
JavaScript
복사
•
FileUpload, Image input 처리(Dropzone 활용)
<Dropzone onDrop={dropHandler}>
{({ getRootProps, getInputProps }) => (
<div
style={{
width: "300px",
height: "240px",
border: "1px solid lightgray",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
{...getRootProps()}
>
<input {...getInputProps()} />
<PlusOutlined style={{ fontSize: "3rem" }} />
</div>
)}
</Dropzone>
JavaScript
복사
4) event handling 부분
•
onSubmit에서 모든 input 값을 한번에 전달
•
필수 입력 값이 비어있는 경우 alert 반환
const onSubmit = (event) => {
event.preventDefault();
if (!WorkTitle || !Description || !Price || !Genre || !WorkImages) {
return alert("모든 값을 입력하세요");
}
const body = {
writer: props.user.userData._id,
author: Author,
title: WorkTitle,
description: Description,
price: Price,
WorkImages: WorkImages,
AuthorImage: AuthorImage,
genre: Genre,
};
axios.post("/api/works/uploadWork", body).then((response) => {
console.log(response);
if (response.data.success) {
alert("작품 업로드에 성공했습니다");
props.history.push("/");
} else {
alert("작품 업로드에 실패했습니다");
}
});
};
JavaScript
복사
라. 메인 페이지 개발
1) DB 내 모든 work 정보 가져오기
가) useEffect 활용하여 LandingPage 접근 시 바로 원하는 정보를 렌더링
나) useEffect 내 axios 활용하여 post 방식으로 요청
2) 카드 컴포넌트 만들기
가) 렌더링 컴포넌트 함수 만들기
나) 반응형 적용하기
3) 이미지 슬라이더 만들기
가) Carousel from antd 활용
•
utils에 새로운 컴포넌트 생성하여 해당 기능 적용(ImageSlider)
•
autoplay 옵션 적용
마. 작품 상세 페이지 개발
1) 상세정보를 DB에서 가져오기
가) DetailWorkPage 생성
나) LandingPage 내 Card 컴포넌트 내 cover 영역에 a 태그 걸기
•
각 work의 고유 id(work._id)를 식별자로 삽입
다) Client Route 설정(Client side에서 이동하는 것이므로 such as Nav)
•
Detail Page는 각 work마다 아이디 값이 다르므로 동적 URI 할당(work/:workId)
•
workId 값은 DetailWorkPage에서 URI 내 Id값을 가져올 때 사용함
라) Client Side에서 특정 work에 대한 상세정보를 서버에 요청
•
특정 work에 대한 Id는 props.match.params 사용
•
URI로 특정 work에 대한 정보를 담아서 요청할 것이므로 get 방식 사용
•
type=single로 설정하는 이유: 추후 사용
2) WorkImage 컴포넌트 만들기
가) 종속성 설치(react-image-gallery)
나) DetailWorkPage 작업
•
JSX 부분에 기본 style 적용
•
서버로부터 받은 정보 State으로 관리
•
반응형 웹 컴포넌트 추가
다) WorkImage 컴포넌트 작업
•
props로 DetailWorkPage 내 State 정보 받아오기
•
react-image-gallery 적용
◦
npm react-image-gallery 문서 참고(기본 코드 및 css 가져오와서 index.css 적용)
•
useEffect의 [] 인자로 props.detail 을 넣어야 정상적으로 렌더링이 되는 이유
→ useEffect는 첫번째 렌더링을 포함한 모든 렌더링 후에 실행됨. 첫번째 렌더링 때, props.detail을 부모 컴포넌트로부터 받지 않았기 때문에 WorkImages에는 아무값도 들어있지 않다. 하지만 후에 부모 컴포넌트에서 props에 해당하는 값을 서버로부터 받은 후 WorkImage 컴포넌트에 props로 특정 값이 전달되어 WorkImages State 안에 의미 있는 값이 들어오게 된다.
3) WorkInfo 컴포넌트 만들기
가) DetailWorkPage 작업
•
JSX 부분에 기본 style 적용
•
서버로부터 받은 정보 State으로 관리
•
반응형 웹 컴포넌트 추가
나) WorkInfo 컴포넌트 작업
•
props로 DetailWorkPage 내 State 정보 받아오기
•
antd의 Descriptions 컴포넌트 활용
바. 장바구니 페이지 개발
1) redux 적용
가) 컴포넌트 내 이벤트 발생 및 dispatch 함수 호출
// WorkInfo.js
const dispatch = useDispatch();
const clickHandler = () => {
dispatch(addToCart(props.detail._id));
};
JavaScript
복사
나) action 함수 정의 및 실행(dispatch 함수의 매개변수로 action 함수 전달)
•
action 함수는 State의 상태변화를 위한 참고
•
서버에서 처리한 값을 반환함
// user_reducer
export function addToCart(id) {
let body = {
productId: id,
};
const request = axios
.post(`${USER_SERVER}/addToCart`, body)
.then((response) => response.data);
return {
type: ADD_TO_CART,
payload: request,
};
}
JavaScript
복사
다) reducer 함수 실행
•
action 함수의 반환값을 참고하여 State 변화
•
return 값 해석
◦
return {...state, userData:{}} : 기존 User state에 같은 데이터 형식의 state을 추가함
◦
userData: {...state.userData, cart: action.payload}: 특정 User State의 데이터를 그대로 사용하되 cart filed에 action 함수의 return 값을 추가함
ex) gentleman@gmail.com 필드를 가지고 있는 user instance에 cart 필드의 내용을 추가함
case ADD_TO_CART:
return {
...state,
userData: {
...state.userData,
cart: action.payload,
},
};
JavaScript
복사
라) 페이지 라우팅(CartPage)
•
views/CartPage/CartPage.js
•
App.js에서 Route 설정
마) rightMenu에 탭 추가
2) DB에 카트페이지에 필요한 정보 요청하기
가) User Collection의 cart 필드 안의 각 work id를 기준으로 각 상품에 대한 정보 가져오기
•
현재 로그인한 유저의 User State 안에 cart 상품이 있는 지 확인
•
있다면, 모든 work id를 배열에 저장(cartItems)
•
주의할 점) props는 상위 컴포넌트로부터 받아오는 변수이므로 즉각적으로 받아올 수 없다. 따라서 useEffect의 매개변수로 입력하여 해당 정보가 들어오면 이하 로직을 실행할 수 있도록 해야 함
// CartPage.js
function CartPage(props) {
useEffect(() => {
let cartItems = [];
// 1. 로그인한 유저의 카트 필드에 한개 이상의 데이터가 있다면
if (props.user.userData && props.user.userData.cart) {
if (props.user.userData.cart.length > 0) {
// 1-1. cartItems 배열에 각 카트 필드 내 work id 정보 삽입
props.user.userData.cart.forEach((cartItem) => {
cartItems.push(cartItem.id);
});
}
}
}, [props.user.userData]);
JavaScript
복사
나) dispatch 함수 호출: cartItems와, cart 필드를 매개변수로한 Action 전달
•
cartItems 외 cart가 필요한 이유: 장바구니 페이지에 cart 내 quantity 정보를 렌더링해야 하기 때문에
다) Action 함수 정의: 각각의 cartItem에 대한 work info를 DB로부터 받아오고, cart의 quantity 정보를 추가한 데이터를 반환함
// user_actions.js
export function getCartItems(cartItems, userCart) {
const request = axios
.get(`${WORK_SERVER}/works_by_id?id=${cartItems}&type=array`)
.then((response) => {
userCart.forEach((cartItem) => {
response.data.workDetailInfo.forEach((workDetail, index) => {
if (cartItem.id === workDetail._id) {
response.data.workDetailInfo[index].quantity = cartItem.quantity;
}
});
});
return response.data.workDetailInfo;
});
JavaScript
복사
라) Reducer를 활용한 상태변화: Action에서 반환받은 데이터를 이용하여 상태변화 발생
•
기존 user-userData State에 user 이하 cartDetail State을 추가함
// user_reducer
case GET_CART_ITEMS:
return { ...state, cartDetail: action.payload };
JavaScript
복사
3) 결제 기능 추가
가) Paypal 버튼 추가
•
종속성 설치 및 관련 라이브러리 가져오기
3. 배포
가. Docker 기반 개발환경 테스트
1) client docker file
FROM node:alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY ./ ./
CMD [ "npm", "run", "start" ]
Docker
복사
2) nginx docker file
FROM nginx
COPY ./default.conf /etc/nginx/conf.d/default.conf
Docker
복사
3) server docker file
FROM node:alpine
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "start"]
Docker
복사
4) docker-compose file
version: "3"
services:
client:
build:
dockerfile: Dockerfile.dev
context: ./client
container_name: smart_gallery_client
volumes:
- /smart_gallery/node_modules
- ./client:/smart_gallery
stdin_open: true
nginx:
restart: always
build:
dockerfile: Dockerfile
context: ./nginx
container_name: smart_gallery_nginx
ports:
- "3000:80"
server:
build:
dockerfile: Dockerfile.dev
context: ./server
container_name: smart_gallery_server
volumes:
- /smart_gallery/node_modules
- ./server:/smart_gallery
Docker
복사
5) AWS S3 저장소로 이미지 파일 경로 수정
•
server side
const path = require('path');
const multer = require('multer');
const multerS3 = require('multer-s3');
const AWS = require('aws-sdk');
AWS.config.loadFromPath(__dirname + '/../config/awsconfig.json');
let s3 = new AWS.S3();
let upload = multer({
storage: multerS3({
s3: s3,
bucket: 'smartgallerystorage',
key: function (req, file, cb) {
cb(null, `${Date.now()}_${file.originalname}`);
},
acl: 'public-read-write',
}),
});
router.post('/image', upload.single('file'), function (req, res, next) {
let file = req.file;
let AWSfilePath = `https://smartgallerystorage.s3.ap-northeast-2.amazonaws.com/${file.key}`;
let result = {
success: true,
filePath: AWSfilePath,
fileName: file.key,
};
res.json(result);
});
JavaScript
복사
4. 사전지식
가. 토큰 기반 인증(feat. Velopert, https://velopert.com/2350)
1) Stateful vs Stateless
•
stateful 서버(서버 기반 인증 시스템): 클라이언트 상태정보를 서버 내 메모리 또는 DB 내 저장, 클라이언트 요청 시 상태정보 기반으로 세션 생성하여 서비스 제공
*세션: 서버측에서 사용자의 정보를 저장하고 유지하는 것
•
stateless 서버: 클라이언트의 상태정보를 클라이언트에 저장, 클라이언트 요청만으로 서비스 제공
2) 서버 기반 인증 시스템의 문제점
•
낮은 수준의 확장성(Scalability)
→ 세션 정보를 서버에 저장하므로 사용자가 증가함에 따라 서버의 저장공간 및 성능에 무리
→ 분산환경에서 특정 클라이언트의 세션 정보를 저장한 서버에서만 해당 클라이언트 요청 처리 가능
3) 토큰 기반 시스템의 원리
•
서버측에서 사용자 정보 요청 받고 해당 정보가 유효하면 'signed token' 발급함. 클라이언트 측에 해당 token을 저장하고 서버 요청 시 함께 전달함. 서버는 해당 토큰을 검증하고 요청에 응답함.
4) 토큰 기반 시스템의 장점
•
처리역량의 확장성(Scalability)
→ 사용자 증가에 따라 서버의 저장공간 및 성능에 영향 X
→ 분산환경에서도 클라이언트와의 매칭 고려 X
•
서비스 활용의 확장성(Extensibility)
→ 토큰을 활용하여 일부 권한을 다른 서비스와 공유할 수 있음. 구글 로그인으로 다른 서비스 이용 가능.
나. res.json vs res.send
1) 결론
JSON 형태로 응답 시, res.json 사용하는 것이 불필요한 함수 호출 발생을 줄이는 것
다. Array methos 정리
1) forEach: 배열의 모든 요소에 대해 콜백함수 실행함. 반환값 없음
2) map: 배열의 모든 요소에 대해 콜백함수 실행하고 그 결과에 대해 새로운 배열로 반환
3) filter: 배열의 모든 요소를 대상으로 특정 조건을 만족하는 요소만 들어간 배열을 반환
4) split: String.split('구분자') → 문자열에 대해 구분자를 기준으로 배열 형태로 반환
5) join: Array.prototype.join('구분자) → 배열의 모든 요소 간 구분자를 삽입하여 문자열 반환
5. 에러 정리
가. GET / POST 쓰임새
[http://localhost:3000/api/works/getWorks](http://localhost:3000/api/works/getWorks) 404 (Not Found)
createError.js:16 Uncaught (in promise) Error: Request failed with status code 404
at createError (createError.js:16)
at settle (settle.js:17)
at XMLHttpRequest.handleLoad (xhr.js:61)
JavaScript
복사
•
•
모든 작품 정보(주요 정보)를 가져오는 것인 만큼 당연히 POST 방식을 사용하는 것임
•
Get도 동작하나 Header에 정보 노출되므로 자제
나. Docker 개발환경 테스트 중 에러
Error: Error loading shared library /smart_gallery/node_modules/bcrypt/lib/binding/bcrypt_lib.node: Exec format error
•
bcrypt module 이상으로 판단하고 bcryptjs 설치 후 시도했으나 유사 에러 발생
•
dockerfile.dev 파일을 바꾼 후 정상 작동(MovieApp 버전 → TodoList 버전으로 변경 후 정상)
다. Local server storage의 Image 파일을 AWS S3에 저장
1) AWS S3에 파일 업로드 전에 multer module에 익숙해지기
•
아래와 같은 코드를 200615_NODESTUDY_NODE 프로젝트에서 테스트함
•
업로드되는 파일은 /routes/upload 폴더에 저장됨
// /app.js
var express = require("express");
var app = express();
var bodyParser = require("body-parser");
var router = require("./routes/index");
var indexRouter = require("./routes/index");
var fileUploadRouter = require("./routes/upload");
app.use("/", indexRouter);
app.use("/upload", fileUploadRouter);
app.use("/upload", express.static("upload"));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.set("view engine", "ejs");
app.use(router);
JavaScript
복사
// ./routes/upload.js
const express = require("express");
const router = express.Router();
const multer = require("multer");
let storage = multer.diskStorage({
destination: function (req, file, callback) {
callback(null, "./routes/upload/");
},
filename: function (req, file, callback) {
callback(null, file.originalname + " - " + Date.now());
},
});
// 1. multer 미들웨어 등록
let upload = multer({
storage: storage,
});
// 2. 파일 업로드 처리
router.post("/create", upload.single("imgFile"), function (req, res, next) {
// 3. 파일 객체
let file = req.file;
// 4. 파일 정보
let result = {
originalName: file.originalname,
size: file.size,
};
res.json(result);
});
// 뷰 페이지 경로
router.get("/show", function (req, res, next) {
res.render("board");
});
module.exports = router;
JavaScript
복사
// /views/borad.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<form action="/upload/create" method="post" enctype="multipart/form-data">
<input type="file" name="imgFile">
<input type="submit" value="파일 업로드하기">
</form>
</body>
</html>
HTML
복사
2) AWS S3에 파일 업로드 하기
•
에러) MulterError: Unexpected field
→ upload.single의 인자는 client에서 form data의 title에 해당함
→ 따라서 아래 코드의 imgFile을 file로 변경하여 에러 해결
→ 회고) 몇 안되는 새로운 코드의 원리에 대해 이해했다면 에러를 해결하는데 오랜시간이 걸리지 않았을 것이다. 나는 기존 코드(존 안 강사)와 블로그에서 참고한 코드를 하나씩 비교하고 바꿔가면서 검증하느라 많은 시간을 사용했다. 반성하자!
//works.js
router.post('/image', upload.single('imgFile'), function (req, res, next) {
JavaScript
복사
// FileUpload.js
let formData = new FormData();
// config 추가하는 이유: data type에 대해 명시하여 server에서 원활히 req 처리
const config = {
header: { "content-type": "multipart/form-data" },
};
formData.append("file", files[0]);
JavaScript
복사
•
아래와 같은 코드를 200615_NODESTUDY_NODE 프로젝트에서 테스트함
◦
위의 app.js 설정이 되어 있는 지 확인하고 테스트할 것
// routes/index.js
var express = require("express");
var app = express();
var router = express.Router();
var path = require("path");
const multer = require("multer");
const multerS3 = require("multer-s3");
const AWS = require("aws-sdk");
AWS.config.loadFromPath(__dirname + "/../config/awsconfig.json");
let s3 = new AWS.S3();
let upload = multer({
storage: multerS3({
s3: s3,
bucket: "smartgallerystorage",
key: function (req, file, cb) {
let extension = path.extname(file.originalname);
cb(null, Date.now().toString() + extension);
},
acl: "public-read-write",
}),
});
router.post("/upload", upload.single("imgFile"), function (req, res, next) {
let imgFile = req.file;
res.json(imgFile);
});
router.get("/upload", function (req, res, next) {
res.render("upload");
});
module.exports = router;
JavaScript
복사
// views/upload.ejs
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="imgFile">
<input type="submit" value="S3에 보내기">
</form>
</body>
</html>
JavaScript
복사
라. Docker 기반 배포 중 에러
1) .gitignore로 숨긴 여러 파일을 모두 git 상에 업로드를 해야 정상적으로 배포가 되었다...
dev.js, awsconfig.json까지...
→ 주요 정보를 은닉하기 위해 아래와 같은 시도를 하였으나 모두 배포 실패라는 결과로 돌아옴
•
Travis CI 웹 내 환경변수로 관련 정보 입력
•
AWS EB 내 환경변수로 관련 정보 입력
•
docker-compose.yml 내 환경변수 정보 입력
2) 정보은닉 성공
→ docker-compose.yml에 환경 변수 선언하고 소스코드에서 process.env. 형태로 불러와서 사용함
•
docker-compose.yml을 .gitignore에 포함시켜 정보은닉을 해도 서비스 배포에는 문제가 없었음
•
심지어 EB 내 환경변수에 관련 환경변수를 선언하지 않았음에도 정상 작동 하였음.
•
이를 검증하기 위해 이미 .gitignore에 포함시킨 docker-compose.yml 내 AWS key 값을 변경하였음. 다시 말해, dockeer-compose 파일 내에만 AWS key 값이 선언된 상태임에도 서비스가 정상 배포 되었음
•
하지만... 위와 같은 방법으로는 upload 기능에 에러가 발생함.
→ AWS S3 접근 불가
•
운영환경에서 AWS S3에 접근하기 위해 EB 웹 서비스 내 관련 환경변수 입력 후 upload 기능 정상 작동