1. CBD 구조의 이벤트 등록 문제

현재 CBD의 구조 상 이벤트를 component를 호출할 때마다 이벤트 목록을 확인하여 이미 등록된 이벤트는 중복 등록되지 않도록 하고 있다.

그래서 효율적이라고 생각했다. 하지만 하위 컴포넌트에서 이벤트를 등록해주고 이벤트 핸들러 안에서 this.prop로 상태를 참조할 때, 이벤트가 등록될 시점을 참조하고 있어서 setState로 전역 상태를 변경시켜줘도 변경된 state를 이벤트 핸들러 내부에서 참조할 수 없는 문제점이 발생하였다.

이러한 문제점을 해결하기 위해 구조를 바꿔보려는 시도를 하였지만, 프로젝트 시간 상 이는 감당하기 어려워 포기하였다.

컴포넌트 호출 시마다 이벤트 제거와 새로 등록

위 문제를 해결하기 위한 방법으로 컴포넌트를 호출할 때마다 등록된 이벤트를 제거하고 새로 이벤트를 등록하는 방법이 있다.

하지만 이벤트 핸들러를 **익명함수(() => {})**를 통해 등록해주고 있었기 때문에 이를 전부 기명함수로 변경해줘야지만 removeEventListener() 메서드를 사용하여 등록된 이벤트를 제거할 수 있었다.

하지만, 기명함수로 바꿔서 addEventListener() 메서드로 등록을 해줘도 제대로 동작하지 않았고 이부분에서 많은 시간을 할애하였다. 지금 내가 해결하려는 문제가 작은 부분에 몰두해있는 것 같고 시간이 부족하여 방법을 바꾸기로 하였다.

그래서 현 구조에서 하위 컴포넌트의 상태를 관리해주기 위해 App 컴포넌트에 상태를 등록하여 전역 상태로 관리해주기로 결정하였다.

전역 상태로 하위 컴포넌트 상태 관리

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
let openCard = [];
let matchedCard = [];
let isStarted = false;
let convertedTime = 0;

class MatchingCards extends Component {
addEvent() {
return [
this.createEvent({
type: "click",
selector: ".start-button",
handler: () => {
this.props.start();
},
}),
this.createEvent({
type: "click",
selector: ".card-front",
handler: (e) => {
if (openCard.length === 2 || !isStarted) return;

this.props.checkCard(+e.target.closest(".cards").dataset.id);
if (openCard.length === 2) {
if (
this.props.shuffledNum[openCard[0] - 1] ===
this.props.shuffledNum[openCard[1] - 1]
) {
console.log("HOLYMOLY");
this.props.matchCard(openCard);
matchedCard = [...matchedCard, ...openCard];
}
setTimeout(() => {
this.props.resetOpenedCard();
}, 500);
}
},
}),
this.createEvent({
type: "click",
selector: ".reset-button",
handler: () => {
openCard = [];
matchedCard = [];
this.props.resetGame();
},
}),
];
}

domStr() {
openCard = this.props.openCard;
isStarted = this.props.isStarted;
convertedTime = this.props.convertedTime;

return `
<div class="container">
<h1 class="game-title">MATCHING CARDS</h1>
<div class="card-container">
${this.props.shuffledNum
.map(
(num, index) => `
<div class="cards ${
openCard.includes(index + 1) || matchedCard.includes(index + 1)
? "opened"
: ""
}" data-id="${index + 1}">
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">${num}</div>
</div>
</div>
`
)
.join("")}
</div>
<p class="result-message ${
matchedCard.length === 18 ? "" : "hidden"
}">Congratulations!</p>
<p class="display">${convertedTime}</p>
<div class="active-button-container">
<button class="start-button">Start</button>
<button class="reset-button">RESET</button>
</div>
</div>
`;
}
}
  • 전역 상태를 domStr()을 호출할 때 지역 변수에 할당하여 지역 변수를 가지고 이벤트 핸들러 내부에서 로직을 구현하였다.

  • 이로 인한 문제점이 게임의 진행을 위해서는 렌더링을 해주기 위해서는 전역 상태를 setState로 변경해줘야 하고 지역 변수를 변경도 이벤트 핸들러 내부에서 따로 해줘야 하므로 같은 로직을 구현하는 상태를 2번 관리해줘야 하는 불편함이 생겼다.

  • 이는 다음주에 리팩토링이 필요할 것으로 보인다.

2. 로그인 기능 및 서버 구현

클라이언트 사이드

먼저 클라이언트는 로그인 페이지에서 input 값으로 요청을 보낸다. 이 때, 서버에 대한 요청을 e.preventDefault()로 막아주고 payload에다가 input 값을 담아서 axios로 post 요청을 보내주었다.(서버의 응답이 도착할 때 까지 await해주었다.)

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
29
30
31
// Signin.js
this.createEvent({
type: 'submit',
selector: '.signin_info',
handler: async e => {
e.preventDefault();

const payload = [...new FormData(document.querySelector('.signin_info'))].reduce(
(obj, [key, value]) => ((obj[key] = value), obj),
{}
);

try {
const { data: user } = await axios.post('/signin', payload);
console.log('😀 LOGIN SUCCESS!');
if (user) this.props.navigate('/', user);
localStorage.setItem('user', JSON.stringify(user));
} catch (e) {
console.log('😱 LOGIN FAILURE..');
document.querySelector('.error').classList.remove('hidden');
}
},
}),

// App.js
navigate(path, user = null) {
if (window.location.pathname === path) return;
window.history.pushState(null, null, path);
if (user) this.setState({ path, user });
else this.setState({ path });
}

