JavaScript는 초기 HTML을 조작하는 언어로써 처음엔 보잘것 없었지만 웹이 빠르게 발전하고 변화함에 따라 같이 유명해지고 변화에 발 맞춰 살아남았다고 볼 수도 있다. 결론적으로 지금은 여러방면으로 사용하는 언어가 되었는데 TypeScript는 왜 나왔을까 ?
TypeScript는 JavaScript의 슈퍼셋이다. TypeScript는 JavaScript가 지원하는 모든 기능을 지원하기 때문인데 왜 그런 방향으로 갔을까 ? 프로그래밍 언어의 좋고 나쁨을 떠나서 현재 웹에서 JavaScript는 유일한 언어이고 그렇기 때문에 JavaScript를 대신한다라는 개념보다 공생하는 개념이 훨씬 전략적으로 좋을 것이라 생각한다.
TypeScript가 JavaScript라는 언어와 공생하는 방법은 JavaScript가 가지고 있지 않은 부분(타입, 형)을 메꾸는 방식이다 JavaScript는 언어의 특성 상 Type(형)이라는 것이 없기 때문에 TypeScript는 JavaScript가 가진 모든 기능을 지원하면서 Type을 명시적으로 지정할 수 있는 언어인것이다.
예를 들어
type Centimeter = number;
type RainbowColor = 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'indigo' | purple
let age = 10;
let weight:number = 80;
let height:Centimeter = 176;
let color:RainbowColor = 'orange';
color = 'black' // error
프론트엔드 개발 환경이 복잡할 수 밖에 없는 이유 ? :웹 앱의 규모가 굉장히 커지고 복잡해짐
JavaScript의 디자인은 단순히 HTML을 조작하기 위한 언어였지만 ES2015부터 모듈 스펙을 지원하기 시작한다. 모듈 스펙은 파일과 파일간의 어떤 기능을 사용하기 위해 어떤 파일의 기능을 해당 파일로 불러들여서 사용하는 것을 말한다. (import export)
모듈 스펙을 사용하는 경우는 예를들어 JavaScript를 사용하기 위해선 HTML 파일의 <script> 태그를 사용하여 로딩하는데 js파일의 갯수가 대량이 되어버리면 스크립트 태그를 그 만큼 작성해야 한다. 때문에 운영하는데도 불편하고 또 문제점이 생길수 있다.
그렇기 때문에 HTML의 <script>가 아닌 import, export를 사용하면 되는데 이러한 문법은 현실적으로 2가지 문제가 있다.
첫 번째는 브라우저 호환성 문제이다. 사용자들이 어떤 브라우저의 어떤 버전을 사용할지 모르기 때문에 브라우저의 호환성을 최대한 넓게 가져가야 한다. 두 번째는 최신 문법을 사용하려 하는 경향이다. 프로그래밍 언어의 문법은 차차 발전하고 이전 문법의 좋지 않은 관습이나 불편한 것들은 사라지거나 변경되어 더 좋은 문법으로 나타나는데 이 또한 브라우저 호환성 문제가 더욱 생기기 마련이다.
따라서 이런 문제들을 해결하기 위해 엔지니어링이 시도가 된다. 엔지니어링이란 예를 들어 node.js를 보면 브라우저 뿐만 아니라 일반 데스크탑, PC 서버 등에서 JavaScript를 실행할 수 있도록 해주는 환경이다. JavaScript 개발자들이 JavaScript로 다양한 툴들을 만들고 그것들을 배포하기 시작했다. 그 배포된 툴들은 npm을 통해서 그것을 사용하고자 하는 프로젝트에 불러다가 쓰기 수월해졌다.
번들러는 시작되는 자바스크립트 파일을 하나 지정하면 그 파일을 통해 다른 자바스크립트 파일을 불러와서 그렇게 불러진 자바스크립트 파일을 하나로 만들어 주는 것을 의미한다. 브라우저는 그렇게 하나로 만들어진 파일만을 읽어와서 수 많은 파일들을 실행한 결과를 가져올 수 있다.
결국 여러개의 파일을 하나로 만들어주는 번들러라는 프로그램이 나왔고 그 번들링이라는 작업으로 인해 모듈을 부르는 스펙을 못 쓰던 상황에서 모듈 스펙을 쓸 수 있게 되었고 파일을 하나로 관리할 수 있어 HTML을 운영하기도 현실적으로 쉬워졌다.
또한, 번들러는 자바스크립트 파일 뿐만 아니라 번들링을 하는 과정에 있어 파일 용량을 위한 주석제거나 이미지 파일의 용량이 일정 이상이되면 압축을 통한 이미지 최적화, 소스 파일의 어글리파이 작업등을 수행할 수 있다.
이러한 기능들을 추가하다보니 번들러는 점점 커지고 웹 앱의 규모가 커짐으로 이어졌다.
트랜스파일링이란 내가 작업한 프로그래밍 언어를 브라우저가 이해할 수 있는 자바스크립트 언어로 변환 하는것. 개발자는 JavaScript의 최신 버전으로 개발하고 그러면서 발생할 수 있는 브라우저의 호환성 문제는 트랜스 파일러를 통해 해결할 수 있다. (ex. Babel, TypeScript)
netscape(firefox의 전신)에서 LiveScript 탄생: html을 조작하기 위한 언어
공식명칭: EcmaScript
JavaScript는 Adobe의 전성기 시절에는 별 다를것 없이 있다가 Adobe가 점점 사장되는 시기에 2015년에 버전 5.0, ES2015가 나오면서 현대의 모던 자바스크립트가 되었다.
ES5가 모던 자바스크립트의 가장 주도적인 버전이라고 칭하는데, 그 이유는 ES6, ES7, ES8 등으로 버전이 업그레이드 되었지만 해당 버전을 지원하지 않는 브라우저도 많기 때문에 최신 버전으로 개발하였을 때, 모든 사용자들에게 동작하는 웹앱을 만들기 어렵다.
그리하여 트랜스파일러(Babel, typescript 등)를 사용하면 높은 버전으로 개발하고 5.0 버전으로 변환한 후에 브라우저에서 동작시킨다.
typescript는 JavaScript의 슈퍼셋으로 모든 JavaScript의 기능을 지원한다.
웹 앱의 구성요소: HTML, CSS,JavaScript
브라우저(런타임 환경): 웹앱을 실행시킴
node.js(런타임 환경): 웹앱을 실행시킴
웹 서버에서 HTML 파일을 브라우저에 전송시키고 브라우저는 해당 HTML을 해석해서 화면에 UI를 그리고 난 후 JavaScript가 실행된다. JavaScript는 HTML을 조작할 수 있는데 그렇다는건 브라우저에서 HTML 파일에 작성된 UI를 화면에 그리고 난 후에도 추가적으로 JavaScript로 HTML을 또 그릴 수 있다.
최초의 HTML에 내용이 거의 없고 상황에 따라 JavaScript로 필요한 UI를 만들어 내는 방식을 CSR(Client Side Rendering)이라고 하고 웹서버에서 HTML이 만들어져서 브라우저로 전송되는 방식을 SSR(Server Side Rendering)이라고 한다.
어떤 웹 앱이던 CSR과 SSR 형식으로 개발 할 수 있다. 해당하는 앱의 성격에 맞게 렌더링을 하도록 개발하는 것이 개발자의 역할이다.
다른 구성요소: HTML, CSS를 제외한 그래픽 시스템 (canvas 등) 미디어파일, 웹 워커, 웹 어셈블리
{id : 세션아이디에 숨겨져있던 유저의 아이디} 인 게시물을 찾아서 그 데이터의 결과를 done(null, result)를 실행한다. 그러면 결과가 req.user 부분에 들어간다. (로그인한 유저의 DB 데이터를 볼 수 있다.) 로그아웃은 req.logout()으로 실행할 수 있다.
이 방법으로 간단한 검색엔진처럼 검색이 가능하다. (ex. 단어 -단어, "단어 단어" 등)
단점: 띄어쓰기를 기준으로 검색이 안된다 (영어에 특화된 검색)
- 해결방법
1. 검색할 양을 제한한다.
- 날짜별로 검색하거나, skip(), limit() 함수를 이용해서 페이징 처리를 한다.
2. text Search를 사용한다.
- MongoDB Atlas말고 직접 설치한다. 띄어쓰기 단위 indexing이 아닌 글자 단위의 indexing 알고리즘을 사용할 수 있다. (nGram)
3. Search index를 사용한다.
- MongoDB Atlas에서 Search index를 생성할 수 있다. text index랑 비슷하지만 lucene.korean으로 변경하면 한국어에 쓰기 좋게 조사 등을 제거하고 검색을 할 수 있다.
- Search index
app.get('/search', (req, res) => {
var 검색조건 = [
{
$search: {
index: 'titleSearch', // Search index에서 설정한 name
text: {
query: req.query.value,
path: '제목' // 제목날짜 둘다 찾고 싶으면 ['제목', '날짜']
}
}
},
// { $sort: { _id: 1 } }, // 오름차순 1, 내림차순 -1
// { $limit: 10 } // 갯수 설정
// { $project : { 제목: 1, _id: 1, score: { $meta: "searchScore" } } } // meta 부분은 검색되는 빈도 등의 점수를 기준으로 검색
]
db.collection('post').aggregate(검색조건).toArray(function(error, result){ // find() 대체
console.log(result);
res.render('search.ejs', {posts: result});
})
})
router
서버에 요청하는 수 많은 경로들을 분류하여 관리할 수 있다.
root/routes/파일.js
server.js와 나란한 경로에 routes라는 폴더를 생성하고 그 안에 js 파일을 생성한다.
// 라우팅하는 .js 파일
var router = require('express').Router(); // express의 Router기능
router.get('/shop/shirts', function(req, res){
res.send('셔츠 파는 페이지입니다.');
});
router.get('/shop/pants', function(req, res){
res.send('바지 파는 페이지입니다.');
});
module.exports = router; // import/export 대체 가능
app.use('/', require('./routes/shop.js') ); // 미들웨어 형식으로 라우터를 적용
- URL 단축
app.use('/shop', require('./routes/shop.js') );
/////////////////////////////////////////////
var router = require('express').Router();
router.get('/shop/shirts', function(req, res){
res.send('셔츠 파는 페이지입니다.');
});
router.get('/shop/pants', function(req, res){
res.send('바지 파는 페이지입니다.');
});
module.exports = router;
- 라우터에 미들웨어 적용
var router = require('express').Router();
// router.use(loginCheck); // 전역적으로 loginCheck라는 미들웨어 사용 가능
router.use('/shop/shirts', loginCheck); // '/shop/shirts'의 경로에 loginCheck라는 미들웨어 사용 가능
function loginCheck(req, res, next) {
if (req.user) { next() }
else { res.send('로그인 안하셨는데요?') }
}
router.get('/shop/shirts', function(req, res){
res.send('셔츠 파는 페이지입니다.');
});
router.get('/shop/pants', function(req, res){
res.send('바지 파는 페이지입니다.');
});
module.exports = router;
Google Cloud Platform 배포
1. app.yaml 파일 생성
runtime: nodejs
env: flex
/////////////// 밑으로는 안적어도 자동 생성
manual_scaling:
instances: 1
resources:
cpu: 1
memory_gb: 0.5
disk_size_gb: 10
2. server.js 포트 확인
app.listen(8080, function() {
console.log('listening on 8080')
})
- 구글클라우드 기본 포트가 8080. (.env 파일 세팅있다면 확인)
3. MongoDB Atlas Network Access 모든 아이피 허용 확인
- 모든 아이피 (0.0.0.0) 접속허용 (Allow Access from Anywhere 로 체크)
4. Google Cloud Platform 시작
- 회원가입 및 카드등록 및 프로젝트 생성
- 프로젝트 생성 후 App Engine에서 배포 진행
- SDK 설치 진행
5. 배포
** 프로젝트가 설치되어 있는 경로로 이동한다.**
gcloud init
Google Cloud Platform 개설한 구글 아이디 로그인 및 프로젝트 선택
gcloud app deploy
작업이 끝나면 배포할 소스파일, 이름, url 등을 알려주고 배포 완료.
이미지 처리
이미지 업로드 및 이미지 서버를 만들 수 있다.
<form method="POST" action="/upload" enctype="multipart/form-data" > // multipart => 인코딩을 하지 않아 상대적으로 파일 용량 적게 가져옴
<input type="file" name="profile">
<button type="submit">전송</button>
</form>
: ejs
npm install multer // multipart
multipart/form-data 라이브러리 설치
let multer = require('multer');
// var storage = multer.memoryStorage({}) // memory 상에 저장 (휘발성)
var storage = multer.diskStorage({
destination : function(req, file, cb){ // 이미지 저장 경로
cb(null, './public/image')
},
filename : function(req, file, cb){ // 이미지 이름 설정
cb(null, file.originalname )
},
fileFilter: function (req, file, callback) { // 이미지 저장 시 필터처리
var ext = path.extname(file.originalname);
if(ext !== '.png' && ext !== '.jpg' && ext !== '.jpeg') {
return callback(new Error('PNG, JPG만 업로드하세요'))
}
callback(null, true)
},
limits:{
fileSize: 1024 * 1024 // 이미지 사이즈 제한
}
});
var upload = multer({storage : storage});
: (server.js) multer 세팅
// 이미지 저장 페이지
app.get('/upload', function(req, res){
res.render('upload.ejs')
});
// 이미지 저장처리
app.post('/upload', upload.single('profile'), function(req, res){ // input태그의 name속성 'profile'
res.send('업로드완료')
}); // upload 미들웨어처럼 실행
// 저장된 이미지 불러오기
app.get('/image/:imageName', function(req, res){
res.sendFile(__dirname + '/public/image/' + req.params.imageName); // __dirname => 현재root경로
})
: server.js
서버와의 실시간 소통 (SSE)
1) 1초마다 서버에게 메세지를 요청한다.
2) 서버랑 유저간 지속적인 소통채널을 연다.
2)의 경우가 서버의 부하가 덜 하고 get이나 post 등의 요청은 1회 요청이지만 지속적으로 서버에 응답을 하려할 때 사용한다.
// chat.ejs
var 지금누른채팅방id;
var eventSource; //일단변수
$('.list-group-item').click(function(){
지금누른채팅방id = this.dataset.id;
//프론트엔드에서 실시간 소통채널 여는법
eventSource = new EventSource('/message/' + 지금누른채팅방id);
eventSource.addEventListener('test', function (e){
console.log(JSON.parse(e.data));
});
});
1. GET 요청 대신 new EventSource('/message/' + 지금누른채팅방id); 형태의 코드를 실행하면 서버에서 만들어놓은 실시간 채널에 입장 가능하다.
2. eventSource.addEventListener('서버에서작명한이벤트명') 이런 코드를 쓰면 서버가 보낸 데이터를 수신할 수 있다. 서버가 res.write() 할 때마다 내부 코드를 실행해줍니다.
3. e.data 안에는 서버가 보낸 데이터가 들어있다.
DB 변동사항 실시간 업데이트
* MongoDB change stream
- 데이터 베이스의 변동을 감지하여 서버에게 업데이트 사항을 알려준다.
// ejs
var 지금누른채팅방id;
var eventSource;
$('.list-group-item').click(function(){
지금누른채팅방id = this.dataset.id;
$('.chat-content').html('') //일단 메세지들 전부 비우기
eventSource = new EventSource('/message/' + 지금누른채팅방id);
eventSource.addEventListener('test', function (e){
console.log(e.data);
var 가져온거 = JSON.parse(e.data);
가져온거.forEach((a) => {
$('.chat-content').append(`<li><span class="chat-box">${a.content}</span></li>`)
});
});
});
1. 가져온 데이터를 JSON -> Object 형태로 변환
2. Array로 되어있는 object 안에 하나하나 메세지 document 들을 반복문을 통해 하나씩 분리
3. 그거 안에 있던 메세지들을 <li></li> 태그로 만들어서 원하는 곳에 출력
// server
const 찾을문서 = [
{ $match: { 'fullDocument.parent': 요청.params.parentid } } // collection의 변경 감지 부분
];
const changeStream = db.collection('message').watch(찾을문서);
changeStream.on('change', result => {
console.log(result.fullDocument);
var 추가된문서 = [result.fullDocument]; // 화면에 반영할 부분
응답.write('event: test\n');
응답.write(`data: ${JSON.stringify(추가된문서)}\n\n`);
});
change stream을 이용해서 DB 감시 한다.
- 우선 { parent : 요청.params.parentid } 인 게시물들만 감시
- 그런 게시물들에 변동사항이 생기면 [result.fullDocument] 이걸 유저에게 보내줌
- 물론 [], {} 이런 자료들은 JSON으로 바꿔서 보내야한다.
socket.io
SSE(서버에서 일방적으로 실시간 응답) 말고도 서버와 유저간에 Web Socket을 통해 양방향 실시간 통신이 가능하다.
npm install socket.io
라이브러리 설치
// (server.js)
const http = require('http').createServer(app);
const { Server } = require("socket.io");
const io = new Server(http);
- const app = express()보다 하단에 적용
// 기존
app.listen(process.env.PORT, function(){
console.log('listening on 8080')
});
// 변경
http.listen(process.env.PORT, function(){
console.log('listening on 8080')
});
app: express를 이용해서 서버를 띄움
http: 기본 nodejs 라이브러리 + socket.io를 이용해서 서버를 띄움
- 웹소켓 연결
// (server.js)
app.get('/socket', function(요청,응답){
응답.render('socket.ejs')
});
io.on('connection', function(){ // 웹소켓으로 서버에 connection했을 때
console.log('연결되었어요');
socket.on('user-send', function(data){ // user-send라는 이벤트 발생 시 data 실행
console.log(data) // '안녕하세요'
});
});
연결 됐을 때 할 작업
// ejs
<body>
<script src="https://cdn.socket.io/4.4.0/socket.io.min.js" integrity="sha384-1fOn6VtTq3PWwfsOrk45LnYcGosJwzMHv+Xh/Jx5303FVOXzEnw0EpLv30mtjmlj" crossorigin="anonymous"></script>
<div>채팅방</div>
<button id="send">서버에 메세지 보내기</button>
<script>
var socket = io(); // 웹소켓을 이용해 서버와 실시간 소통채널 개설
$('#send').click(function(){
socket.emit('user-send', '안녕하세요') // (이벤트 명, 전달할 데이터)
})
</script>
</body>
- package.json에 있는 버전 맞춰서 cdn 설치
- emit: 'user-send'라는 이벤트 명으로 보낸 '안녕하세요'라는 데이터 수신
- 서버에서 클라이언트로 데이터 보내기
io.emit('작명', '보낼메세지');
모든 유저에게 메세지를 보낸다 (broadcast 한다)
// (server.js)
io.on('connection', function (socket) {
socket.on('user-send', function (data) { // user-send 이벤트가 일어나면
io.emit('broadcast', data) //모든사람에게 데이터 전송
});
});
io.on('connection', function(socket){
console.log(socket); // header, id 등이 출력 됌
io.to(socket.id).emit("broadcast", '서버응답임'); 원하는 소켓id를 가진 사람에게만 메세지 보냄
});