28th SOPT APPJAM 앱잼 - 두리번(DOORIBON) 회고 (3)
2021.07.20 - [프로그래밍/잡다한 개발 일지] - 28th SOPT APPJAM 앱잼 - 두리번(DOORIBON) 회고 (1)
2021.07.22 - [프로그래밍/잡다한 개발 일지] - 28th SOPT APPJAM 앱잼 - 두리번(DOORIBON) 회고 (2)
🛠 개발 flow
어느정도 완성된 API 명세서 초안을 가지고 실제 API 구현에 들어갔다. 이때 뷰를 보며 최종적으로 적은 명세서가 약 30개정도 였다. 사실 이때 모두 작성된 건 아니었고, 성향테스트 부분이 미완성이었다. 성향 테스트 명세서까지 작성하고 구현하면 좋았지만 서버에서 정해둔 기한을 이미 2일정도 오버한 상태였고, 일단 개발에 들어가야 빠르게 배포할 수 있겠단 생각이 들었다.
소스 코드 관리는 모두 github에서 진행했다. 우리의 개발 flow는 다음과 같다.
1. issue open
그날 그날 혹은 앞으로 개발해야 할 API 들을 Issue 로 열어놨다. 해당 API를 개발할 사람이 issue에 본인의 Label을 달고 개발했다.
누가 현재 어떤 API 를 짜고 있는지 바로 확인이 가능해서 좋았다.
또한 PR을 날릴 때 관련 issue 번호를 추가할 수 있어서 유용하게 사용했다.
2. 각자 개발
개발 시 모르거나 막히는 부분은 바로바로 질문하고 서로 도와주었다. 구(글)선생님이 많이 도와주셨다.
3. PR
개발 후 Postman 테스트까지 완료한 API 들은 Pull Request를 날렸다. PR 시 모든 팀원들이 확인 후 merge 하도록 규칙을 정했다.
확인 시 사용한 우리끼리의 단어도 있었는데 iOS 팀의 "LGTM" (Looks Good To Me ?)를 보고 따라했다.
서버는 "OHRJA", "DNGJA" = 오히려 좋아, 대놓고 좋아
별 의미는 없지만 나름대로 재미있게 코멘트를 남길 수 있었다.
우리 셋 다 github code review 를 잘 안해봐서 (?) 저렇게 코드에 바로 comment 를 남길 수 있다는 점을 중반부에 알아챘다 ^^..
그 전까지 다 comment 에 바로 ~~.ts 몇번째에 뭐 지워주세요 이랬던..
내가 좀 더 github 사용법을 익히고 시작했어야 하는 아쉬움이 있었다.
무튼 셋 다 PR 날리면 꼼꼼하게 봐줘서 무리없이 merge에 들어갈 수 있었다. 여기서 느낀건 나는 정말 꼼꼼하지 못하단 점.. 나도 다른 친구들의 code를 꼼꼼히 확인했는데 주현이나 설희가 내가 못본 부분을 바로바로 알아채서 comment를 남겨줬다. 내가 기능적인 부분을 우선시하게 점검해서 그런 것 같기도 하다. 자잘한 code style 문제들도 잘 확인해야겠단 생각이 들었다.
예를 들자면 error -> err 약어로 쓴 점 이런건 내가 잘 알아채지 못했다.
4. Merge, issue close
모두가 PR을 확인하고 수정까지 완료되면 develop branch로 merge 했다. merge 후에는 꼭 슬랙에 pull 받아달라는 message를 남겨 추후 발생할 conflict 를 예방했ㄷ.. 하지만 conflict는 언제나 발생한다!
🔫 나를 시험에 들게한 것들
1. 성향 테스트
내가 성향테스트를 개발한 경험이 있었지만 해당 경험은 web 개발이었고, front 만 사용한 정적 웹이었다. 두리번에서 원하는 성향 테스트 기능은 모두가 성향 테스트 결과를 공유하고, 누가 어떤 답변을 했는지까지 보여주는 것이었다. 이 부분에서 서버를 어떻게 활용해야 할 지 고민이 많았다. 고민했던 부분의 뷰는 1차 뷰로
이런 느낌이었다. 즉, 어떤 멤버가 어떤 답변을 했는지, 어떤 성향인지 모두 db에서 저장해야 했다.
이 부분에서 모든 유저가 자신이 답한 성향테스트 질문-답변을 저장한다면 db size에서 너무 비효율적일거란 생각이 들었다.
특히나 1번과 5번이 함께 있어요 이 부분 같은 경우는 그냥 서버에서 던져주기만 하면 되는 것도 아니고, 서버단에서 filtering 하여 줘야하는 부분도 들어있어 어떻게 해야 효율적일지 고민이었다.
이 때 클라 분들께서 성향테스트 관련 전체 회의를 요청하셨고, 내가 고민하는 부분들을 전체 팀원들에게 말할 기회가 생겼다.
여기서 또 하나를 배웠는데 나는 웹으로 성향테스트를 개발할 때, 웹 자체에서 json module 로 질문, 답변, score를 가지고 바로 처리하여 당연히 두리번에서도 질문이나 답변, score 처리를 클라단에서 해결 할거라고 생각했다.
하지만 이 부분에서 iOS 갓혜진언니가 iOS, Android 두 가지로 개발하는 상태에서 그렇게 클라에서 따로 저장하는건 비효율적이라고 알려주셨다. 따라서 고민끝에 질문, 답변을 서버에서 던져주고, score 계산 같은 경우에는 클라에서 가중치 점수를 더한 배열만 주면 서버에서 max score를 처리하고, 성향 결과를 던져주겠다는 결론을 냈다.
이어서 클라분들도 성향테스트 관련 생각을 말씀해주셔서 앱잼 기간 내에 끝낼 수 있을 정도로 성향테스트 기능과 질문, 결과 개수를 축소하는 방향으로 마무리 지을 수 있었다.
축소된 뷰에서는 누가 어떤 답변을 선택했는지를 지우고, 해당 답변에 몇명이 답변했는지만 저장했다. 이 부분이 좋았던게 초기에 서버끼리 회의할 때, 사실 답변마다 누가 답했는지 저장하는 것 보다는 사람 수만 counting 하여 저장하게끔 하는게 우리 입장에선 최선이다! 라고 말했었는데 딱 이런 방향으로 기능이 축소되어 서버 입장에서는 개발하기가 훨씬 수월했다. 사실 기획분들께서 고심하여 생각해낸 기능을 개발자들이 구현하기 어렵단 이유로 축소하는게 나도 마음 아프긴 했는데 기디 분들이 개발자분들이 할 수 있을만큼 하게 타협해주셔서 너무 감사했다.
하지만.. 이제와서 말하지만 사실 counting 하는 부분도 은근 구현이 까다로웠다.
초기에 성향 DB model 내 카운팅 부분을
const TendenciesSchema = new mongoose.Schema({
count: [[{
type: Number
}]],
tendency: [TendencySchema]
});
이런식으로 구현해놔서 각 질문 마다 답변을 2중 배열로 저장하게했다.
즉, count[1][2] = 2번 질문에 3번 답변을 선택한 사람의 수 (0-index 라 +1)
{
"title": "내가 원하는 여행 스타일은?",
"content": {
"answer": [
"최대한 많은 관광지를 둘러보고 싶어",
"쉬엄쉬엄 여유롭게 구경 다니고 싶어",
"구경보다는 편안한 곳에서 느긋하게 힐링하고 싶어",
"같이 가는 사람들이 하자는 대로 다닐래"
],
"count": [
9,
0,
0,
0
]
}
},
이런식으로 response-body를 구현했는데, 클라분들께서 해당 형식을 질문 -> [답변, 카운트] 형식으로 바꿔달라고 요청하셔서 만들어둔 counting 로직을 어떻게 수정해야할 지 까다로웠다.
결과적으로 이런 식으로 수정해드렸다. 이 과정에서 주현이와 고민을 하다가 model 자체를 response body 형태로 수정하기 보다는 controller에서 전달할 때 형식을 수정하는 방향으로 가자고 결론을 내렸다.
사실 우리 입장에서는 초기 형식으로 드려도 클라에서 index로 꺼내 쓸 수 있을거라 생각했는데 저런식의 json 접근이 어렵다는 사실을 몰라서 죄송하기도 했다. 처음에 짤 때 명세서로 클라 분들과 조금 더 소통을 했음 할 걸 그랬다란 생각도 들었다. 물론 서로 바빴으니까! 푸하하
let outIndex = -1; // 외부 카운트 인덱스
let index = 0; // 내부 카운트 인덱스
questions.map((question) => {
let contentArray = [];
outIndex++;
question.content.map((item) => {
let element = {
answer: item.answer,
count: count[outIndex][index++]
}
contentArray.push(element);
});
const resultData = {
title: question.title,
content: contentArray
}
data.push(resultData);
index = 0;
이런식으로 DB 는 그대로 두고 resultData를 새로 만들어서 하나의 2차원 배열로 묶여있던 count array를 분리하여 매핑해주었다.
이 부분 짜다가 주현이랑 알고리즘 문제 푸는 기분이라고 농담도 했다 ㅋㅋ
2. 복잡한 Query!
2편에서도 말했다싶이 우리 DB는 굉장히 복잡하다. depth 자체가 깊다. 그룹 별 user에게만 보여지는 정보들이 대부분이라 db 내에 array 형식으로 저장된 정보가 많아서 service 로직에서 쿼리할 때 큰 고통을 얻었다.
const findBoard = async (boardsId: mongoose.Types.ObjectId, tagData: String) => {
try {
const boardList = await Board.aggregate([
{ $unwind: '$post' },
{ $lookup: { from: 'users', localField: 'post.writer', foreignField: '_id', as: 'writerName' } },
{ $match: { _id: boardsId, "$expr": { "$eq": ["$post.tag", tagData] } } },
{
$project: {
"_id": "$post._id",
"name": { $arrayElemAt: ["$writerName.name", 0] },
"content": "$post.content"
}
}
]);
return boardList;
} catch (error) {
console.log(error);
throw error;
}
};
보드 내에는 태그별로 글이 나눠져있어서 tag로 쿼리를 해야했다.
하지만 저장된 형태가
const BoardPostSchema = new mongoose.Schema({
writer: {
type: mongoose.SchemaTypes.ObjectId,
ref: "User",
},
tag: {
type: String,
required: true
},
content: {
type: String,
required: true
}
});
const BoardSchema = new mongoose.Schema({
post: [BoardPostSchema]
});
이런식으로 배열이어서 배열 내부에서 tag로 쿼리를 해야하는 상황이었다.
구글링은 mongoose array query, mongoose query in array ,, 등 다채로운 검색 끝에 aggregation을 이용해 차례대로 pipeline을 짜서 해결할 수 있다는 정보를 얻었다.
일단 unwind를 이용해 배열 내부를 하나의 스키마 형태로 분리하는 작업이 필요했고, 이후에 tag를 쿼리하여 전달하면 된다.
그 과정에서 writer 필드는 UserDB 와 ref 관계이므로 lookup을 통해 가져와야했다. 모든 pipeline이 완료되면 project를 이용해 하나로 묶어서 보내주었다.
이후에 멘토님 피드백을 받으며 멘토님 말씀대로 각각의 DB에 group Id만 ref 관계로 두고 쿼리했다면 더 쉽지 않았을까 하는 아쉬움도 들었다. 아무튼 이렇게 배열 쿼리하는 부분이 굉장히 까다로웠지만 3명이 모이니까 해결은 되더라는.
3. 카카오 소셜 로그인
로그인에서 우리는 릴리즈를 대비하여 애플 로그인+카카오 로그인 으로 진행하기로 했다. 앱스토어 심사 시 타사 소셜 로그인을 사용하려면 애플 로그인이 필요해서이다. 실제 앱잼에서는 카카오 로그인까지만 구현했는데, 내가 맡아서 담당한 부분이라 열심히 공부했다. 소셜 로그인 구현은 처음이어서 0에서부터 카카오 developers 문서를 읽으며 플로우를 익혔다.
아예 레포를 따로 파서 카카오 로그인 부분만 구현하는 식으로 연습했다. token에 대한 이해가 잘 되지 않아서 반복해서 문서를 읽기도 했다. 생각보다 카카오 문서가 (한글로) 잘 정리되어있어서 타 블로그를 보지 않고 그냥 카카오에서 제공하는 문서만 보고 바로 구현할 수 있었다. 또 아요 리드 오빠가 본인이 예전에 정리해둔 서버-클라 소셜 로그인 flow 문서를 공유해줘서 아주 유용하게 썼다!
https://github.com/jokj624/Kakao-login
레포를 파서 직접 클라단 까지 웹으로 구현해보면서 카카오 로그인 flow를 익힐 수 있었다.
이후에 직접 두리번 서버에 api 를 짜면서 기존에 연습하지 않았던 refresh 토큰부분까지 넣을 수 있었다.
const user = await axios({
method: 'GET',
url: 'https://kapi.kakao.com/v2/user/me',
headers: {
Authorization: `Bearer ${access_token}`
}
}); // user 정보 받아오기
const checkUser = await userService.findUserByEmail({ email: user.data.kakao_account.email });
const data = {
name: user.data.kakao_account.profile.nickname,
email: user.data.kakao_account.email,
profileImage: user.data.kakao_account.profile.profile_image_url
}
if (!checkUser) {
//DB에 없는 유저는 새로 생성한 후 토큰 발급한다.
const newUser = await userService.createUser(data);
const jwtToken = getToken(newUser._id);
return res.status(sc.OK).json({
status: sc.OK,
success: true,
message: "유저 생성 성공",
data: {
user: newUser,
token: jwtToken,
access_token: access_token,
refresh_token: refresh_token
}
});
}
//DB에 이미 존재하는 유저는 토큰 발급 후 전달한다.
const jwtToken = getToken(checkUser._id);
return res.status(sc.OK).json({
status: sc.OK,
success: true,
message: "유저 로그인 성공",
data: {
user: checkUser,
token: jwtToken,
access_token: access_token,
refresh_token: refresh_token
}
});
} catch (error) {
console.log(error);
res.status(sc.INTERNAL_SERVER_ERROR).json({
status: sc.INTERNAL_SERVER_ERROR,
success: false,
message: "서버 내부 오류"
});
}
};
사실 아직까지 내가 구현한 api 가 완벽한지에 대한 확신은 없지만 다행히 로그인, 회원가입에서 큰 문제는 없었다.
아쉽게도 시간 상 앱잼에서 카카오 로그인 서버-클라 연동은 못했지만 처음 해보는 소셜 로그인을 구현하는데 성공해서 나름대로 기뻤다.
💻 성향테스트 web 개발
3주차에는 서버에서 거의 개발할 일이 없어서 의외로 한가했다(?) 그러다 우스겟소리로 나온 내가 성향테스트 web 만들어줄게~ 가 현실이 되었다. 전에 성향테스트 웹을 개발한 경험을 살려 Dooribon 성향테스트 preview web을 개발했다.
개발에서 배포까지 생각보다 별로 안걸렸다. 데모데이 전날이었지만 그날 밤에 배포까지 완료해 솝트 사람들에게 미리 해볼 수 있는 링크를 줄 수 있었다.
https://d2tnok5h4fx13d.cloudfront.net/
개발은 react + javascript로 진행했다.
https://github.com/TeamDooRiBon/DooRi-Web
이미 완성 되어있는 피그마 뷰를 활용해 거의 비슷하게 구성해보았다.
생각보다 사람들이 테스트도 해주시고, 결과도 공유해주셔서 감사했다.
❤️ 가끔은 refresh 해도 좋잖아?
서버에게 refresh란 방탈출 아닐까? 내가 방탈출하자고 졸라대서 착한 서버 친구들이 방탈출을 또! 해줬다.
서버 배열 쿼리하는 방법 찾을 때보다 좀 더 열심히(PM 눈 감아) 방탈출을 찾았다. 화생설화를 데려가고 싶었는데 예약이 치열해서 원래 해보고 싶었던 + 평이 무난했던 싸인 이스케이프 - 하이팜에 데려갔다. 이날 하이팜 하고 난 싸인 이스케이프 모든 테마를 졸업했다!
설희-주현의 빛나는 두뇌로 하이팜을 통과하고 맛있는 라멘도 먹었다.
이 날 먹고 내가 데려간 카페에서 웃프게도 와이파이가 안돼서 셋 다 핫스팟 키고 일했다는...
오랜만에 서버끼리 오순도순 리프레쉬 타임을 가져서 절거웠다.
하지만 절거움은.. 여기서 끝나지 않는데?
서버와의 리프레쉬 이후 아요끼리 합숙하는 숙소에 놀러갔다.
원래 앱잼은 모든 팀원이 합숙하며 진행하는 묘미였지만 현재 코로나 때문에 모든 팀원이 합숙하지는 못했다. 우리 팀은 PM, TI 언니들이 용산구청에 직접 전화하여 컨택한 후 파트별로 나눠서 합숙을 진행할 수 있었다. 아요는 먼저 진행하고 있었다.
이 날 절겁게 작업하다가 갑자기 탄생한 게임이 바로
두리또 : 작은 움직임이 만드는 우리다운 게임
두리또다. 얼마나 절거웠냐면 두리번 말고 두리또 게임 어플을 만들어서 릴리즈 하자는 말도 나왔다.
두리또의 규칙은 다음과 같다.
1. 모든 사람은 마니또를 뽑는다. (이때 마니또는 자신이 될 수도 있다.)
2. 자신의 차례에는 3가지 행동이 가능하다. (1. 질문, 2. 찬스, 3. 정답 맞추기)
2-1. 돌아가면서 한명씩 다른 1명에게 질문을 한다. (ex. 그 사람과 단둘이 밥 먹은적 있어?)
이때, 똑같은 질문은 금지하고, 예/아니오로 대답할 수 있는 질문만 가능하다.
3-1. 찬스 뽑기 후 다음 차례에 사용한다. (ex. 마니또 이름 성 물어보기, 질문 거부, 거짓말 1회, 마니또 교체 등)
3-2. 모든 참여자들의 마니또를 맞춘다.
3. 모든 참여자들의 마니또 관계를 정확히 맞춘 사람이 승리
우리는 작업 중 갑자기 이 게임을 시작하더니 몇시간을 절겁게 두리또게임을 하며 보냈다. 두리또는 인물 관계가 중요한 게임이라 노트 필기를 하며 진행했는데 이때, 팀원들은 거의 고3 수능 필기보다 열심히 노트속에 인물 관계를 정립해나갔다.
후에는 두리또 ver1, ver2, ver3까지 확장해 나가며 각종 탈락자를 고를 때 (ex. 설거지, 먹고 음식 치우기, 청소 등) 두리또를 애용하게 되었다.
개발하며 각 파트끼리(혹시 몇명만 이런건 아니죠?) 이런 게임과 refresh를 곁들이다 보니 개발하며 힘들었던 기억도 싹 사라지고 좋았다. 더욱이 아요 파트 사람들과 친해질 수 있어서 좋았다. 물론 이제 아요숙소 였지만 나도 숙박비를 냈다는...
사실 이런 게임이나 방탈출 등을 할 때 다른 팀원들이 일 안하고 뭐하냐는 눈치(?)를 줄 수도 있었지만 항상 이런 나의 일탈에 같이 동참해주는 팀원들이 너무 고마웠다 :-) 이런 놀고 먹는 면에서 제법 잘 맞는 두리번이었다. 놀땐 놀고 할땐 하니까!
절거운 두리또 게임 다음날 둘둘 잔치가 열렸는데 기획단에서 refresh 데이, 친해지길 바래겸 10가지 질문 답변을 통해 나와 잘맞는 누군가와 데이트를 할 수 있는 기회를 주었다. 참여자는 해당 장소에 도착할 때까지 누구인지 알 수 없게 오카방으로 대화하였다.
이 잔치에서 한XX 씨가 '데이트'에 굉장한 설렘을 느끼고 있었지만 그의 매칭남은 김XX 여서 실망했다는 후일담도 존재한다.
둘둘 잔치로 향하기 전 아요 숙소에서 머물던 나와 박유지인 언니들과 + 인우 까지 갑자기 이태원 옷가게에서 주민룩을 구매하는 충동적인 짓을 저질렀다. 하지만 이 옷은 앱잼옷으로 후에 굉장한 찬사를 받고 있다.
💋 기획 + 디자인 + 서버가 왜 합숙해?
우리는 3주차에 기획, 디자인, 서버가 합숙을 진행했다. 으응? 기디서가 왜 합숙을 해 ? 물어 본다면.. 그냥 절거우니까
사실 그냥 기디 합숙에 서버가 침입한 넉낌이지만 오히려 너무 절거웠던 3주였다.
3주차에는 이미 서버 배포가 끝난 상태였기에 우리는 더미데이터 작업, 서버 자잘한 오류 수정등의 작업을 진행했다.
더미 데이터는 기획단에서 거의 완벽한 여행 일정을 짜줘서 우리끼리 나중에 여행갈 때 이거 쓰면 되겠다는 농담도 했다.
기디서 합숙 때 또 재밌는 일이 많았는데 대표적으로
유XX 씨가 아이스 아메리카노를 주문했는데 본인의 음료를 제외하고 모두 핫으로 시켜서 냉동실에 넣어두고 먹은 웃픈일이 있었다.
본인의 음료는 아이스로 시킨게 대단했다. 와중에 내가 열심히 차갑게 해둔 아아를 김x운 님께서 가져가서 자연스럽게 먹은 일화도 있었다.
다운 언니를 킹받게 하는 랄랄 모임(제기랄, 제랄, 기랄, 아기랄, 주기랄, 설기랄 등) 일원으로서 다운언니를 위한 ppt 선물도 증정해줬다.
목표에 맞게 언니가 매우 킹받아했다. 우리 숙소는 2층이었는데 refresh 타임을 가지러 내려오던 길에 1층에 놓여있는 두리펀 빨래 건조대를 보았다. 두리번의 형제 두리펀으로 추정된다.
피엠님께서 요리를 해주시겠다고 했으나 나와 유 xx 씨의 반대아닌 반대로 팬케이크에는 실패했다. 하지만 데모데이 전 새벽 열심히 더미데이터 막바지 작업을 하고 있던 서버 3명에서 피엠님께서 라면을 선사하셨다. 너무 맛있었다!!!
두번째 사진은 김xx씨가 본인이 시원하게 먹겠다며 냉동실에 넣어두고 그만 사라져 화산 폭발처럼 부풀어 오른 맥주사진이다.
3~4일정도에 짧은 합숙이었지만 미쳐 올리지 못한 다른 재밌는 일화도 많았다. 작업도 하면서 다양한 추억을 쌓아서 좋았던 마지막주였다. 집에서 외로운 마지막 주를 보냈다면 이토록 즐기지 못했을 3주차! 합숙을 제안해준 기디분들께 감사하다.
🎉 유종의 미
코로나로 인해 이번 앱잼 대상 수상은 과제 제출로 판단해 절대 평가로 진행됐다.
우리 팀은 기획단의 빠짐없는 트래킹으로 모두 과제를 성공적으로 제출해 대상을 받을 수 있었다. 고생한 3~5주를 보상받는 대상 수상이었다! 모든 파트원들이 농땡이 피우는 것 없이 열심히 작업해준 결과라고 생각한다.
팀빌딩 전 그리고 후 초반에는 두리번 서비스 너무 큰데 3주안에 구현할 수 있을까? 란 생각이 컸었는데 여러 사람이 모여서 일하니까 정말로 가능한 일이었다. 클라분들께서도 기능에 관해서 가능한지 물어보면 못하겠다는 말보다는 할 수 있다고 말씀해주신 것도 큰 것 같다. 클라분들이 가능하다고 하니까 서버도 덩달아 힘내서 작업할 수 있었다. 🎊
이로써 나의 첫 앱잼이 마무리 되었다! 첫 앱잼에 서버 리드개발자라는 큰 역할을 맡아 근심, 걱정이 많았는데 다른 팀원들이 너무 잘해줘서 내가 한 일도 많이 없지만 잘 끝낼 수 있었다.
앱잼 전에 YB로서 첫 앱잼에서 얻어가고 싶은 것에서 '성장', '사람'을 뽑았는데 두 가지 모두 가져간 앱잼이었던 것 같다.
앱 개발자 분들과 처음 진행해본 서버 개발, 혼자가 아닌 함께 작업하는 법을 배우게 해준 서버, 기, 디, 안드, 아요 등 타 파트와 소통하는 법 등을 경험하여 한층 더 성장한 서버 꿈나무 개발자가 되었다. 서버에 대한 흥미도 더 높아졌다.
물론 내 자신의 성장보다도 더 큰 것은 너무 잘 맞았던 두리번 팀원들을 만난 것! 다들 너무 재밌고, 실력도 좋았다.
이런 팀 다시 만날 수 있을까 👏🏻
3주동안 함께 노력하고, 작업하고, 놀아준 우리 두리번 팀원들 모두에게 감사합니다!
앞으로도 열심히 해봐요 🤝