일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- express
- 게임개발
- 게임
- frontend
- 스프링
- MongoDB
- react
- 백엔드
- bcrypt
- springboot
- IntelliJ
- unity
- JSON
- 유니티
- RiotAPI
- 백준
- 깃
- c#
- oAuth
- 파이썬
- 코딩
- AWS
- Python
- node.js
- jwt
- OAuth2.0
- spring
- 프로그래밍
- netlify
- 스프링부트
- Today
- Total
Unwound Developer
Node.js와 Riot API를 활용한 프로젝트 Arcane 본문
개요
https://github.com/ysh038/Arcane
https://project-arcane.netlify.app
(Riot API가 잘 최신화 되지 않고, 컨벤션이 일정하지 않은 관계로 API로 받아오는 정보들이 잘 나타나지 않을 수도 있습니다.)
이번에 Node.js를 공부하면서 친구 한 명과 2인이서 프로젝트를 진행했습니다.
진행할 때, 차근차근 글을 작성하면서 했어야하는데, Node.js, Express 등등 프로젝트를 진행하면서 필요한 부분을 그때 그때 공부하면서 진행하느라 다 하고 나서야 글을 작성합니다.
프로젝트 주제로 Riot API를 활용한 웹페이지를 선정하게 된 이유는 첫번째로, 친구랑 같이 개발하는 첫 번째 프로젝트였기에 너무 무거운 주제보단 주위에서 가볍게 볼 수 있는 주제를 선정하길 원했습니다. 그리고 오픈 API를 찾아보는 와중에 '롤'이라고 불리는 인기 온라인 게임 'League of Legends'의 API를 무료로 제공하는것을 보고 선정했습니다.
다음은 Riot API를 제공받을 수 있는 사이트의 링크입니다. Riot API는 다양한 정보를 제공해주는데, 보통 'League of Legends'(이하 롤) 유저들의 게임 플레이 기록에 관한 정보들이 주를 이룹니다. 그래서 유저들의 전적 검색 기능이 주 기능인 롤 전적 검색 사이트를 개발해봤습니다. 이미 op.gg등의 실제로 서비스중인 전적검색 사이트 많은데, 이 웹페이지들을 참고해서 어떤식으로 기능을 만들었을까를 생각하며 프로젝트를 진행했습니다.
기술스택
언어, 플랫폼
도구
실행환경
프로젝트 제작과 실행은 2인이서 진행하였는데, 모두 윈도우 OS환경에서 실행했습니다.
Client 부분은 JavaScript의 라이브러리인 React환경에서 개발, 실행했습니다.
배포는 무료로 사용가능한 Netlify에서 진행했습니다.
Server 부분은 자바스크립트 RunTime인 Node.js와 그의 프레임워크인 Express로 개발, 실행했습니다.
배포는 AWS의 FreeTier 기간을 이용해 AWS로 배포했습니다.
데이터베이스는 No SQL 데이터베이스인 MongoDB를 사용했습니다.
페이지 작동 방식
Arcane 페이지의 작동방식은 Client, Server, DB간의 상호작용으로 이루어져있습니다.
다음은 Client, Server, DB의 간단한 Request,Response 구조입니다.
로그인,회원가입 Process
sequenceDiagram
Client->> Server: 개인정보 입력 후 회원가입요청
Server->>DB: 패스워드 Bcrypt로 암호화 후 DB에 저장
Note right of Server : Client와 Server간 통신은 Axios를 통해서
Note right of DB: 회원가입 완료
DB->>Server: 회원가입 요청이 올바르게 종료됐음을 응답
Server->>Client: 회원가입 완료했음을 응답
Client->> Server: 로그인 요청
Server->> DB: 입력한 정보를 Bcrypt의 decode메소드로 해독, 존재하는 사용자인지 확인요청
Note right of DB: 입력한 정보를 가진 사용자가 있음을 확인
DB->>Server: 로그인 수락
Server->>Client: 로그인 수락
전적 검색 Process
sequenceDiagram
Client->> Server: 이미 DB에 존재하는 사용자의 전적을 요청하는지 확인요청
Server->> DB : Client가 요청한 사용자가 DB에 이미 존재하는지 확인
Note right of DB : 요청한 사용자가 존재한다면
DB-->> Server : 요청한 사용자의 데이터 전달
Server -->> Client : 요청한 사용자의 데이터 전달
Note left of Client : 종료
Note right of DB : 요청한 사용자가 없다면
DB-->> Server : DB에 요청한 사용자가 없음을 알림
Server-->>Client : DB에 요청한 사용자가 없음을 알림
Note left of Client: 사용자가 존재하지 않는다면 아래 실행
Client->> Riot: api인증키를 사용해 riotAPI요청
Riot->>Client: 인증키가 올바르다면 요청한 데이터 응답
Note right of Client: 받은 데이터 client에게 보여줌
Client->> Server: riotAPI에게 받은 데이터 전달
Server->>DB: Client에게 받은 riot데이터 저장/가공 요청
DB->>Server: 올바르게 처리됐음을 알림
Server->>Client: 올바르게 처리됐음을 알림
라이브러리
Arcane페이지가 동작하는데 있어서 중요한 역할을 하는 라이브러리 몇가지가 있습니다.
다음은 중요하면서 자주사용된 라이브러리들 입니다.
Axios
Client와 Server의 비동기 통신을 위한 Promise 기반 라이브러리입니다.
await axios
.get("/auth/login", {
params: {
username: inputUsername,
password: inputPassword,
},
}) //
.then((res) => {
token.saveToken(res.data.token);
window.location.replace("/");
})
.catch((err) => {
console.log(err);
if (err.response.status === 401) {
alert("아이디(비밀번호)가 틀렸습니다");
} else {
alert("로그인에 실패했습니다.");
}
});
클라이언트에서 서버로 요청을 보낼 때 POST, GET, PUT, DELETE 이 4가지의 메소드를 가지고 CRUD를 할 수 있습니다. 서버에서도 마찬가지로 각각의 요청에 대해 응답을 해줍니다.
// 회원가입 & 로그인 & 유저관련 디비 설정
app.use("/auth", authRouter);
// 글 작성
app.use("/post", postRouter);
// 유저 전적 검색
app.use("/api/summoners", summonersRouter);
// 내 정보
app.use("/api/mypage", mypageRouter);
// 위의 라우터 모두 충족하지 않을경우
app.use((req, res, next) => {
res.sendStatus(404);
});
// 에러 발생시
app.use((error, req, res, next) => {
console.error("error: " + error);
res.sendStatus(500);
});
const router = express.Router();
router.get("/", authController.me);
router.get("/info", authController.IsExistFromClient);
router.get("/login", authController.login);
router.get("/check", authController.checkMarking);
router.get("/exist", authController.IsExistFromClient);
router.post("/signup", authController.signup);
router.post("/marking", authController.bookMarking);
export default router;
Server에선 Routing을 통해 시각적으로 정돈되어보일 뿐 아니라, 개발과 유지보수에 불편함을 줄였습니다.
Axios는 Promise를 리턴하기 때문에, Arcane 웹 페이지에서 Client와 Server간 통신은 대부분 이렇게 비동기로 이루어집니다.
따라서, Client가 Server에 뭔가 요청한 후에 Server의 응답을 기다린 후 진행합니다.
bcrypt, jwt
Client에서 회원가입을 요청했을 때, DB에 패스워드 원본으로 저장한다면 보안 문제가 있을것입니다. bcrypt는 Blowfish 암호를 기반으로 설계된 암호화 함수이며 현재까지 사용중인 가장 강력한 해시 메커니즘 중 하나입니다. 이를 통해, 패스워드를 원본 그대로 저장하지 않고 암호화한 상태로 저장합니다.
// 데이터베이스의 사용자 정보들과 조회 하여 일치하는거 찾기
const user = await userRepository.findByUsername(username);
// 존재하는 유저라면 해당 유저의 비밀번호가 맞는지 체크
if (!user) {
console.log("아이디가 존재하지 않습니다.");
return res.status(401).json({ message: "Invalid user or password" });
}
// 유저 존재시 비번 체크
// bcrypt의 compare을 사용하여 우리 데이터베이스에 저장된 hash버전의
// password와 사용자가 입력한 password가 동일한지를 검사
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
//비번 틀릴시
return res.status(401).json({ message: "Invalid user or password" });
}
// userRepository에서 받아온 사용자 고유 id로 토큰을 만듬
const token = createJwtToken(user.id);
res.status(201).json({ token, username });
bcrypt의 hash메소드를 이용해서, 입력받은 패스워드를 암호화합니다.
파라메터로 SaltingRounds라는 것을 보내주는 것을 볼 수 있습니다. Salting은 사용자가 보낸 비밀번호에 난수까지 추가하여 해시함수에 집어넣는 것입니다. Salt를 적용하여 나온 해시에다가, 다시 동일한 Salt을 적용하여 한번 더 해시를 도출하고, 이걸 계속 반복시키는 행위를 Salting Rounds라고 합니다. Arcane에서는 12로 값을 할당했습니다.
이렇게 암호화한 패스워드를 jsonwebtoken의 createJwtToken메소드를 이용해, 검증된 토큰을 생성해서 Client에 돌려준다. 그리고, Client는 서버로부터 받은 인증된 토큰을 LocalStorage에 가지고있다가,여기에 local storage 사진 무언가 서버에 요청할 때, 이 인증된 토큰을 함께 Header 등에 넣음으로 로그인 된(Authenticated)사용자 임을 Server에 알린다.
mongoDB, mongoose
mongoDB는 noSQL 데이터베이스입니다. noSQL은 'Non Relational Operation Database SQL'의 줄임말로써 기존의 관계지향형 데이터베이스가 아닌 데이터베이스들을 의미합니다. noSQL DB에 대한 경험이 없어, 다양한 경험을 쌓고자 mongoDB를 선택했는데, 장점과 단점이 명확했습니다.
처음 noSQL로 서버와 데이터베이스 간 구조를 만들 땐, noSQL이 유연하고 간편해서 편리했습니다. 하지만, 생각보다 데이터간 참조가 많고 변경될 때도 많아서, 모든 Collection을 일일이 수정해야하는 일이 생겼습니다. 이에 프로젝트 진행시에 어떤 데이터베이스를 사용할지 쉽게 정해선 안 될것임을 생각했습니다.
mongoDB는 스키마가 없는 형태라고 했는데, Node.js에서 model이라는 mongoDB api로 스키마와 유사한 형태를 이룰 수 있습니다.
e: String } }],
});
const userSchema = new Mongoose.Schema({
username: { type: String, required: true },
password: { type: String, require: true },
email: { type: String, require: true },
signupDate: { type: Date, default: Date.now }, // 회원가입 일시
postlike: [postSchema],
bookMark: [],
});
const matchHistorySchema = new Mongoose.Schema({
matchId: { type: String, required: true },
summonerName: { type: String, required: true },
queueType: { type: String, required: true },
result: { type: String, required: true },
queueDate: { type: String, required: true },
champion: { type: String, required: true },
championLevel: { type: Number, required: true },
spell1: { type: String, required: true },
spell2: { type: String, required: true },
mainRune: { type: String, required: true },
subRune: { type: String, required: true },
item0: { type: String },
item1: { type: String },
item2: { type: String },
item3: { type: String },
item4: { type: String },
item5: { type: String },
item6: { type: String },
kills: { type: Number, reqired: true },
deaths: { type: Number, reqired: true },
assists: { type: Number, reqired: true },
kda: { type: String },
cs: { type: Number, reqired: true },
time: { type: String, reqired: true },
participants: [
{
summonerName: String,
champion: String,
},
],
});
const summonerSchema = new Mongoose.Schema({
summonerName: { type: String, required: true },
profileIconId: { type: Number, required: true },
level: { type: Number, required: true },
soloRankQueueType: { type: String, required: true },
soloRankTier: { type: String, required: true },
soloRankRank: { type: String },
soloRankLP: { type: Number, required: true },
soloRankWinNum: { type: Number, required: true },
soloRankLoseNum: { type: Number, required: true },
flexRankQueueType: { type: String, required: true },
flexRankTier: { type: String, required: true },
flexRankRank: { type: String },
flexRankLP: { type: Number, required: true },
flexRankWinNum: { type: Number, required: true },
flexRankLoseNum: { type: Number, required: true },
matchList: [matchHistorySchema],
});
export const User = Mongoose.model("User", userSchema);
export const Post = Mongoose.model("Post", postSchema);
export const Comment = Mongoose.model("Comment", commentSchema);
export const MatchHistory = Mongoose.model("MatchHistory", matchHistorySchema);
export const Summoner = Mongoose.model("Summoner", summonerSchema);
mongoose는 Server(Node.js)와 DB(mongoDB)를 연결시켜주는 ODM입니다. mongoose덕분에 Node.js에서 mongoDB와 상호작용하기가 매우 편리했습니다.
import { User } from "../model/schema.js";
// 사용자 아이디로 찾기
export async function findByUsername(username) {
return User.findOne({ username });
}
export async function findById(id) {
return User.findById(id);
}
export async function createUser(user) {
return new User(user)//
.save()
.then((data) => data.id)
.catch((err) => console.log(err));
}
mongoose의 메소드를 사용하는 것 만으로 데이터베이스에 접근해, 데이터를 가져오거나 추가하고 삭제하는 것이 가능했습니다.
Riot API
Arcane 프로젝트를 시작하게 된 이유입니다. Riot사의 API를 사용하기 위해서는 API 인증키가 필요합니다. 해당 키는 링크에서 로그인 후 발급받을 수 있습니다. 다음은 Riot API를 사용하는 예시입니다.
// riotAPI.js
// 특정 소환사의 pid, ppuid 등의 정보 가져오기
/**소환사명을 통해서 라이엇으로부터 해당 소환사 정보 불러오기 (id, puuid, account id, username, ....) */
async getSummoner(username) {
const link = `https://kr.api.riotgames.com/lol/summoner/v4/summoners/by-name/${username}?api_key=${this.#Riot_API_Key}`;
const json = await getAPI(link);
return json;
}
해당API는 riotAPI.js 라는 파일에서 관리합니다. getSummoner함수에서 소환사명(League of Legends계정의 사용자명)을 받으면, RiotAPI를 호출하는 링크를 만듭니다. 링크에는 함수에서 파라메터로 받은 사용자명, 이전에 발급받은 API인증키 등이 사용됩니다.
/**axios를 사용하여 해당 link의 api를 불러옴 */
async function getAPI(link) {
let data;
await axios
.get(link)
.then((res) => (data = res.data))
.catch((err) => {
throw err;
});
return data;
}
getAPI함수는 파라메터로 넘겨준 링크를 사용합니다. 이전에 만든 링크를 통해, 실제로 RiotAPI에게 데이터를 받아옵니다. (Axios사용)
getChampionIcon(champion_id) {
if (champion_id === "FiddleSticks") champion_id = "Fiddlesticks";
const link = `https://ddragon.leagueoflegends.com/cdn/${this.#Version}/img/champion/${champion_id}.png`;
return link;
}
getSkillIcon(skill_id) {
const link = `https://ddragon.leagueoflegends.com/cdn/${this.#Version}/img/spell/${skill_id}.png`;
return link;
}
API를 통해 이미지를 직접받아오는 함수들도 이런식으로 존재합니다.
다른 컴포넌트나 페이지에서 riotAPI.js를 라이브러리처럼 활용해, 위 같은 함수들을 호출합니다. 이를 통해, Riot의 League of Legends서버 데이터를 활용해 Arcane 웹 페이지에서 보여줍니다.
위 내용은 깃허브 README에도 작성되었습니다.
블로그에는 README에서 작성하지 않은 더 자세한 프로젝트의 구조와 코드를 작성해보겠습니다.
'Web > Node.js' 카테고리의 다른 글
Arcane 프로젝트 - netlify.toml (0) | 2022.11.14 |
---|---|
Arcane 프로젝트 - MongoDB와 Mongoose (0) | 2022.11.14 |
Arcane 프로젝트 - Axios 비동기 통신 (0) | 2022.11.13 |
Arcane - 클라이언트 구조 (0) | 2022.11.13 |
Arcane 프로젝트 - 서버 구조 (0) | 2022.11.11 |