서버에서 응답이 오면 navigate 함수에 메인페이지의 path와 응답으로 받은 데이터를 인자로 넘겨주었다. navigate 함수에서는 user가 있으면 user 상태를 변경하여 렌더링을 해주고 user가 없으면 path에 따라서만 라우터 변경과 렌더링을 해준다.

서버 사이드

서버에서는 해당 url로 post 요청이 왔을 때, 서버의 데이터에 해당 유저의 id, password를 비교하여 존재하는 유저인지 확인하여 JWT 토큰을 발행하여 쿠키에 담고 이 쿠키와 함께 유저의 닉네임을 응답으로 전달해주었다.

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
29
30
// server.js
app.post("/signin", (req, res) => {
const { userid, password } = req.body;
// userid 가 없거나, password가 없으면 띄워주는 에러메세지
if (!userid || !password)
return res
.status(401)
.send({ error: "사용자 아이디 또는 패스워드가 전달되지 않았습니다." });

// users배열에 userid와 password가 일치하는 user있는지 find 해서 user에 담아준다.
const user = users.find(
(user) => user.userid === userid && user.password === password
);

// 만약 위 검색에서 user가 담기지 않았다면 띄워주는 에러메세지
if (!user)
return res.status(401).send({ error: "등록되지 않은 사용자입니다." });

// 로그인 성공 시 user 가 있다면 jwt 토큰을 발급해서 response의 헤더에 쿠키에 담아서 보내줘야함.
const accessToken = jwt.sign({ userid }, process.env.JWT_SECRET_KEY, {
expiresIn: "1d",
});

res.cookie("accessToken", accessToken, {
maxAge: 1000 * 60 * 60 * 24 * 7, // 7d
httpOnly: true,
});

res.send(user.nickname);
});

JWT(JSON Web Token)이란?

Access token을 이용한 서버 인증 방식 중 하나이다.

서버에 로그인 요청이 오면 사용자를 확인한 다음 JWT을 발행한다. 생성한 토큰을 클라이언트에게 쿠키를 통해 전달해주고 클라이언트는 쿠키에 이 토큰을 저장한다.

토큰은 암호화되어있고 클라이언트에서는 암호화를 해석한 코드를 가지게 하면 안되며 암호를 해석하는 일은 서버에서만 해야한다.

이제 토큰이 있는 사용자는 권한이 필요한 요청을 할 때마다 토큰을 header에 실어 서버에 보낸다.

서버는 토큰을 해석하여 로그인한 사용자인지를 판단하여 성공 및 실패 응답을 보내준다.

.env 이란?

.env 파일에는 개발을 하면서 중요한 정보들 특히, DB 관련 정보와 API_KEY 등의 정보를 보관하는 용도의 파일이다.

이러한 파일들은 git에 올리면 안되므로 gitignore를 해줘야한다.

  • 사용방법
1
2
3
npm install dotenv  //다운로드

require("dotenv").config(); //js 파일 상단에 적어주기
  • .env 파일은 최상위 루트에 생성해야한다.
1
2
3
4
5
// .env
PORT = 5010;

// server.js
const port = process.env.PORT;
  • 이러한 방식으로 사용한다.

3. 로그인한 유저에게 인가

로그인한 유저는 JWT를 쿠키에 가지고 있으므로 권한이 필요한 요청을 할 때, 즉 로그인한 사용자만 접근 가능한 페이지에 접근 요청을 서버에 보내면 서버는 JWT를 해석하여 로그인한 사용자인지 아닌지를 판단하여 그에 알맞은 응답을 보낸다.

그에 대한 로직은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const auth = (req, res, next) => {
/**
* 토큰이 리퀘스트의 Authorization 헤더를 통해 전달되면 req.headers.authorization으로 전달받고
* 토큰이 쿠키를 통해 전달되면 req.cookies.accessToken으로 전달받는다.
*/
const accessToken = req.headers.authorization || req.cookies.accessToken;

try {
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_KEY);
console.log(`😀 사용자 인증 성공`, decoded);
next();
} catch (e) {
console.error('😱 사용자 인증 실패..', e);
res.status(401).send({ error: '등록되지 않은 사용자입니다.' });
}

app.get('/rank', auth, (req, res) => {
res.send('로그인된 사용자입니다.');
});
  • /rank url로 요청이 오면 auth 함수를 실행한다.

  • auth 함수에서는 요청의 header나 쿠키에 권한이나 access token이 있는지를 확인하여 이를 해석하고 로그인 성공 실패 유무를 판단하여 응답을 보내준다.

소감

프로젝트를 시작하기 전에 기술적인 기획이 매우 중요하다는 것을 깨닫게 되었다. 하위 컴포넌트에서 지역 상태를 관리할 수 없을 것이라는 것을 구조를 보고 알 수 있었더라면, 그에 따라 구조를 개선한 후 프로젝트에 임할 수 있었을 텐데 중간에 큰 문제에 봉착하게 되니 머리가 텅 비워지는 상황을 경험하였다. 결국 시간을 고려하여 회피를 하게되는 문제점이 발생하게 되었다.

팀원 중 한명이 몸이 안좋아 2명이서 프로젝트를 진행하게 되었다. 3명이서 같이 나눠서 공부하고 공유하면 더욱 좋았을텐데 이점은 조금 아쉽다. 건강상의 이유로 참여하지 못한 팀원도 안쓰러웠다. 다음주에 쾌유해서 돌아오면 그동안 공부했던 것을 공유해주면서 배웠던 지식을 다시 정리해봐야겠다.

댓글 공유

  • page 1 of 1

loco9939

author.bio


author.job