프로젝트를 마치며…

  • 기획을 할 때, 기술적으로 어떻게 풀어나갈지 기술기획을 꼼꼼히 하지 못한 것이 아쉬움

  • 이 때, 기술기획이 제대로 안이루어져서 CBD 라이브러리를 사용하면서 앞으로 겪게될 문제를 개발 과정에 파악하게 되어서 난감하였다. ex) 지역 상태 관리 어려운 점…

  • 노션으로 일정관리와 문서화를 템플릿을 적극 활용하여 일관성있게 해야겠다.

  • 라우터가 App에 종속되어 있는 것이 아니라 render 함수처럼 따로 분리되도록 하고 싶었는데, 그러기 위해서구조를 대폭 수정해야하여 시간적 어려움이 있어 아쉬웠다.

  • 매번 새로운 지식을 얻어감에 즐거움을 느꼈다. 공식문서 읽는 것은 아직 어렵지만, 예제를 따라해보고 이해한 후 공식문서 읽으면 잘 읽히는 것을 알게되었다.

  • 이전 프로젝트 때 했던 실수를 어느정도 개선해나가며 진행할 수 있어서 좋았다. ex) this 바인딩

  • 해결책을 생각해낼 때 과연 이게 진짜 해결책인지, 회피인지 한번 더 생각해보는 안목이 생겼다.

  • 어렵고 막히는 부분이 있을 때 팀원에게 물어보면서 같이 해결해나갈 수 있어 든든하고 즐거웠다.

댓글 공유

1. 라우터 파일 구분을 해주자.

왜 라우터를 모듈로 빼서 관리를 해줘야 하는가?

App 컴포넌트가 다른 컴포넌트 정보를 모아서 렌더에게 이렇게 그려달라는 정보를 담고 있는데, App 에서 path에 맞게 컴포넌트를 지정해주는 router 메서드가 같이 있어도 논리가 맞지 않는가?

라우터가 하는 일은 path를 인수로 받으면 history API를 설정해주고, path에 맞는 컴포넌트를 반환해주는 역할을 한다.

이 역할을 하는 라우터를 App 컴포넌트의 프로퍼티 메서드로 종속 시키는 것이 맞는가?

내 생각에는 데이터가 바뀔 때 화면 렌더링에 영향을 주는 데이터를 상태로 관리 해야하므로, path가 바뀔 때, 컴포넌트가 바뀌니 path든 component든 상태로 가져야 한다.

즉, App도 컴포넌트이므로 그려지는데 필요한 정보를 담고 있기만 하면 된다.

  • 라우터에서 렌더링을 호출하면 App의 domStr이 새로 그려지게되는데 이때, Page 또는 Path를 확인하여 컴포넌트를 그려줘야한다.
  • 그런데 라우터함수가 렌더만 해주는데 상태를 변경해주지 않고 어떻게 그려주지?
    ⇒ 라우터 함수가 Page를 반환하고 그 페이지를 App의 domStr에서 받아서 그려주기로 해보자.
    • 위의 것이 될까? App에서 처음 호출될 때는 return 값으로 Page를 넘겨줄 수 있겠지
    • 하지만 이벤트 발생하거나 서버에 요청으로 라우터 변경되었을 때, router가 호출되는데, 그 때는 반환만 해주고 렌더링을 해주고 있지 않아서 변경되지 않을 것이다.
    • 그렇다고 router에 렌더링을 넣어준다고 하면 App의 domStr을 호출하여 새로 그려줄텐데… 무엇을 기준으로 그려주는 것인가? router에서 받은 path를 기준으로 그 path에 맞는 컴포넌트로 그려주는데 이것을 App에서 어떻게 알 수 있나?

결론

이를 해결하기 위해서는 기존의 App 컴포넌트에서 setState로 render를 시켜주는 구조를 바꾸고 App에서 render를 해주고 router에서도 render를 해주는 구조로 바꿔줘야한다.

소감

오늘은 라우터를 파일로 분리하려는 시도와 리팩터링을 하면서 공부를 했다.

