[Node, React] 개발자 지망생 스터디 - 23일차
☑︎ Authentication : 화면 출력과 처리를 하나의 애플리케이션을 수행 - 서버 랜더링
☑︎ 클라이언트의 종류가 다양하고 확장 가능성이 있음
☑︎ 서버 랜더링의 형태를 활용하는 경우
웹 브라우저에서 접속을 해서 철이하는 부분을 생성
☑︎ 안드로이드가 동일한 서비스를 사용하고자 하는 경우?
안드로이드 화면은 서버에서 랜더링이 불가능함
안드로이드 프로젝트를 만들어야하고 서버에 안드로이드를 위한 코드를 추가
안드로이드 처리를 위한 별도의 URL을 생성해서 처리
동일한 요청에 대한 URL이 2개 - 번거로움
☑︎ 서버는 데이터만 주는 형태를 만들고 클라이언트는 별도로 생성해서 서버의 데이터를 받아서 처리하도록 함 (서버와 클라이언트의 분리)
☑︎ 클라이언트 종류가 추가 되더라도 서버는 수정할 필요가 없음
Client <-> Web Server <-> Application Server <-> Data Server
☑︎ Application Server가 랜더링을 하지 않고 데이터를 제공하면 API Server라고 함
5. JWT(JSON Web Token)
4) routes 디렉토리에 토큰을 발급하는 처리를 수행해주는 v1.js 파일을 생성하고 작성
📁 routes-> 📄 v1.js
const express = require('express'); const { verifyToken } = require('./middlewares'); const router = express.Router(); router.post('/token',async(req, res) => { const {clientSecret} = req.body; try{ //도메인 찾아오기 const domain = await Domain.findOne({ where:{clientSecret}, include:{ model:User, attributes:['nick', 'id'] } }); if(!domain){ return res.status(401).json({ code:401, message:"등록 되지 않은 도메인 입니다." }) } // 토큰 생성 const token = jwt.sign({ id:domain.User.id, nick:domain.User.nick }, process.env.JWT_SECRET, { expiresIn:'1m', // 유효기간 issure:'itoriginal' // 발급자 }); return res.json({ code:200, message:'토큰 발급이 완료되었습니다.', token }) }catch(error){ console.error(error); return res.status(500).json({ code:500, message:"서버 에러" }) } }) // 토큰을 확인해 보기 위한 처리 router.get('/test', verifyToken, (req, res) => { res.json(req.decoded); }) module.exports = router;
5) App.js 파일에 v1.js 등록
🗂 apiserver -> 📄 App.js
// 라우터 설정 const v1Router = require('./routes/v1'); app.use ('/v1',v1Router);
6. API 서버에서 데이터를 제공하도록 작성
1) v1.js 수정
📁 routes-> 📄 v1.js
const express = require('express'); const { verifyToken } = require('./middlewares'); const jwt = require('jsonwebtoken'); const {Domain, User, Post, Hashtag} = require("../models"); const router = express.Router(); //데이터를 리턴하는 요청 처리 router.get('/posts/my', verifyToken, (req, res) => { Post.findAll({where:{userId:req.decoded.id}}) .then((posts) => { console.log(posts); res.json({code:200, payload:posts}) }) .catch((error) => { console.error(error); return res.status(500).json({ code:500, message:'서버 에러' }) }) }) //토큰 발급 router.post('/token', async(req, res) => { const {clientSecret} = req.body; try{ //도메인 찾아오기 const domain = await Domain.findOne({ where:{clientSecret}, include:{ model:URLSearchParams, attribute:['nick', 'id'] } }); if(!domain){ return res.status(401).json({ code:401, message:"등록되지 않은 도메인입니다." }) } //토큰 생성 const token = jwt.sign({ id:domain.User.id, nick:domain.User.nick }, process.env.JWT_SECRET, { expiresIn:'1m', //유효기간 issuer:'itoriginal' //발급자 }); return res.json({ code:200, message:'토큰이 발급되었습니다.', token }) }catch(error){ console.error(error); return res.status(500).json({ code:500, message:'서버에러' }) } }) //토큰을 확인하기 위한 처리 router.get('/test', verifyToken, (req, res) => { res.json(req.decoded); }) module.exports = router;
7. Server에 데이터를 요청하는 클라이언트 애플리케이션 제작
(CREATE) -> 🗂 apiclient
[ 기본 설정 ]
# 터미널에 npm init
#필요한 패키지 설치
> npm install express dotenv axios cookie-parser express-session morgan nunjucks
> npm install --save-dev nodemon
💡 axios : JavaScript에서 ajax나 fetch API 대신에 사용할 수 있는 JavaScript 웹 요청 라이브러리
#생성된 pakage.json 파일에 아래 내용을 추가 ("start" : "nodemon app")
"scripts": { "start": "nodemon app", "test": "test" }
#App.js 파일 생성하고 작성
🗂 apiclient -> (CREATE) 📄 App.js
const express = require('express'); const morgan = require('morgan'); const cookieParser = require('cookie-parser'); const session = require('express-session'); const nunjucks = require('nunjucks'); const dotenv = require('dotenv'); dotenv.config(); const indexRouter = require('./routes'); const app = express(); app.set('port', process.env.PORT || 4000); app.set('view engine', 'html'); nunjucks.configure('views', { express: app, watch: true, }); app.use(morgan('dev')); app.use(cookieParser(process.env.COOKIE_SECRET)); app.use(session({ resave: false, saveUninitialized: false, secret: process.env.COOKIE_SECRET, cookie: { httpOnly: true, secure: false, }, })); app.use('/', indexRouter); app.use((req, res, next) => { const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`); error.status = 404; next(error); }); app.use((err, req, res, next) => { res.locals.message = err.message; res.locals.error = process.env.NODE_ENV !== 'production' ? err : {}; res.status(err.status || 500); res.render('error'); }); app.listen(app.get('port'), () => { console.log(app.get('port'), '번 포트에서 대기중'); });
# views 디렉토리 생성 (nunjucks 때문) 하고 error.html 생성하여 작성
🗂 apiclient -> (CREATE) 📁 views -> (CREATE) 📄 error.html
<h1>{{message}}</h1> <h2>{{error.status}}</h2> <pre>{{error.stack}}</pre>
#.env 파일을 만들고 필요한 속성을 작성
🗂 apiclient -> (CREATE) ⚙️.env
> 앞서 생성한 "apiserver"를 터미널에서 서버를 실행시킨 후,
> localhost:8000(apiserver PORT 번호)에 접속하여 localhost:4000(apiclient에 사용할 PORT 번호)의 클라이언트 비밀키를 생성하여 .env 파일에 작성
COOKIE_SECRET = nodeclient CLIENT_SECRET = 🔑
#routes 디렉토리를 생성하고 라우팅 관련 코드를 index.js 파일을 생성하여 작성
🗂 apiclient -> (CREATE) 📁 routes -> (CREATE) 📄 index.js
> 실행을 하고 브라우저에 localhost:4000/test 를 입력하면 토큰 내용이 출력되어야 함const express = require('express'); const axios = require('axios'); const router = express.Router(); router.get('/test', async(req, res, next) => { try{ if(!req.session.jwt){ const tokenResult = await axios.post('http://localhost:8000/v1/token',{ clientSecret:process.env.CLIENT_SECRET }); if(tokenResult.data && tokenResult.data.code === 200){ req.session.jwt = tokenResult.data.token; }else{ //토큰 발급 실패 return res.json(tokenResult.data); } } //토큰 내용 확인 const result = await axios.get( 'http://localhost:8000/v1/test', { headers:{authorization:req.session.jwt} }) return res.json(result.data); }catch(error){ console.error(error); return next(error); } }) module.exports = router;
#API 요청을 위해서 index.js 수정
🗂 apiclient -> 📁 routes -> 📄 index.js
> 브라우저에 localhost:4000/mypost 를 입력하고 데이터가 넘어오는지 확인const express = require('express'); const axios = require('axios'); //매번 동일한 요청을 위한 URL을 상수로 설정 const URL = 'http://localhost:8000/v1'; //ajax 요청을 할 때 누가 요청했는지 확인해주기 위해서 //origin header 추가 axios.defaults.headers.origin = 'http://localhost:4000'; //토큰 발급 코드 const request = async(req, api) => { try{ if(!req.session.jwt){ const tokenResult = await axios.post( `${URL}/token`,{ clientSecret:process.env.CLIENT_SECRET }); req.session.jwt = tokenResult.data.token; } //토큰 내용 확인 const result = await axios.get( `${URL}${api}`, { headers:{authorization:req.session.jwt} }) return result; }catch(error){ //토큰 유효 기간 만료 if(error.response.status === 419){ //기존 토큰 삭제 delete req.session.jwt; //다시 토큰을 생성해달라고 요청 return request(req, api); } return error.response; } } const router = express.Router(); router.get('/mypost', async(req, res, next) => { try{ const result = await request(req, '/posts/my'); res.json(result.data); }catch(error){ console.error(error); next(error); } }) router.get('/test', async(req, res, next) => { try{ if(!req.session.jwt){ const tokenResult = await axios.post( 'http://localhost:8000/v1/token',{ clientSecret:process.env.CLIENT_SECRET }); if(tokenResult.data && tokenResult.data.code === 200){ req.session.jwt = tokenResult.data.token; }else{ //토큰 발급 실패 return res.json(tokenResult.data); } } //토큰 내용 확인 const result = await axios.get( 'http://localhost:8000/v1/test', { headers:{authorization:req.session.jwt} }) return res.json(result.data); }catch(error){ console.error(error); return next(error); } }) module.exports = router;
> 텍스트는 넘어오는데 데이터가 없으면 authentication 프로젝트 실행해서 comment 를 몇 개 작성해야 함
8. 서버수정
1) 사용량 제한
> API Server를 만들었을 때 데이터를 무제한 제공하게 되면 트래픽이 많이 발생해서 속도가 느려질 수 있음
> DDos 공격의 대상이 될 수 도 있음.
> 일정한 주기를 가지고 제한을 하기도 하고 사이즈 나 횟수 제한을 가하기도 함
> Kakao 같은 경우는 횟수 제한 과 사이즈 제한을 동시에 함
2) 기존 서버를 수정했을 때 처리
> 기존 코드를 무조건 바꾸는 것은 위험함
> 기존 코드는 그대로 두고 deprecated 나 서비스 중지 메시지를 전송하는 형태로 새로운 내용을 적용하는 것이 좋음
3) node 의 middleware 와 Java 의 Filter, Spring 의 Interceptor 와 AOP
실제 처리를 하기 전이나 후에 동작하는 로직을 작성하는 용도로 사용
위의 것들을 사용하는 경우는 Business Logic과 Common Concern의 분리를 하기 위해서나 공통된 처리를 해야하는 경우임
4) 사용량 제한을 위한 ApiServer 프로젝트 수정
🗂 apiserver
# 사용량 제한을 위한 패키지 설치
> npm install express-rate-limit
# middlewares.js 파일에 사용량 제한을 위한 미들웨어를 생성
📁 routes -> 📄 middlewares.js
exports.isLoggedIn = (req, res, next) => { //로그인 되어 있으면 다음 라우터 처리를 수행하고 그렇지 않으면 에러 발생 if (req.isAuthenticated()) { next(); } else { res.status(403).send('로그인 필요'); } }; exports.isNotLoggedIn = (req, res, next) => { //로그인 되어 있지 않았다면 다음으로 넘어가고 그렇지 않으면 리다이렉트 if (!req.isAuthenticated()) { next(); } else { const message = encodeURIComponent('로그인한 상태입니다.'); res.redirect(`/?error=${message}`); } }; const jwt = require('jsonwebtoken'); exports.verifyToken = (req, res, next) => { try{ //토큰 확인 req.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SECRET); //인증에 성공하면 다음 작업 수행 return next(); }catch(error){ if(error.name === 'TokenExpiredError'){ return res.status(419).json({ code:419, message:'토큰이 만료되었습니다.' }); } return res.status(401).json({ code:401, message:"유효하지 않은 토큰입니다." }) } } //사용량 제한을 위한 미들웨어 const RateLimit = require('express-rate-limit'); exports.apiLimiter = RateLimit({ windowMs:60*1000, //1분 max:10, delayMs:0, handler(req, res){ res.status(this.statusCode).json({ code:this.statusCode, message: '1분 단위로 요청을 해야 합니다.' }) } }); //구버전 API 요청 시 동작할 미들웨어 exports.deprecated = (req, res) => { res.status(410).json({ code:410, message:"새로운 버전이 나왔습니다. 새버전을 사용하세요" }) }
#routes 디렉토리에 새로운 버전의 요청을 처리할 v2.js 파일을 생성하고 작성
📁 routes -> (CREATE) 📄 v2.js
> v1.js 파일을 COPY한 후, 내용 수정const express = require('express'); const { verifyToken, apiLimiter } = require('./middlewares'); const jwt = require('jsonwebtoken'); const {Domain, User, Post, Hashtag} = require("../models"); const router = express.Router(); //데이터를 리턴하는 요청 처리 router.get('/posts/my', apiLimiter, verifyToken, (req, res) => { Post.findAll({where:{userId:req.decoded.id}}) .then((posts) => { console.log(posts); res.json({code:200, payload:posts}) }) .catch((error) => { console.error(error); return res.status(500).json({ code:500, message:'서버 에러' }) }) }) //토큰 발급 router.post('/token', apiLimiter, async(req, res) => { const {clientSecret} = req.body; try{ //도메인 찾아오기 const domain = await Domain.findOne({ where:{clientSecret}, include:{ model:User, attribute:['nick', 'id'] } }); if(!domain){ return res.status(401).json({ code:401, message:"등록되지 않은 도메인입니다." }) } //토큰 생성 const token = jwt.sign({ id:domain.User.id, nick:domain.User.nick }, process.env.JWT_SECRET, { expiresIn:'10m', //유효기간 issuer:'adam' //발급자 }); return res.json({ code:200, message:'토큰이 발급되었습니다.', token }) }catch(error){ console.error(error); return res.status(500).json({ code:500, message:'서버에러' }) } }) //토큰을 확인하기 위한 처리 router.get('/test', apiLimiter, verifyToken, (req, res) => { res.json(req.decoded); }) module.exports = router;
#v1 수정 (deprecated 설정 추가)
📁 routes -> 📄 v1.jsconst express = require('express'); const { verifyToken, deprecated } = require('./middlewares'); const jwt = require('jsonwebtoken'); const {Domain, User, Post, Hashtag} = require("../models"); const router = express.Router(); //모든 라우팅 처리에서 deprecated 적용 router.use(deprecated); //데이터를 리턴하는 요청 처리 router.get('/posts/my', verifyToken, (req, res) => { Post.findAll({where:{userId:req.decoded.id}}) .then((posts) => { console.log(posts); res.json({code:200, payload:posts}) }) .catch((error) => { console.error(error); return res.status(500).json({ code:500, message:'서버 에러' }) }) }) //토큰 발급 router.post('/token', async(req, res) => { const {clientSecret} = req.body; try{ //도메인 찾아오기 const domain = await Domain.findOne({ where:{clientSecret}, include:{ model:User, attribute:['nick', 'id'] } }); if(!domain){ return res.status(401).json({ code:401, message:"등록되지 않은 도메인입니다." }) } //토큰 생성 const token = jwt.sign({ id:domain.User.id, nick:domain.User.nick }, process.env.JWT_SECRET, { expiresIn:'1m', //유효기간 issuer:'adam' //발급자 }); return res.json({ code:200, message:'토큰이 발급되었습니다.', token }) }catch(error){ console.error(error); return res.status(500).json({ code:500, message:'서버에러' }) } }) //토큰을 확인하기 위한 처리 router.get('/test', verifyToken, (req, res) => { res.json(req.decoded); }) module.exports = router;
#App.js 파일에 v2.js 파일을 사용할 수 있도록 설정을 추가
🗂 apiserver -> 📄 App.jsconst v2Router = require('./routes/v2'); app.use ('/v2',v2Router);
#apiclient 프로젝트의 routes 디렉토리의 index.js 파일에서 URL을 수정
🗂 apiclient -> 📁 routes -> 📄 index.js//const URL = 'http://localhost:8000/v1'; const URL = 'http://localhost:8000/v2';
5) CORS(Cross-Origin Resource Sharing)
# SOP(Same Origin Policy - 동일 출처 정책)
> 어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용 하는것을 제한하는 브라우저의 보안 방식
> 브라우저에서는 XMLHttpRequest(ajax) 와 Fetch API 같은 경우는 다른 출처에 리소스를 요청할 때 적용
> img, link, script, video, audio, object, embed, applet 태그는 SOP 적용을 받지 않음
# CORS(교차 출처 정책)
> 추가 HTTP 헤더를 사용해서 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 자원에 접근할 수 있는 권한을 부여해서 브라우저에 알려주는 것
> ajax 나 Fetch API가 다른 출처의 데이터를 가져와 사용하기 위해서는 올바른 CORS 헤더를 포함한 응답을 반환해야 함
> 서버를 만들 때 이 부분을 고려 하여 작성해야 하고 이미 만들어진 경우나 다른 곳에서 만든 API를 이용해야 하는 경우는 Proxy를 이용해 함.
6) ajax 오류
# 클라이언트 프로젝트의 routes 디렉토리의 index.js 파일에 라우팅 코드 추가
🗂 apiclient -> 📁 routes -> 📄 index.js
router.get('/', (req, res)=>{ res.render('main', {key:process.env.CLIENT_SECRET}) });
# 클라이언트 프로젝트의 views 디렉토리에 main.html 파일을 만들고 작성
🗂 apiclient -> 📁 views -> 📄 main.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="result"></div> <script src="https://unpkg.com/axios/dist/axios.min.js"> </script> <script> axios.get('http://localhost:8000/v2/token', { clientSecret:'{{key}}' }) .then(res => { document.getElementById('result').innerHTML = JSON.stringify(res.data); }) .catch(error => { console.error(error); }) </script> </body> </html>
> 브라우저에서 localhost:4000 으로 접속하고 검사 창을 출력해서 에러 메시지를 확인
7) CORS 설정
#서버 프로젝트에서 CORS 구현을 위한 패키지를 설치
🗂 apiserver
> npm install cors
#서버 프로젝트의 v2.js 에 적용
🗂 apiserver -> 📁 routes -> 📄 v2.js
const cors = require('cors'); const url = require('url'); //무조건 CORS를 허용 router.use(cors({ credentials:true })) //Domain에 등록된 경우만 전송할 수 있도록 설정 router.use(async (req, res, next) => { //현재 요청 도메인이 데이터베이스에 등록된 도메인인지 찾아오기 const domain = await Domain.findOne({ where:{host:url.parse(req.get('origin')).host} }) if(domain){ cors({ origin:req.get('origin'), credentials:true })(req, res, next); }else{ next(); } })
9. 기타
1) node에서 다른 서버의 데이터 가져오기 : request 모듈
2) websocket
> 클라이언트와 서버 연결을 유지한 상태로 데이터를 주고 받을 수 HTML5 SPEC
> http 나 https는 연결을 유지하지 않고 header의 오버헤드가 큼
> 짧은 메시지를 자주 전송하는 시스템에서는 적합하지 않은 프로토콜임
3) push - Server Sent Events
> 클라이언트의 요청이 없어도 서버가 메시지를 전송하는 것
React_component
1. React
> 자바스크립트로 뷰를 만들기 위한 라이브러리
> SPA 구현을 위한 라이브러리
> 화면을 만드는 부분을 제외하면 별도의 패키지를 설치해서 애플리케이션을 제작
💡 SPA(Single Page Application)란?
전체 화면을 리랜더링하지 않고 변경된 일부분만을 리랜더링 할 수 있는 컴포넌트 기반의 애플리케이션임
❖ react를 학습 전
ajax 나 fetch api 또는 axios 와 같은 외부 데이터를 가져올 수 있는 부분을 학습을 해두는 것이 좋음
> react-native 는 스마트 폰 애플리케이션 개발(안드로이드 와 아이폰 개발 동시에 가능)을 위한 자바스크립트 라이브러리
> react 의 출력 시스템은 Virtual DOM 이라는 개념을 사용하는데 메모리 상에 DOM을 생성해서 현재 화면에 출력된 DOM 과 비교를 한 후 변경된 부분만 리랜더링 하는 개념으로 동작
2.프로젝트 생성 및 실행
1) 프로젝트 생성
💡 yarn ?
> node.js 런타임 환경을 위해서 페이스 북이 개발한 패키지 관리 시스템으로 npm을 이용해서 별도로 설치해야 사용 가능
$npm install --location=global
$yarn create react-app 애플리케이션이름
(Windows 에서는 power shell 에서 yarn 이 막혀있어서 Set-ExecutionPolicy RemoteSigned 명령을 수행한 후 작업)
💡 npx ?
> npm 대신에 실행을 할 수 있도록 만든 보조 도구로 만들어지는데 시간이 다소 걸림
$create-react-app 애플리케이션이름
라이브러리 설치할 때도 yarn add로 안되면 npm install 로 하면 됨
2)프로젝트 실행
$yarn start
$npm start
> 실행을 하면 localhost:3000 번의 URL을 가지고 웹 브라우저가 실행되고 화면에 react 로고가 출력
> 실행이 될 때 모든 파일들을 읽어서 번들러를 이용해서 실행 가능한 자바스크립트 파일을 만들어서 실행
이 때 webpack 과 babel이 동작
> 소스 코드를 수정하면 자동으로 번들러가 해석을 해서 화면에 적용함
3. Component
- 화면을 구성하는 단위
- View > Component(Segment) > Control
View : 하나의 화면
Component(Segment) : Control을 1개 이상 모아서 만든 논리적인 화면 구성 단위
Control : 작은 부품 하나
☑︎ 만드는 방법
# 클래스로 구성
> Componet 라는 클래스로부터 상속을 받아서 render 라는 함수에 출력할 내용을 리턴하는 형식으로 생성
> 멤버 변수 사용이나 수명 주기 메서드를 사용하는 것이 편리
class 이름 extends Component{ render(){ return 출력할 내용 } }
# 함수로 구성
> 출력할 내용을 리턴, 클래스로 만드는 것보다는 가볍고 속도가 빠름
> 최근에는 함수로 구성하는 경우가 많음
function 이름(){ return (출력할 내용) } const 이름 = () => { return (출력할 내용) }
# 확장자
> js, jsx(HTML에 사용되는 자바스크립트 와 구분하기 위해서), tsx(타입스크립트 문법을 사용한다것을 명시하기 위해서)등을 사용
# 만들 때 주의사항
> 컴포넌트는 Root 가 1개 이어야 함
> div 나 span 또는 <> 태그로 묶어서 표현
> react 에서는 데이터 원본을 직접 수정하지 않음
다른 곳에서 넘겨준 데이터는 복제를 해서 수정한 후 다시 대입하는 형태를 취하게 됨