시작하며
담당업무
- 서비스 기획 및 개발 환경 설정 60%
- 개발 문서 작성, 업무 분담 및 일정 조율 80%
- 팀 나누기, 사다리 타기, 주사위 배틀 기능 구현 100%
- Bootstrap 템플릿으로 공통모듈 제작 100%
- 서비스 배포 100%
Keep
필요한 것 정복하기
Git을 공부하게된 이유
국비과정 중에 Git에 대한 이론적인 개념만 가볍게 배우고 넘어갔다. 사실 아직까지도 의문이다. 과정에서 왜 Git을 써보게 하거나 디테일한 부분에 대한 이야기 그리고 개발자에게 얼마나 중요한지에 대해 알려주지 않았다. 그래서 이제서야 채용 공고 혹은 먼저 취업한 동료들과 여자친구의 경험을 통해 거의 모든 곳에서 사용한다는 것을 알게되어 충격이었다.
학습을 위한 주제선정
이번 프로젝트는 Github를 공부하기 위해 시작했다. 그래서 프로젝트의 주제나 퀄리티는 중요하지 않았고, 프로젝트의 한 싸이클을 Github와 함께 경험해보는 것이 중요했기 때문에 작은 스케일의 주제를 선정하는게 효율적인 선택이었다. 브레인 스토밍 끝에 해리포터 속 마법의 분류모자(호그와트의 신입생들의 기숙사를 분류해주는 모자)를 모티브 삼았고 인원 배정, 미니 게임을 기능으로 삼았다.
온라인 클래스로 학습하기
쉽게 구글링으로 접할 수 있는 정보로는 약간 아쉬움이 남아, 인프런에 있는 온라인 클래스를 참고했다. Git과 GitHub 시작하기를 팀원과 같이 수강했는데 무료 강의에다가 강의 시간이 2시간 20분 남짓이며 터미널을 고집하지 않고 Sourcetree라는 툴을 사용해 거부감이 낮았다. 개념을 익히기에 충분했다.
프로젝트로 실습하기
- 원본 레포지토리를 fork하고 클라이언트로 clone을 받는다.
- 작업을 하고 commit을 하고 push를 하고
- upstream으로 Pull Request 하고
- merge한다.
위의 과정으로 작업을 진행했다. 실무 환경을 잘 알고있던 여자친구에게 과정에 대한 조언을 받았고 흔히 일어나던 Github로 인한 문제는 크게 없이 프로젝트를 진행할 수 있었다. 하지만 커밋 단위에 대한 아쉬움이 남는데 기준을 잘 몰랐던 것 같다는 생각이 들었다. 개인적으로 커밋 단위가 더 잘게 나누어져야하지 않나 생각이 들었다. 그렇게 하지 않았다 보니 되돌려야하는 경우가 생겼을때 곤란하기도 했었고 작업 내용들을 직관적으로 확인하기 불편했던 적이 몇 번 있었다. 그래서 가이드 기준같은게 있나 찾아보니 오히려 이 부분에 대한 다양한 의견을 찾아볼 수 있었다.
중요한 것에 집중하기
DB와 서버 사용하지 않기
이번 프로젝트는 DB를 필요로 하지 않기 때문에 서버에서 할 기능들도 프론트에서 js로 처리하는 것이 좋겠다고 생각했다. 그러면 따로 tomcat를 필요로 하지 않기에 유지비용을 줄이는 데 이점이 있기 때문이다. 그래서 “오히려 좋아” 마인드로 요새 급 부상하고 있는 JavaScript만을 사용해보기로 했다. 에러 추적이 Java에 비해 디테일하지 않다는 점과 기본 문법도 Java와 다르게 조금 더 추상적이라는 점을 제외하고는 이점이 더 크게 보였던 것 같다. 아직 내 수준이 그렇게 높지 않아서 그런 것 일수도 있다.
웹앱 템플릿 사용하기
모바일 웹으로 만들어야 했는데 템플릿을 구매하여 시간을 단축시켰다. 템플릿을 알아보는 과정에서 PWA를 중요하게 생각했다. 가장 큰 특징이 모바일 브라우저 앱의 더보기에서 ‘홈화면에 추가’를 하게 되면 일반적인 앱과 똑같은 환경처럼 사용할 수 있다는 것이기 때문이다. 네이티브 앱처럼 알림 기능은 불가능하다. 안드로이드 뿐만 아니라 ios에서도 가능하다. 템플릿의 컴포넌트들을 조합하고 기획에 맞춰 소스를 커스텀해서 프론트 구성을 빠른 시간에 수월하게 마쳤다.
Problem & Try
코드 리뷰
스타일과 협업
팀 프로젝트를 진행 하면 항상 가장 어렵다고 느끼는게 이 부분인데 개인마다 다른 프로그래밍 수준과 코드 스타일을 가지고 있다. 그래서 기준을 어디로 두어야 할지 모르겠다. 스타일에 대한 존중의 범위는 어디까지이며 만약 로직의 수준이 결과물에 영향을 미친다면 어디까지 용인이 가능한지 쉽게 기준을 정할 수 없다.
Q. “왜 이렇게 코드를 작성했어요?” A. “모르겠어요.”
Q. “이런 방법이 있는데 더 나을거에요.” A. “알려준 방법으로는 어떻게 구현해야할지 모르겠다.”
이런 상황이 종종 발생하곤 했다. 어떤 액션을 취해야할지 몰랐고 대처를 잘 하지 못 했다. 그때그때 코드를 대신 작성하는 것은 서로에게 도움이 되지않기 때문에 하지 않기로 전에 다짐했었고, 마땅한 해결책이 없어 그냥 진행토록 했다. 하지만 내가 PM이었고 팀 구성도 내가 했고 기능 분배도 참여했다. ‘담당 기능을 줄이더라도 시간을 더 배분하자’는 것이 이전 프로젝트에서 느낀점이었지만 적용하려 설득하는 것이 쉽지 않았다.
프로그램의 일관성
한 프로젝트의 결과물이 얼마나 일관성을 갖추어야 하는지. 그리고 일관성이 높은게 꼭 좋다고 할 수 있는지 정확하게 알지 못한다. 나의 완벽주의가 ‘사자의 머리에 뱀의 꼬리를 가진 키메라’를 못 마땅하게 생각한 것인지도 돌아보고 팀의 의욕 문제는 아니었는지 돌아보게 한다. 이 부분은 설계적인 부분에서 혹은 개발을 하다가 확실한 근거가 있는 경우에는 적극적으로 의견을 내보는 걸로 결론을 지었다.
코드 리뷰 문화
프로젝트가 끝나고서야 ‘코드 리뷰’라는 문화를 알게 되었다. PR을 기점으로 서로의 코드를 고민해보고 피드백하는 과정인데 팀 프로젝트를 할 때에도 서로 성장할 수 있는 좋은 과정일 것 같다. 기회가 된 다면 빨리 경험해보고 싶다. 카카오 테크 포스팅을 참고해보고 싶다.
답이 없는 알고리즘
팀 나누기
// arName : 전체 인원 리스트
// numTeam : 나눌 팀의 수
// return: array[팀][인원] 2차원 배열
function makeTeam(arName, numTeam) {
arName = shuffle(arName);
// 팀 설정
let arrTeam = new Array(numTeam);
for (let i = 0; i < numTeam; i++) {
arrTeam[i] = new Array(Math.ceil(arName.length / numTeam));
}
let x = 0,
y = 0;
for (let i = 0; i < arName.length; i++) {
if (i != 0 && i % numTeam == 0) [x, y] = [0, y + 1];
arrTeam[x++][y] = arName[i];
}
return arrTeam;
}
이 로직에서 중요한 것은 랜덤으로 원하는 팀의 수만큼 인원을 나누는 것이다. 다양한 방법이 떠올랐지만 나는 for문의 개수를 최대한 줄이고 싶었다. 그래서 2차원 배열을 컨트롤 하기 위해 2개의 변수로 핸들링했다.
사다리 타기
처음 사다리의 선들을 만드는 로직을 짜는 것은 어렵지 않았다. 하지만 사다리 타기의 결과를 모르면 의미가 없다. 그래서 플레이어의 아이콘이, 생성된 사다리를 따라 결과로 이어지는 기능을 구현해야했는데 정말 어려웠다. 계속 로직을 뒤엎어야 했다. 그리고 화면으로 연결하는건 또 다른 문제였다. 결국 화면의 요소들의 값을 고정하고 보이지 않는 div Element를 이용해서 성공을 했는데 과정은 이렇다.
- 사다리를 만든다.
function makeSadari(num) { const number = 2 * num + 1; let arSadari = new Array(number); for (let i = 0; i < number; i++) { arSadari[i] = new Array(len); for (let j = 0; j < len; j++) { arSadari[i][j] = 0; if (i % 2 == 1) { arSadari[i][j] = 'x'; } } } // 랜덤 숫자 방향 입력 let sfCnt = 1; let tempLine = 0; while (true) { let rdLine = (Math.floor(Math.random() * (num - 1))) * 2 + 2; if (tempLine == rdLine) continue; arSadari[rdLine][sfCnt] = Math.floor(Math.random() * 3 + 1); tempLine = rdLine; sfCnt++; if (sfCnt == len - 1) break; } return arSadari;
- 경로를 분석한다.
function findSadari(arSadari, focusLine) { let x = focusLine * 2 - 1; let y = 1; let arDistance = new Array(len * 2); let tempDistance = ""; while (true) { if (arSadari[x - 1][y] == '0' && arSadari[x + 1][y] == '0') { tempDistance += 2; y++; } else { if (arSadari[x - 1][y] == '1') { tempDistance += 4; x -= 2; } else if (arSadari[x - 1][y] == '2') { tempDistance += 7; x -= 2; } else if (arSadari[x - 1][y] == '3') { tempDistance += 1; x -= 2; } else { if (arSadari[x + 1][y] == '1') { tempDistance += 6; x += 2; } else if (arSadari[x + 1][y] == '2') { tempDistance += 3; x += 2; } else if (arSadari[x + 1][y] == '3') { tempDistance += 9; x += 2; } } tempDistance += 2; y++; } if (y >= len) break; } // 대각선 보정 tempDistance = tempDistance.replaceAll('12', '1'); tempDistance = tempDistance.replaceAll('32', '3'); tempDistance = tempDistance.replaceAll('7', '27'); tempDistance = tempDistance.replaceAll('9', '29'); for (let i = 0; i < tempDistance.length; i++) { arDistance[i] = Number.parseInt(tempDistance.charAt(i)); } return arDistance; }
- 경로에 따라 움직이는 함수들을 작동한다.
function doMove(lineNum) {
let arDistance = findSadari(sadari, lineNum);
moveDown(lineNum);
for (let i = 0; i < arDistance.length; i++) {
switch (arDistance[i]) {
case 1 :
moveJump(lineNum, 1);
break;
case 2 :
moveDown(lineNum);
break;
case 3 :
moveJump(lineNum, 3);
break;
case 4 :
moveSide(lineNum, "l");
break;
case 6 :
moveSide(lineNum, "r");
break;
case 7 :
moveJump(lineNum, 7);
break;
case 9 :
moveJump(lineNum, 9);
break;
}
}
$(".baseIconImg").eq(lineNum-1).removeAttr("onclick");
}
// 이동 방향
// 7 9
// 4 6
// 1 2 3
function moveDown(typeNumber) {
let target = $(".baseIconImg").eq(typeNumber - 1);
arIconTp[typeNumber - 1] += sizeTop;
target.delay(200).animate({top: arIconTp[typeNumber - 1]}, {duration: 100});
}
function moveSide(typeNumber, moveDistance) {
let target = $(".baseIconImg").eq(typeNumber - 1);
switch (moveDistance) {
case "l":
arIconLt[typeNumber - 1] -= sizeLeft;
break;
case "r":
arIconLt[typeNumber - 1] += sizeLeft;
break;
}
target.delay(200).animate({left: arIconLt[typeNumber - 1]}, {duration: 100});
}
function moveJump(typeNumber, moveDistance) {
let target = $(".baseIconImg").eq(typeNumber - 1);
switch (moveDistance) {
case 1:
arIconTp[typeNumber - 1] += sizeTop;
arIconLt[typeNumber - 1] -= sizeLeft;
break;
case 3:
arIconTp[typeNumber - 1] += sizeTop;
arIconLt[typeNumber - 1] += sizeLeft;
break;
case 7:
arIconTp[typeNumber - 1] -= sizeTop;
arIconLt[typeNumber - 1] -= sizeLeft;
break;
case 9:
arIconTp[typeNumber - 1] -= sizeTop;
arIconLt[typeNumber - 1] += sizeLeft;
break;
}
target.delay(200).animate({top: arIconTp[typeNumber - 1], left: arIconLt[typeNumber - 1]}, {duration: 100});
}
지금까지 개발한 것 중 가장 생각하기 어려웠고 그만큼 뿌듯했던 기능인 것 같다. 성공하고 사다리를 따라 내려가는 아이콘들을 보며 얼마나 짜릿하고 희열감이 느껴졌는지 아직까지 생생하다.
마치며
Github을 위해 시작한 팀 프로젝트였지만 소통이 아닌 스타일에 대한 갈등을 통해 협업에 대한 경험을 더 쌓고 자체 알고리즘을 구현하며 전제, 프로세스, 사용자 경험에 대한 고민을 통해 더 의미있는 경험을 할 수 있어서 좋았다.