내일은 GCP로 배포하는 법을 공부해서 배포를 해보려고 한다.

내일 배포와 발표준비를 마치면 이제 자바스크립트 수업도 모두 끝이난다. 2달 반 정도의 시간이 참 빨리도 지나간 것 같다.

역시 자바스크립트가 동료들과 페어로 하는 수업이 많아서 더 재밌게 느껴진 것 같다.

앞으로 다가올 리액트 수업 때도 열심히 듣기 위해 만반의 준비를 해야겠다.

댓글 공유

라우터 구현

카테고리 Project, holymoly

1. 회원가입 로직 구현

회원가입 페이지의 input값과 유효성 검사에 따라 에러메세지를 보여줘야 하므로 이를 상태로 관리해줘야한다.

상태 변경함수와 상태를 props로 전달해준다. 유효성 검사 로직을 true, false 값을 구현하는데 헷갈렸다.

Lodash 구현 실패…

페어 프로그래밍을 할 때는 lodash 잘 썼는데, server랑 연동을 하니 왜 lodash 안써지냐 ?

내일 해결하자.

2. 라우터 구현

라우터 구현을 며칠째 쓰는지 모르겠다…

라우터를 제대로 이해해야만 했다…

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
// App.js
const isSigned = async () => {
const { data } = await axios.get("/auth");

return data.success;
};

const routes = [
{ path: "/", component: Home },
{ path: "/rank", component: Rank, guard: isSigned, redirectTo: Signin },
{ path: "/signin", component: Signin },
{ path: "/signup", component: Signup },
{ path: "/matching", component: MatchingCards },
];

class App extends Components {
...
router(path = window.location.pathname) {
// header 에서 클릭이벤트로 href 를 받아서 path에 담는다.
const route = routes.find(route => route.path === path);

// change url path
(async () => {
!route.guard || (await route.guard())
? this.setState({ Page: route.component })
: this.setState({ Page: route.redirectTo });
})();
}
}
  • “pjax 방식으로 라우터를 구현하기 위해서 서버와 도움이 필요하다”라는 말이 이제는 이해가 된다. ajax 방식은 url이 변경되지 않아 seo에 문제가 있어서 이를 해결하기 위해서 pushState를 통해 history API를 구현해준다.

  • 그리고 router 함수는 path를 받아서 해당 path의 컴포넌트를 가지는 route를 찾아서 현재 페이지로 렌더링해준다.

  • 이 때, routes 배열안의 어떤 라우터는 접근 권한이 필요한 라우터가 있는데, 이들한테는 guard라는 프로퍼티가 존재한다. 그러므로 router 함수에서도 guard라는 프로퍼티가 있을 때, 또 guard 프로퍼티의 값이 true, false냐에 따라 컴포넌트를 다르게 보여줘야하는 로직을 짜야한다.

이 개념이 CSR(Client Side Rendering)이다. 서버에서는 다음과 같이 코드를 구현해주었다.

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
// server.js
const auth = (req, res, next) => {
const accessToken = req.headers.authorization || req.cookies.accessToken;

try {
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_KEY);
console.log(`😀 사용자 인증 성공`, decoded);
next();
} catch {
console.error("😱 사용자 인증 실패..");
console.log(accessToken);
return res.redirect("/signin");
}
};

const isSigned = (req, res) => {
const accessToken = req.headers.authorization || req.cookies.accessToken;

try {
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET_KEY);
console.log(`😀 사용자 인증 성공`, decoded);
res.send({ success: true });
} catch {
console.error("😱 사용자 인증 실패..");
res.send({ success: false });
}
};

// auth route
app.get("/signin", (req, res) => {
res.sendFile(path.join(__dirname, "./public/index.html"));
});

app.get("/auth", isSigned);

