git fork 제대로 파헤치기

카테고리 git

📌 git fork

fork

git fork는 위 사진 처럼 강사님의 원격 저장소의 레포지토리를 나의 원격 저장소로 복제해오는 것이다. 복제해오는 것이지 연결되는 것은 아니다.

사용하는 이유

  1. 프로젝트를 할 때, 팀장 원격 저장소를 두고 팀원들이 fork하여 각자의 원격 저장소에서 작업을 하여 분업화가 가능하다.

  2. 팀장의 원격 저장소는 배포와 밀접한 관련이 있으므로 직접적으로 팀장 원격 저장소로 코드를 push 하지 못하고 Pull Request를 통해 연결할 수 있으므로 안정적인 개발이 가능하다.

사용 방법 및 주의사항

  1. 팀장의 원격 저장소를 나의 원격 저장소로 fork 해온다.

이 때, main 브랜치만 복사해오는 옵션이 있는데, main만 가져오고 싶다면 체크하고 아니면 해제한다.

  1. fork한 나의 원격 저장소를 나의 로컬로 clone 해온다.

  2. 원격 저장소 목록에 팀장의 원격 저장소를 ‘Leader’라는 이름으로 저장하여 추가한다.

git remote add Leader <팀장의 원격 저장소 URL>

URL이지 git clone 링크와는 다르다.

  1. 만약 팀장이 코드를 변경하고 push하여 팀장의 원격 저장소가 바뀌고 이를 나의 로컬로 가져오고 싶다면 ?

git fetch Leader

이 때, 팀장의 모든 브랜치가 아닌 특정 브랜치만 가져오고 싶다면

git fetch Leader <branch 명>

  1. 가져온 코드를 내 로컬과 병합해줘야 한다면 ?

git pull Leader <branch 명>

처음 팀장 원격 저장소 fork 해와서 나의 로컬의 main 브랜치는 팀장의 원격 저장소의 main 브랜치와 연결이 되어있다. 정확히는 track 추적을 할 수 있다.

하지만, 팀장의 원격 저장소에서 새로운 브랜치를 생성하였고 나의 로컬에서도 해당 브랜치를 가져오고 싶다면 추가적으로 track 명령어를 실행해줘야한다.

  1. 팀장이 브랜치 새로 파고 내 로컬에서 가져오고 싶다면?

git fetch Leader <branch 명>

우선 fetch로 해당 branch를 가져온다.

git checkout -t Leader <branch 명>

내 로컬에서 해당 브랜치로 checkout 하여 브랜치를 옮길 때, -t or track 명령어로 추적한다.

git pull Leader <branch 명>

이제 pull을 받아오면 코드를 가져올 수 있다.

만약 추적을 안하고 가져오게 된다면 변경사항이 없는 상태인데도, pull 해올 때 마다 Merge commit을 하라는 문구가 뜨게될 것 이다…

🏓 소감

수업 중에 강사님 원격 저장소와 연결했는데 문제가 생겼다. 처음 fork 할 때는 강사님 원격 저장소에는 main branch 밖에 없어서 내가 fork 해왔을 때, 연결된 branch는 main branch 뿐이었던 것을 몰랐다.

수업 중 강사님께서 브랜치를 새로 생성하셨고 나도 똑같이 fetch, checkout, pull을 하였는데, 다음과 같은 오류가 발생했다.

“관계 없는 커밋 내역의 병합을 거부합니다”

이러한 오류가 발생한 이유는 새로 생성한 branch를 fetch로 가져오기만 하고 강사님의 원격 저장소의 branch를 추적하지 않기 때문에 발생한 문제였다.

git branch -vv

위 명령어를 사용하면 현재 branch가 어떤 branch를 추적하고 있는지 알 수 있다. 그래서 이미 생성된 branch를 git branch -D <branch 명> 명령어로 삭제한 뒤 위의 방법대로 새롭게 생성한 뒤 추적을 해주었더니 문제가 해결되었다.

앞으로 git 사용하다가 오류가 나오면 무시하지 말고 어떤 오류인지 제대로 파악하고 해결하도록 해야겠다.

댓글 공유

프로젝트를 마치며…

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

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

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

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

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

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

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

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

댓글 공유

1. Software Development Life Cycle (SDLC)

소프트웨어 개발의 전체적인 라이프 사이클을 이해하여 개발 과정에서 발생하는 일을 정략적으로 관리하자.

해야할 일

  1. 요구사항 분석

  2. 설계

  3. 구현

  4. 테스트

  5. 유지보수

위 과정을 진행하기 위한 대표적인 모델은 다음과 같다.

1.1. Waterfall

SDLC 일련의 단계를 모든 팀원들이 참여한다. 심각한 문제라면 다시 이전단계로 돌아가야하겠지만, 작은 문제라면 일단 다음 단계로 넘어간다.(작은 문제를 지금 고친다 하더라도 사이드 이펙트 발생까지 생각하기 어렵다.)

장점

  • 팀을 관리하기가 편하여 대규모 팀에 적합

  • 어떤 단계가 마무리되면 결과가 뚜렷함

1.2. Agile

작은 기능들의 성공을 반복시켜서 프로젝트 완성, 너무 많은 플랜을 지양하고 요구사항에 맞는 코드를 작성한다.

커뮤니케이션 중요(daily scrum, 코드리뷰, 회고)

Daily scrum

매일 15분간 회의를 진행하여 본인이 진행할 기능, 수정사항에 대해 우선순위를 부여하고 순서대로 진행한다.

  • Product Backlog: 제품 전체의 요구사항
  • Sprint Backlog : 프로덕트 기능 하나하나에 대해서 적어 놓음

Sprint

Sprint는 2주 단위로 진행을 한다. 첫날에 planning meeting을 진행하여 2주간 일정을 세세하게 세운다.

ex) 10월 25일 HTML설계, 10월 26일 컴포넌트 구조 설계 …

회고

2주간의 Sprint가 끝나면 회고를 진행한다.

  • 3L 전략(Liked, Learned, Lacked)

Sprint 전 준비사항

  1. UseCase 작성하기

UseCase는 flowchart와 다르니 flowchart화 되고 있다면 재고해봐야한다.

UseCase는 사용자가 어떤 행동을 할 수 있는지를 나열하고 이들간의 관계를 연결한 구조로 작성한다.

flowchart가 아니므로 순서를 따를 필요가 없다.

소감

점점 막히는 부분도 많아지고 이전에 제대로 구조를 설계하지 않고 회피하고 구현에만 집중하다보니 리팩터링을 어떻게 시작해야할지 막막하다…

프로젝트 막바지에 다가오니 의욕도 떨어지는 것 같다. 재충전이 필요하다.

그래도 이틀남았으니 내일 하루는 리팩터링에 시간을 쏟고 목요일은 배포와 회고, 발표준비를 하면 프로젝트를 잘 마무리 할 수 있을 것 같다.

댓글 공유

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%를 다 직접 구현하진 못했더라도 어느정도 자신이 구현하지 않은 부분에서도 이해하고 설명할 수 있을 것이라 생각한다.

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

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

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

댓글 공유

loco9939

author.bio


author.job