만만치 않았던 숙련주차를 끝나고 오늘부터 심화주차에 진입했다. 오늘 심화주차에서 배운 것은Prettier, Socket, Refresh Token, 트랜젝션에 대한 개념을 배우고, 약간의 실습도 하였다.
해당 챕터에서 배운 것은 보통 개발자마다 각자의 코드를 작성하는 다양한 기준이 있을 것이다. 이런 것들이 코드 서식이라고 하는데 여러분이 코드를 작성하는 방식을 말한다. 보통 사소한 것으로도 싸울 때가 많은 데, 예를 들면, space, tab으로 싸운다던가 세미클론 넣고 안 넣고로 싸우던가, 줄바꿈이나 괄호 기준이 다르다던가 등 이런 것으로도 싸우는 경우도 발생하기도 한다.
그런 코드 서식을 관리해주는 기능이 있는데 그게 바로 Prettier(프리티어) 패키지이다. 보기 어려운 코드들을 쉽게 변환 해주는 기능이기도 하다.
사용하는 방법은 간단한데, 다음 같은 패키지를 설치해주면 된다.
npm i prettier -D
그런 다음에 app.js 옆에 파일 하나 만들어서 .prettierrc.js 를 만들고 다음과 같은 코드를 추가하면 된다. 이 부분은 입맛에 맞게 자유롭게 바꿀 수 있고 설정 할 수 있다. 아래는 어떻게 적는지 예시코드이다.
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: true,
singleQuote: true,
arrowParens: 'always',
};
그런 다음에 package.json 파일에 가서 script 안에다 다음과 같은 문구를 추가한다.
{
"name": "여러분의 프로젝트 이름일거예요",
"version": "1.0.0",
"scripts": {
"start": "node app.js",
"prettify": "prettier --write *.js **/*.js" // 추가 코드
},
... 생략
}
마지막으로 터미널에서 해당 명령을 실행하면 .prettierrc.js에 맞게 프로젝트안에 있는 모든 파일들이 코드 서식에 맞게 정렬이 된다.
npm run prettify
코드 서식 정리 하는 방법은 위에 설정한 이후에는 VScode에서도 사용 하는 방법이 있다. VSCode에 가서 Prettier 라는 플러그인 설치하면 된다.
하는 방법은 다음과 같다.
VsCode에서 아래 단축키를 입력해서 Format Document를 열어주세요! (Window: Shift + Alt + F mac: ⇧+⌘+P)
그런 다음 Configure Default Formatter창이 나올경우 Configure… 항목을 선택한 후 "Select a default formatter …" 라고 뜰텐데, 아래에 보이는 "Prettier - Code formatter" 를 선택하면 된다.
마지막으로 프리티어 사용하는 규칙에 대해서 설명하는데 다음과 같이 적용이 가능하다.
true
인 경우 세미콜론을 항상 붙이도록 해줍니다.이런 식으로 기준은 본인에 입맛에 스스로 원하는 대로 정하면 된다.
해당 챕터에서는 소켓의 개념을 배우고, Socket.io를 적용해보는 실습을 하였다. 소켓을 배우기 전에 TCP와 UDP의 개념과 차이를 정리하였다.
그리고 나서 Socket의 개념을 강의노트에는 다음과 같이 정리가 되었다.
socket.io
는 웹소켓을 포함하여, 웹소켓을 사용하지 못하는 환경에서도 웹소켓과 비슷하게 사용이 가능하도록 구현해놓은 라이브러리입니다. 그렇기 때문에 socket.io
는 웹소켓과 완전히 동일하다고 오해하지 않으시길 바랍니다!이제 Socket.io 사용하는 방법은 다음과 같이 패키지를 설치하면 된다.
npm i socket.io -S
그런 다음에 각각 index.html, app.js 파일을 만들어서 다음과 같이 코드를 작성하면 Socket.io의 기본세팅은 마무리 된 것이다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.socket.io/socket.io-3.0.1.min.js"></script>
<title>Hello Socket.io!</title>
</head>
<body>
<script>
const socket = io("ws://localhost:3000");
socket.on("connect", () => {
socket.send("Hello!");
});
socket.on("message", (data) => {
console.log(data);
});
</script>
</body>
</html>
// app.js
const io = require("socket.io")(3000, {
cors: {
origin: "*",
methods: ["GET", "POST"],
},
});
io.on("connection", (socket) => {
console.log("새로운 소켓이 연결됐어요!");
socket.on("message", (data) => {
console.log(data);
});
});
이걸로 node app.js로 서버 실행 후 크롬에다 index.html을 자체를 붙혀놓으면 실행 된 것을 확인 할 수 있다.
여기까지는 기본세팅이고 이 뒤로는 저번 숙련주차때 했던 SPA_MALL 프로젝트에다가 Socket.io를 적용하는 실습을 해 보았다. 누군가 구매를 했다면 실시간으로 왼쪽 하단에 다음과 같이 알림창이 뜨는 실습을 하게 되었다.
이런 식으로 프론트앤드에서도 이런 화면이 뜨게 구현을 해 보았고, 백엔드에서도 로그를 남길 수 있는 작업도 해 보았다.
이렇게 socket.io에 대해서 개념을 배워서 약간의 실습도 진행하였다. 다만, 자세한 실습 코드까지 올리면 엄청난 스압이 있기에 이런 것을 배우고 구현 연습을 했다는 것만 기록으로 남겼다.
지난 숙련주차때 jwt 토큰을 이용해서 구현했던 것은 Access 토큰이였다. 하지만 이번에는 Refresh 토큰까지 같이 구현하는 실습을 가지게 되었다.
먼저 Access Token와 Refresh Token에 대해서 개념과 장점을 강의노트를 참고하여 정리를 하면 다음과 같다.
1) Access Token이 무엇인가요?
Access Token은 사용자의 권한이 확인(ex: 로그인) 되었을 경우 해당 사용자를 인증하는 용도로 발급하게됩니다.
이전에 저희가 구현하였던 Cookie로 jwt를 발급하고 설정한 Expires 기간이 지날 때 인증이 만료되게 하는것 또한 Access Token이라고 부를 수 있습니다.
사용자가 Access Token을 가지고 인증을 요청할 경우 Token을 생성할 때 사용한 비밀키(Secret Key)를 가지고 인증하기 때문에, 복잡한 설계없이 코드를 구현할 수 있고, 여러 분기를 거치지 않아도 된다는 장점이 있습니다.
2) Refresh Token이 무엇인가요?
Refresh Token은 Access Token 처럼 해당하는 사용자의 모든 인증 정보를 관리하는 것이 아닌, 특정한 사용자가 Access Token을 발급받기 위한 용도로만 사용됩니다.
Refesh Token은 사용자의 인증정보를 사용자가 가지고 있는 것이 아닌, 서버에서 해당 사용자의 정보를 저장소 또는 별도의 DB에 저장하여 관리합니다. 그렇기 때문에, 서버에서 특정 Token 만료가 필요할 경우 저장된 Token을 제거하여 사용자의 인증 여부를 언제든지 제어가 가능하다는 장점이 있습니다.
그래서 Refresh Token은 사용자가 서버와 최초 인증시에 발급을 받게되는데, 이번 프로젝트에서 하나의 파일에서 Refesh Token과 Access Token이 어떤식으로 동작하는지 확인해보는 작업을 진행해 보았다. 시작은 당연히 터미널에 다음과 같은 토큰과 관련된 패키지를 설치하였다.
npm init -y
npm install express jsonwebtoken cookie-parser -S
이제 app.js에서 다음과 같은 두가지의 코드를 추가하였다, 먼저 Refresh Token과 Access Token을 발급하는 API를 만드는 코드는 다음과 같다.
let tokenObject = {}; // Refresh Token을 저장할 Object
app.get("/set-token/:id", (req, res) => {
const id = req.params.id;
const accessToken = createAccessToken(id);
const refreshToken = createRefreshToken();
tokenObject[refreshToken] = id; // Refresh Token을 가지고 해당 유저의 정보를 서버에 저장합니다.
res.cookie('accessToken', accessToken); // Access Token을 Cookie에 전달한다.
res.cookie('refreshToken', refreshToken); // Refresh Token을 Cookie에 전달한다.
return res.status(200).send({ "message": "Token이 정상적으로 발급되었습니다." });
})
// Access Token을 생성합니다.
function createAccessToken(id) {
const accessToken = jwt.sign(
{ id: id }, // JWT 데이터
SECRET_KEY, // 비밀키
{ expiresIn: '10s' }) // Access Token이 10초 뒤에 만료되도록 설정합니다.
return accessToken;
}
// Refresh Token을 생성합니다.
function createRefreshToken() {
const refreshToken = jwt.sign(
{}, // JWT 데이터
SECRET_KEY, // 비밀키
{ expiresIn: '7d' }) // Refresh Token이 7일 뒤에 만료되도록 설정합니다.
return refreshToken;
}
코드를 간략하게 설명하자면 사용자가 GET
/set-token/:id
API를 호출했을때 Access Token과 Refresh Token을 2개 발급하게 되고, Refresh Token을 Key 값으로 입력된 id를 찾을 수 있게 하는 코드라고 보면 된다.
그리고 accessToken
, refreshToken
이라는 Key로 Cookie를 2개 발급하게 됩니다. 이때 각각의 만료시간은 10초, 7일간의 설정을 두게 하였다.
다음은 Refresh Token과 Access Token을 검증하는 API를 만드는 코드는 다음과 같이 적으면 된다.
app.get("/get-token", (req, res) => {
const accessToken = req.cookies.accessToken;
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.status(400).json({ "message": "Refresh Token이 존재하지 않습니다." });
if (!accessToken) return res.status(400).json({ "message": "Access Token이 존재하지 않습니다." });
const isAccessTokenValidate = validateAccessToken(accessToken);
const isRefreshTokenValidate = validateRefreshToken(refreshToken);
if (!isRefreshTokenValidate) return res.status(419).json({ "message": "Refresh Token이 만료되었습니다." });
if (!isAccessTokenValidate) {
const accessTokenId = tokenObject[refreshToken];
if (!accessTokenId) return res.status(419).json({ "message": "Refresh Token의 정보가 서버에 존재하지 않습니다." });
const newAccessToken = createAccessToken(accessTokenId);
res.cookie('accessToken', newAccessToken);
return res.json({ "message": "Access Token을 새롭게 발급하였습니다." });
}
const { id } = getAccessTokenPayload(accessToken);
return res.json({ "message": `${id}의 Payload를 가진 Token이 성공적으로 인증되었습니다.` });
})
// Access Token을 검증합니다.
function validateAccessToken(accessToken) {
try {
jwt.verify(accessToken, SECRET_KEY); // JWT를 검증합니다.
return true;
} catch (error) {
return false;
}
}
// Refresh Token을 검증합니다.
function validateRefreshToken(refreshToken) {
try {
jwt.verify(refreshToken, SECRET_KEY); // JWT를 검증합니다.
return true;
} catch (error) {
return false;
}
}
// Access Token의 Payload를 가져옵니다.
function getAccessTokenPayload(accessToken) {
try {
const payload = jwt.verify(accessToken, SECRET_KEY); // JWT에서 Payload를 가져옵니다.
return payload;
} catch (error) {
return null;
}
코드를 간략하게 설명하자면 사용자가 발급받은 Access Token과 Refresh Token을 가지고 API를 호출할 때, 가지고 있는 토큰에 상태에 맞게 Response가 반환되는것을 확인할 수 있다.
Access Token이 정상적으로 인증되었을 경우 Payload의 값을 Response에서 확인할 수 있게 되었다. 참고로 아까도 말했지만 Access Token의 만료기간이 10초로 설정되어 있기 때문에 10초 뒤에 검증을 눌러도 만료되었다고 나오게 된다. 즉 10초내로 토큰을 받고 확인을 해야 정상적으로 검증이 되는 코드라고 보면 된다.
해당 챕터에서는 트랜젝션(ACID)의 개념을 정리하고 그것에 대한 성질을 정리하였다.
트랜잭션(Transaction)은 작업의 완전성을 보장해주기 위해 사용되는 개념입니다. 특정한 작업을 전부 처리하거나, 전부 실패하게 만들어 데이터의 일관성을 보장해주는 기능입니다.
그거에 대한 성질은 총 4가지인데 정리하면 다음과 같다.
MySQL에서의 트랜젝션은 다음과 같은 쿼리를 작성하면 명령을 내릴 수 있다.
-- 트랜잭션을 시작합니다.
START TRANSACTION;
-- 성공시 작업 내역을 DB에 반영합니다.
COMMIT;
-- 실패시 START TRANSACTION이 실행되기 전 상태로 작업 내역을 취소합니다.
ROLLBACK;
그 다음으로는 락(luck)에 대해서 개념을 정리 하였다.
락(Lock)은 동시성을 제어하기 위해 사용하는 기능입니다. 해당하는 데이터를 점유하여 다른 트랜잭션의 접근을 막아 동시성과 일관성의 균형을 맞추기 위해 사용합니다.
락의 종류와 락킹 수준의 개념도 배웠는데 다음과 같이 정리하였다.
**READ
전용 락**이라고 불리기도 하며, 해당 락을 사용하는 트랜잭션이 모든 작업을 수행하였다면 공유 락은 해제됩니다.**WRITE
전용 락이라고 불리며, 트랜잭션이 해당하는 데이터를 점유한 후 다른 트랜잭션이 해당 데이터에 접근 할 수 없도록** 만듭니다.다음은 교착상태와 트랜젝션의 격리 수준에 대해서 정리를 하였다.
교착 상태(Dead Lock)는 여러 테이블에 락(Lock)을 적용하여, 다른 작업이 처리되지 못하게 점유하고 있는 작업이 있을 때, 다른 작업을 끝나는 것을 무한정 기다리는것을 나타냅니다.
트랜잭션의 격리 수준 (Isolation Level)은 여러 트랜잭션이 동시에 처리될 때 다른 트랜잭션에서 변경 및 조회하는 데이터를 읽을 수 있도록 허용하거나 거부하는 것을 결정하기 위해 사용하는 것 입니다.
**READ UNCOMMITTED**
**READ COMMITTED**
SELECT
문을 실행할 때 공유락을 겁니다.REPEATABLE READ
**SERIALIZABLE**
마지막으로 Sequelize의 트랜잭션은 COMMIT, ROLLBACK을 수동으로 사용하여 트랜잭션을 관리하는 UnManaged 트랜잭션과 Sequelize가 자체적으로 트랜잭션의 성공과 실패를 관리하는 Managed 트랜잭션이 있다.
이제 지난번에 sequelize를 이용해서 게시판 프로젝트 실습을 진행했었다. 역시 실습한 코드를 모두 보여주면 엄청난 스압이 되기에 어떤걸 실습 했는지만 설명하겠다. 강의 실습에서 이번에 UnManaged 트랜잭션을 이용해서 구현을 진행하였고, 트랜젝션을 이용해서 회원가입 API를 먼저 구현하였다. 그 다음에는 사용자 히스토리(UserHisotires) 테이블을 추가 작업을 하고 난 후 사용자 이름을 변경하는 API 만들되 UUID라는 것을 이용해서 실습을 하였다.
여기서 UUID(Universally Unique Identifier, 범용 고유 식별자)는 총 4개의 정보를 하이픈(-) 으로 구분하여 순차적으로 저장한 데이터 타입이다. 시간 정보를 포함하고 있어 생성된 순서대로 정렬이 되는 특징을 가지고 있다.
이렇게 해서 트랜잭션을 이용한 구현은 마무리 되었다. 코드 작성 하는데에는 어려움에는 없었으나 개념적으로 정리 하는것이 어려웠다고 생각했던 챕터였다.
그렇게 오늘 배운 것으로는 Prettier, Socket, Refresh Token, 트랜젝션에 대한 개념을 배우고, Token으로 인한 실습과, 트랜잭션을 이용한 간단한 실습을 하였다.
이번에 새롭게 배우는 것들은 상당히 이해하기 어려웠던 개념들이 많았었다. 일단 심화에서 배운 개념은 나중에 개인과제 lv4 할때에는 당장 많은 것들을 적용하지 않는 개념들이지만, 나중에 실무에서는 도움이 되었던 개념들을 설명을 했다는 것을 느꼈다. 그래도 저번 숙련주차때 이해하기 힘들었던 sequrlize 개념은 이번에 심화 주차때 다시 트랜잭션을 공부하면서 어떻게 흘러가는지 이해를 많이 하게 되었던 날이였다. jwt 토큰의 개념도 다시 한번 더 생각 할 수 있었던 하루 였고, 새로운 것을 적용하면서 배워 보는 경험을 해 보았다. 오늘 시도 한 것에서는 적지는 않았지만, 내일 강의로 배울 예정인 Layered Architecture Pattern 의 세션을 미리 듣게 되었다. 간단하게 말하자면 뭔가 요청을 했을 때 컨트롤러, 서비스, 레파지토리를 지나서 데이터베이스의 정보를 가져다와서 다시 역순으로 레파지토리, 서비스, 컨트롤러를 지나서 데이터베이스에 저장된 것들을 받아오는 구조라고 생각하는 것으로 이해했다. 자세한건 내일 다시 해당 개념을 배울 때 이해하기 쉬울 것이라고 예상한다. 실습 자체는 어렵진 않았지만 개념을 이해하는데 힘든 하루였다고 생각했다.
내일은 객체지향이랑 solid의 개념이랑 아까 말했던 Layered Architecture Pattern 개념과 그거에 대한 강의 실습을 하면 심화주차 강의는 마무리 된다. 여기까지 하면 주특기의 모든 강의는 전부 듣게 된다.