app.get("*", auth, (req, res) => {
// auth 미들웨어
res.sendFile(path.join(__dirname, "./public/index.html"));
});
  • 서버에서는 모든 경로에서 get 요청이 들어오면 auth 함수의 내부의 로직을 기준으로 사용자가 JWT 토큰이 있는지 유무를 판단하여 결과를 반환한다. 이 결과에 따라 rootContainer를 응답으로 보내주고 클라이언트는 이를 받아서 조건에 맞게미리 구현해둔 router 함수로 화면에 렌더링을 해준다.

  • 그리고 서버에서 또 다른 함수로 JWT 토큰 유무에 따라 성공, 실패 결과를 전달해줘야하므로 auth 와 별개인 isSignin() 함수를 만들어줬다.

우리는 이 부분에서 auth 라는 함수 하나로만 해결하려고 하다 보니 res.send로 오류를 보내면 /rank URL로 직접 요청시 오류 페이지가 보이게 되고 redirect 를 해주자니 오류가 안보내져서 클라이언트에서 router 함수 로직을 구현할 수 없는 딜레마를 겪었다.

그 결과 클라이언트의 isSigned 비동기 함수가 axios로 get 요청을 보내서 data(boolean)를 반환해준 값을 guard 프로퍼티에 등록해주었다.

이를 사용해 router 함수에서 로직을 구현해주었다.

소감

오늘은 월요일이라서 그런지 머리도 잘 안돌아가고 주말에 푹 쉬어서 죄책감이 약간 들어서 자존감도 떨어지는 하루였다.

그래도 늘 이런 생각을 하면 달라지는 것은 없다. 이런 생각을 할 시간에 좀 더 연구하고 공부하는 게 나의 미래에 더 도움이 된다.

이제 미니 프로젝트 마감이 이번주 금요일로 다가왔으니 앞으로 며칠만 더 힘내서 열심히 공부하고 배운것을 꼼꼼하게 정리하도록 해야겠다.

댓글 공유

1. router 함수와 pushState 메서드

router 함수가 route의 guard 프로퍼티를 통해 로그인한 사용자가 아니라면 /rank 경로로 이동시 /signin 경로로 이동하도록 구현을 해주었다.

이 때 발생한 문제가 화면 렌더링은 signin 컴포넌트로 변경되었지만 라우터는 이상하게 / Home 컴포넌트가 그려지는 것이다.

문제가 어디서 발생하는지 찾아보니, 클릭 이벤트 발생할 때, router 함수가 호출되는데 이 때 상태로 관리하는 Page가 Home에서 변경되지 않는 문제였다.

왜 이 경우에서만 변경되지 않는지에 대해서는 아직 파악하지 못했지만, 리팩터링 시간을 가져서 파악해보도록 해야겠다.

2. 서버에 랭크 데이터를 보내고 랭커들을 랭크 페이지에 보여주자

유저점수를 게시판에 보여주기 위해서 게임이 종료되었을 때, /matching 경로로 post 요청을 보내서 record를 서버에 보내준다.

서버에서는 mongodb와 연결되어 있어 database에서 해당 유저의 이전 기록과 비교하여 더 빠른 기록으로 갱신해준다.

또한 rank 페이지를 누르거나 URL로 직접 요청할 때마다 데이터를 가져와서 보여줘야 하므로 router 함수 내부에서 /ranker 경로로 get 요청을 보내서 데이터를 가져와야한다.

isSignin 함수에서 /auth 경로로 get 요청해서 guard 프로퍼티에 불리언 값을 저장한 것 처럼 /rank 경로가 아닌 /ranker 경로로 get 요청을 해야한다.

1
2
3
app.get("*", auth, (req, res) => {
res.sendFile(path.join(__dirname, "public/index.html"));
});
  • 만약 /rank 경로로 get 요청을 보내면, 위 단계와 겹치지 때문에 다른 경로로 설정을 해줘야 한다.

댓글 공유

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명이서 같이 나눠서 공부하고 공유하면 더욱 좋았을텐데 이점은 조금 아쉽다. 건강상의 이유로 참여하지 못한 팀원도 안쓰러웠다. 다음주에 쾌유해서 돌아오면 그동안 공부했던 것을 공유해주면서 배웠던 지식을 다시 정리해봐야겠다.

댓글 공유

1. CBD 기반 라우터 구현

CBD 라이브러리에서 컴포넌트가 바뀌었을 때, 라우터와 렌더링을 구현해주었다. 각 컴포넌트에서 click 이벤트가 발생하면 그 때의 a 태그의 href 값을 구하여 pushState 메서드로 history를 등록해주고 상태로도 할당해주어 렌더링을 발생시켰다.

즉, signin 클릭해서 click 이벤트 발생하면 history등록, setState로 상태 갱신을 해주었다.

그리고 App 컴포넌트에서는 popstate 이벤트를 등록하여 브라우저의 뒤로가기, 다음 버튼을 클릭했을 때, 해당 path 상태를 window.location.pathname으로 변경시켜줘서 라우터가 변경되면 렌더링 발생하는 기능도 구현하였다.

문제점은 구현을 하다가 작은 부분에서 막혀서 그 작은 부분을 해결하려고 몰두해있었던 점이 문제라고 생각한다. 작은 문제는 근본적인 문제가 아닌 경우가 많은 것 같다. 큰 문제를 해결하면 작은 문제는 알아서 해결될 수 있으니 큰 틀에서 생각하려고 해야겠다.

1. click 이벤트 발생 시 -> pushState 메서드로 history 관리 + setState 메서드로 화면 재렌더링 발생

2. history API 사용 시 -> popstate 이벤트 핸들러 등록하여 setState 메서드로 path 상태를 window.location.pathname로 갱신

popState 시 navigate() 함수 안에서 pushState() 해주고 있어서 더 이상 history API가 제대로 동작하지 않으므로.. 이를 확인하자.

위 두가지 로직이 전부이다.

2. MatchingCards CBD 상태 관리

매칭 카드 알고리즘 구현한 것을 CBD 구조에 맞게 상태로 관리해주었다. CBD 구조가 setState로 상태가 변경되서 렌더링 될 때, App 컴포넌트는 한번만 호출되지만, App 컴포넌트에서 다른 컴포넌트를 그려주는 역할을 하는 domStr() 메서드가 반복 호출 되므로, 이 때, 다른 컴포넌트(클래스)가 new 연산자로 호출되므로 컴포넌트가 새롭게 생성되어 지역 컴포넌트의 상태를 관리해주기가 까다로웠다.

이러한 문제를 해결하기 위해서는 CBD 구조를 개편해야만 했다. 하지만 프로젝트 시간동안에는 힘들것 같아서 상태관리를 App에서만 해주기로 하였다.

App 컴포넌트에서 상태를 등록하고 다른 컴포넌트로 보내줄 때, 아래와 같이 전달해준다.

1
2
3
4
5
${new Page({
...this.state,
navigate: this.navigate.bind(this),
checkCard: this.checkCard.bind(this),
}).domStr()}

상태를 받는 컴포넌트에서는 아래와 같이 props를 통해 받는다.

1
2
3
4
5
6
${this.props.shuffledNum
.map(
(num, index) => `
<div class="cards ${
this.props.openCard[0] === index + 1 || this.props.openCard[1] === index + 1 ? 'opened' : ''
}" data-id="${index + 1}">

단, props에서 받을 때, 메서드를 받게 된다면 this가 꼬일 수 있기 때문에 bind(this)를 사용하여 this를 일치시켜서 보내줘야 한다.

만약 함수정의를 보내주는 것이 아닌 함수 호출값을 보내주고 싶다면 call(this, 인자) 이렇게 전달해줄 수 도 있다.

3. form 형식으로 서버에게 데이터 전송에 대해 알게된 점

서버는 express를 사용하여 구현하였다.

로그인 기능 구현을 위해 form 형식의 input 값을 서버에 axios를 사용하여 보내주었다.

기본적으로 form 형식은 x-www-form-urlencoded 이런 형식의 데이터로 전달된다. 서버는 이런 데이터를 해석하기 위해 파싱을 express.urlencoded({extended:true}) 메서드를 사용한다.

하지만 axios를 사용하면 클라이언트에서 데이터 요청을 json 형식으로 서버에 요청하기 때문에 서로 다른 형식으로 주고 받기 때문에 제대로 된 요청과 응답이 먹히지 않게 되었다.

이러한 문제를 해결하기 위해서 서버에서 express.json()메서드를 사용하여 json 형식의 데이터를 받아 파싱해주도록 하였다.

또한, 서버에서 request를 확인해보면 'content-type': 'application/json'인 것을 확인할 수 있었다.

심지어 axios 홈페이지에서도 json으로 직렬화된 데이터로 요청한다고 나와있다!

이런 부분은 눈으로 읽기는 읽었지만 그 배경에 대해 제대로 이해하지 못한 체로 읽었기 때문이다. 이렇게 하나 하나 배워가다 보면 배경지식도 넓게 쌓여서 새로운 지식을 배울 때에도 두려움이 적어질 것 같다.

소감

이전에 라우터를 구현했던 방법은 상태를 관리하지 않고 했었기 때문에 쉬웠고 상태를 관리하면서 라우터를 구현하는 것은 머리가 복잡해지고 의욕이 떨어졌다.. 그래도 동기들에게 물어보면서 이번 기회에 제대로 알게 된 것 같아 기쁘다.

로그인 구현과 서버에 대해 스스로가 두려움을 느끼고 있는 것 같아 더 머리가 안돌아가는 것 같다.

최대한 이번기회에 로그인 회원가입에 대한 로직과 친숙해져서 다음 프로젝트 때에는 막힘없이 도전해보고 싶다!

댓글 공유

1. 라우터 구현

우선 컴포넌트마다 이벤트를 등록시켜주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
this.createEvent({
type: 'click',
selector: '.header-logo',
handler: e => {
e.preventDefault();

const path = e.target.getAttribute('href');

if (window.location.pathname === path) return;

window.history.pushState(null, null, path);

window.dispatchEvent(
new CustomEvent('home', {
detail: path,
})
);
},
}),
  • 라우터를 구현해주기 위해서 a태그의 href 속성값을 가져와서 path에 할당하고 window.history에 pushState 메서드를 사용하여 history 등록해주었다. history로 등록을 해줘야 브라우저 다음, 이전 버튼이 제대로 동작한다.

  • 위 방식은 pjax 방식으로 클릭 이벤트를 캐치하고 preventDefault로 서버로 요청을 방지한다. 이후 href 속성의 path를 사용하여 ajax 요청을 하는 방식이다.

  • ajax 요청은 브라우저의 url을 변경하지 않아 history 관리가 불가하기 때문에 이 때, pushState 메서드를 사용하여 URL을 변경하고 URL을 history entry로 추가하지만 서버로 HTTP 요청을 보내지는 않는다.

예제를 보면서 pjax 방식을 따라서 구현해봤는데, 우리 팀의 CBD Library 방식에는 맞지 않게 코드를 구현하였다.

문제1. 상태 관리 미흡

이벤트가 발생하여 컴포넌트가 대체되어 화면의 렌더링이 발생하게 되는데, 이를 상태로 관리하지 않고 custom 이벤트를 생성하여 dispatch하여 App 컴포넌트에서 custom 이벤트 리스너를 등록하고 또 이걸 보여주기 위해서 render 함수 있는 곳에서 custom 이벤트 리스너를 등록하는 복잡한 구조로 하였다. 그 결과 각 커스텀 이벤트 마다 render를 호출해주고 있으므로 CBD Library를 사용한 의미가 사라졌다.

  • 이를 해결하기 위해서 현재 내가 있는 페이지를 상태로 관리하는 것으로 코드를 개선하였다.

2. 서버 오픈 시 고려해야할 점

  • 서버 연결 시 server.js에 연결을 하는데 이 때는 node.js 환경이므로, window 객체가 존재 하지 않는다.

  • commonJs 방식으로 서버를 구현했다면 package.json 파일에 type=”module”을 추가해줘야하지만 require 방식으로 구현했다면, type=”module”을 빼줘야 한다.

소감

오늘 혼자 힘으로 라우터에 대해 공부하여 프로젝트에 적용해보며 라우터가 어떻게 동작하는지 알게되었다. 혼자 힘으로 해보니깐 CBD Library 가 어떤 구조로 작동하고 이 구조를 맞춰서 내가 라우터를 어떻게 구현하면 되는지 생각할 수 있어서 인상깊었다.

채련님과 로그인, 회원가입 구현을 위해 서버에 대해서 공부도 하였는데, 내일 좀 더 깊게 공부해봐야겠다. 아직 우리 구조에서 서버와 어떻게 통신을 해야하는지 제대로 이해가 되지 않는다.

지윤님이 만드신 카드 게임 알고리즘에서 카드 2장이 뒤집히기 전에 새로운 카드를 눌렀을 때, 3장의 카드가 뒤집히는 문제점을 같이 해결하였다.

내일은 채련님과 로그인, 회원가입 구현과 서버와의 통신을 같이 구현해보기로 하였다.

오늘도 새로운 것을 많이 배워서 뿌듯한 하루였다. ㅎㅎ

댓글 공유

오늘 배운 것

1. git organization 과 git flow 전략

git organization을 만들어서 팀원들과 git flow 전략을 사용하기로 결정하였다. git flow 전략에 대해서 한번 수업을 듣고 배운 적이 있었는데 그 때 기록해둔 notion 자료가 유용하였다.

git organization을 만들고 팀원들을 초대하고 이 organization 안에서 repository를 생성하고 clone을 받아서 git flow init을 해주면 main, develop 브랜치가 생성된다.

  • main 브랜치는 배포를 위한 브랜치이다.

  • develop 브랜치는 개발을 위한 브랜치이다. feature를 생성하여 commit 단위 별로 feature 브랜치를 생성하고 삭제하며 작업을 이행하였다.

각 feature에서 commit을 하고 develop 브랜치에 push를 하여 팀원들의 코드를 develop 브랜치에서 합쳐서 헙업을 진행할 수 있었다.

2. 노션 칸반 보드

지난 페어프로그래밍때 만든 CBD Library를 사용하여 컴포넌트 단위로 HTML, CSS를 완성하고 나니 갑자기 다음에 무엇을 해야할지 바로 생각이 나질 않았다.

이런 문제점을 해결하기 위해서는 칸반 보드를 작성하여 나와 내 팀원들이 지금 무슨 작업을 하고 있고 현재 작업이 완료되면 어떤 작업을 하게 될지 notion에 정리해두었다.

매일 저녁 9시에는 그날 작업을 하면서 배우거나 느꼈던 점을 서로 공유하는 Daily scrum 시간을 갖고 내일 할일을 정리하는 시간도 가지니 막힘없이 진행할 수 있을 것 같다.

3. 기획서 피드백

어제 하루종일 기획서를 작성하기 위해 회의도 오래 하고 figma 시안에 많은 시간을 쏟아부었다. 하지만 오늘 기획서 발표에 대한 피드백은 참담했다…

디자인은 어떻게 하고, 무슨 기능을 구현하고, 어떻게 협업을 진행할지 정하는 것 까진 좋았다. 하지만 제일 중요한 부분이 빠졌다.

바로 이번 프로젝트에서 구현해야할 로그인, 회원가입 기능 구현 방법과 라우터 기능 구현 방법에 대해서 자세하게 정해보지 않았다.

말그대로 빛좋은 개살구가 되는 기획서가 되버린 것이다. 내가 만약 이런 상태로 회사에 입사하여 기획자라 이거이거 해주실 수 있나요? 라는 질문을 받게 된다면, 아마 기능 구현에 대한 방법을 제대로 생각해보지 않고 겉으로 보기에 쉬워보이니 된다고 했을 것 같다.

소감

이번 프로젝트는 포트폴리오일까? 이력서에 제출할 포트폴리오는 매우 신중해야한다. 면접관은 지원자의 포트폴리오를 보고 100%를 다 직접 구현하진 못했더라도 어느정도 자신이 구현하지 않은 부분에서도 이해하고 설명할 수 있을 것이라 생각한다.

이번 프로젝트도 오늘 기획서 발표대로 하게된다면 프로젝트 구현에만 급급해서 프로젝트가 끝나도 아무것도 남는게 없을 것이다…

그러므로 이번 프로젝트의 목표는 팀원들과 실전 프로젝트를 기획부터 개발, 배포까지 경험해보면서 어떤 부분이 어려웠는지를 직접 경험하고 여기에서 배움이 있어야 한다.

남에게 보여질 빛좋은 개살구 같은 프로젝트는 필요없다. 내가 진득하게 이번 프로젝트를 위해 어떤 부분을 배웠고 어떤 부분을 경험하면서 어려워했는지를 하나하나 기록하고 팀원들과 그날 배운 것을 공유하면서 공부하는 것에 초점을 맞춰야겠다.

댓글 공유

HolyMoly 프로젝트

오늘부터 약 2주간 자바스크립트 프로젝트가 시작된다.

그동안 페어프로그래밍을 통해 배운 내용을 실전 프로젝트에 적용해보고, 게임까지 직접 만들어보면서 재미까지 더불어 챙길 수 있는 프로젝트이다.

주요 기능은 로그인과 회원가입 기능을 구현해야하며, 라우터 기능을 구현해야한다.

우리팀은 3명의 팀원으로 구성되어있고 각자 1개의 미니게임을 구현하고 싶어한다.

메인페이지

메인 페이지에는 캐러셀 기능으로 게임 설명을 보여줄 것이다. 캐러셀 기능은 페어프로그래밍 1 때 기능 구현을 경험해본 바 있다.

그 때, transitionend가 애먹었던 기억이 아직도 잊혀지지 않는다.

로그인 및 회원가입 페이지

주요 기능인 로그인 및 회원가입 페이지 구현은 페어프로그래밍 1에서 구현해보았다. 하지만 시간이 부족하여 제대로 리팩터링을 경험하지 못한체로 끝냈었다.

이번 기회에 그 때 하지 못한 리팩터링까지 같이 해볼 수 있는 기회가 생긴 것 같다.

미니게임 구현

내가 구현할 미니게임의 제목은 “Close to Zero” 게임이다.

말 그대로 0에 가장 근접할 때 버튼을 클릭해서 그 숫자가 가장 0에 근접한 사람이 승리하는 게임이다.

게임 구현은 간단할 것 같다. 하지만 이번 프로젝트는 지난 페어프로그래밍 2편 9번 문제에서 CBD Library를 제작한 것을 활용하여 구조를 가져갈 것이기 때문에 컴포넌트에 대한 이해가 필요하다.

라우터 기능 구현

이번 프로젝트는 앞서 말했듯이 컴포넌트 단위로 HTML 동적으로 생성하는 SPA를 구현할 것이다. SPA는 화면이 전환되어도 URL이 변경되지 않아 SEO에 취약하다는 문제점이 있다.

또한, 이번 프로젝트의 최소 요구사항인 라우터 기능을 구현하기 위해서는 URL이 필요한데, 과연 URL이 없는 SPA에서 어떻게 라우터를 구현할 수 있을까?

그 해답은 컴포넌트와 라우터를 1:1 매핑관계로 두어 URL 뒤의 Path가 변경되면 컴포넌트를 바꿔주면 된다.

이에 대한 자세한 방법은 다음 시간에 알아보자.

소감

팀원들과 같이 줌으로 회의를 하면서 아이디어도 잘나오고 오늘 회의 열심히 달려서 figma 시안도 완성하여서 뿌듯한 공휴일을 보낸 것 같아 기분이 좋다.

내일부터 다시 또 피터지는 프로젝트 시간이 시작되니 컨디션 조절 잘하고 매일 회고도 잊지 않고 지치지 않고 롱런하고 싶다.

댓글 공유

  • page 1 of 1

loco9939

author.bio


author.job