첫 팀프로젝트에서 고민했던 것 (Hard skill)

마스터즈 코스에서의 첫 2주짜리 팀 프로젝트가 눈 깜짝할 사이에 끝났다. 크롱이 첨 팀 프로젝트는 일반적으로 협업에 대한 생소함 때문에 망하는 경우가 많다고 말씀하셔서 우리 조도 처음에는 빨리 코드를 작성해야한다는 생각은 잠시 접어두고 협업 방식을 정하고, 페어프로그래밍을 하는 등 소프트 스킬을 훈련하는데에 시간을 썼다. 그래서 이번 프로젝트에서는 하드 스킬에 관하여 정리할만한 내용이 없을 줄 알았는데, 돌이켜보니 꽤 많아서 그중에 몇가지 추려 정리해보았다.

1. 함수형 프로그래밍

이번 미션에서는 함수형 프로그래밍을 시도해보았다. 이번 프로젝트에서 '내가 짠 코드가 함수형 프로그램이 추구하는 가치들을 잘 실현했는가?'를 생각해보면 그렇지 못한 것 같다. 심지어 아직 잘 이해하지도 못했다. 대신, 이번 프로젝트에서는 함수형 프로그래밍의 주요한 특징들을 지키려고 노력하면서 코드를 작성했고, 왜 그런 특징들을 갖게 되었는지 직접 겪으며 이해하고자 했다.

👍 꽤 해냈어요: 고차 함수
😃 일부 적용했어요: 순수 함수, 참조 투명성
🤔 적용하기 어려웠어요: 무상태, 불변성

함수형 프로그래밍스럽게(?) 일하는 토끼들... (원본 출처)

잘한 점

각 토끼들을 함수로 보면 떼기() 누르기() 자르기() 뭉치기() 찍기() 정도로 정의할 수 있을 것이다. 토끼들은 자기한테 어떤 input이 오든 자기 역할을 수행 후 return한다. input이 같으면 return도 항상 같으며 자기가 수행한 동작이 전체 프로세스에서 어떤 영향을 미치는지 알 필요 없다. 나도 이번 프로젝트를 하면서 이렇게 명확한 역할을 가진 함수들을 정의하고 선언적으로 호출함으로써 원하는 결과물을 얻도록 했다.

이렇게 했더니 좋은 점은 가독성이 좋아지고, 함수를 재사용하기가 쉬웠다.

const parseRawColumnStates = (rawColumnStates) => {
  const parsedRawColumnStates = pipe(
    arrangeColumnOrder,
    storeEachColumnState,
    arrangeEachColumnCardOrder,
    storeEachColumnCardStates
  )([rawColumnStates, {}]);
  return parsedRawColumnStates;
};

const storeEachColumnState = ([rawColumnStates, parsedColumnStates]) => {
  rawColumnStates.forEach((rawColumnState) => {
    makeColumnState([rawColumnState, parsedColumnStates]);
  });
  return [rawColumnStates, parsedColumnStates];
};

위 함수는 server로 부터 받아온 'raw'한 column의 state들을 parsing하는 함수인데 이 함수는 각 역할을 하는 함수들을 호출함으로써 결과물을 만든다. 각 함수들은 자신의 이름에 맞는 역할을 수행하고 그 결과물을 return해준다. 각각의 함수안에서도 필요시 다른 함수를 호출하여 선언적으로 작업을 수행한다.

이렇게 input이 오면 외부 상황과 관계없이 자기 일만 수행하는 함수들을 만들었더니 만들어둔 함수를 조합해서 다른 곳에서 재사용하기도 쉬웠다. 재사용을 염두하고 코드를 작성했던 것이 아닌데 아주 유용하게 재사용이 되는 경험은 정말 짜릿했다.

못한 점

위 짤의 토끼들은 외부 상황에 영향을 받지는 않지만, 들어온 input을 직접 바꾸고(영향을 주고) 있기 때문에 완벽하게 함수형 프로그래밍스럽게 일하고 있다고 할 수는 없다.

다만, 아쉬웠던 점도 있다. 순수함수는 외부 상황에 영향을 받지도 않아야하지만, 동시에 주지도 않아야한다. 즉, input값을 건드리지 않고 새 객체를 만들어 return으로만 소통해야한다. 그러나 이번 프로젝트에서는 적용이 쉽지 않았다.

가장 큰 이유는 중첩되어 있는 state의 일부가 바뀔 때 전체를 새로 만들어 재할당하는 것이 어려웠기 때문이다. 이번 프로젝트에서 사용한 state는 column들 state안에 각 card의 상태들이 중첩되어 들어있었는데, 한 card의 일부 상태를 바꾸기 위해서 전체 객체를 다시 만드는 것이 까다로웠다. destructuring을 잘하면 가능할 것 같은데 시간 상의 문제로 그냥 일부 값만 바꿔주는 식으로 수행했다.

이 고민에 대해 크롱에게 질문했었는데 크롱이 그래서 라이브러리가 있는 것이라고 하셨다. 라이브러리를 사용하면 마치 값을 바꾸듯 상태를 바꿔도 새 상태 객체를 만들어 할당해줌으로써 불변성을 유지한다고 한다. 개인적으로 이런 방식의 학습을 즐긴다. 처음부터 라이브러리를 찾아 사용하는 것도 좋지만, 일단 부딪혀보고 어려움을 겪은 뒤 그것을 해결해주는 라이브러리의 도움을 받으면 (고마움도 느끼고) 라이브러리의 존재 이유를 더 잘 알 수 있게 되기 때문이다.

이 외에도 참조 투명성에 대한 아쉬움도 있지만, 글이 너무 길어져 줄이고, 함수형 프로그래밍에 대한 별도의 글을 작성하게 되면 그때 다뤄봐야겠다.

2. UX에 대한 고민

개인적으로 좋은 UX를 만드는 것에 큰 재미를 느껴서 항상 미션을 수행할 때마다 UX적인 부분을 많이 고려했다. 이번 프로젝트에서는 '드래그앤드롭' 기능을 API 없이 기본 event들만 가지고 구현하는 미션이 있었는데 이 기능을 구현하면서 팀원인 ver와 함께 UX에 대한 고민을 많이 했다.

드래그앤드롭 이벤트를 발생시키는 시점

이번 프로젝트에서는 카드를 더블클릭시 수정이 가능함과 동시에 드래그앤드롭 기능을 지원해야하기 때문에 mouseevent를 적절히 구분할 수 있어야했다. 처음에 가장 직관적으로 떠오른 방법은 다음과 같다.

  1. 카드에서 mousedown시 일정 시간의 delay를 둔다.
  2. 그 사이에 추가적인 mousedown 이벤트가 없으면 드래그앤드롭이라고 판단한다.
  3. 그 사이에 추가 이벤트가 발생하면 더블클릭이라고 판단한다.
function classifyByEventCnt(dragEventHandler, doubleClickEventHandler, interval) {
  let cnt = 0;
  let timer;

  return (...args) => {
    cnt++;
    timer = setTimeout(() => {
      if (cnt === 1) {
        dragEventHandler(...args);
        cnt = 0;
      }
    }, interval);
    if (cnt >= 2) {
      clearTimeout(timer);
      doubleClickEventHandler(...args);
      cnt = 0;
    }
  };
}

이 방법으로 코드를 작성할 때는 정말 드래그가 확실할 때만 드래그 이벤트를 활성화한다는 점에서 충분히 효율적이고 합리적으로 보였다. 그런데 실제 구현 후 동작을 시켜보니 UX적으로 문제가 있었다. '드래그가 아닌 상황'은 의도대로 잘 캐치했는데, 정작 사용자가 드래그를 하고 싶어할 때 문제가 있었다. 사용자는 드래그를 하고자 할때 마우스를 꾹 눌러 확실하게 잡았다! 라고 느낀다음 움직이지 않는다. 마우스를 클릭한 상태로 곧바로 움직인다. 그런데 코드는 일정 시간의 딜레이 후에 드래그앤드롭 기능을 활성화하므로 순간적인 버벅거림이 발생했다.

위와 같이 했던 이유는 드래그앤드롭이 아닌 경우에는 굳이 드래그앤드롭 핸들러를 호출하지 않고자 했기 때문이었다. 성능적으로 얼마나 비용이 발생하는지 정확하게 계산하진 못하지만 드래그가 아님에도 순간적으로 드래그를 활성화했다가 끄는 방식이 부담이 될 수 있다고 판단했기 때문이다. 그러나 UX적으로 약간의 딜레이는 굉장히 좋지 않았기 때문에 방식을 수정했다.

const handleMouseDownEvent = (e, cardNode) => {
  if (e.detail === 1) {
    handleSingleMouseDownEvent(e, cardNode);
  }
  if (e.detail >= 2) {
    handleDoubleMouseDownEvent(cardNode);
  }
};

매 mousedown마다 드래그 이벤트를 활성화하고 mouseup이 발생하면 종료하는 로직을 반복했다. 그랬더니 드래그 시작시 버벅거림은 사라져 UX적으로는 더 나아졌다. 결과물은 아래와 같다.

지금 돌이켜보면, 이 부분은 설계자체가 아쉽다. 클릭인지 드래그인지의 판별을 mousedown이벤트의 횟수로 판단할 것이 아니라, mousedown flag를 둔 후 mousedown인 채로 mousemove가 일어났는지에 따라 판단했으면 더 깔끔했을 것 같다. 즉 event handler는 등록한채로 유지하고 현재 flag에 따라 handler가 호출될지 결정하는 방식도 괜찮았을 것 같다.

개발자는 문학가는 아니다. 제1목표는 User에게 좋은 서비스를 제공하는 것이기 때문에 어떻든 사용자가 사용하기 좋은 서비스가 만드는 것이 중요하다고 생각한다. 그러나 비효율적인 로직이 계속 중첩되면 결국 크게 폭발해 최악의 UX를 선사하게 된다. 그렇기 때문에 항상 UX를 고려하되 로직의 효율성과 간결함을 신경써야한다고 느꼈다.

마무리

이 외에도 많은 기술적인 시도와 고민이 있었다.
1. webpack과 babel등의 개발 환경 세팅을 다른 사람들이 만들어둔 boilerplate를 사용하지 않고 직접 필요한 것을 찾아서 구성한 것.
2. 프론트 두 명으로 구성된 팀이었지만 mock data만 사용하지 않고 DB를 구성하고 서버를 열어 data를 실제로 주고받으며 저장한 것(팀원인 Ver가 백엔드를 담당해줬다).
3. mouseclick의 emit 조건. (같은 element에서 mousedown과 mouseup이 발생했다고 생각했는데 그렇지 않았던 경우)
4. 함수형 프로그래밍에서 각 함수 역할을 분리하는 기준 등...

크롱은 첫 팀프로젝트는 망하는 경우가 대부분이니 부담 없이하라고 하셨지만, 조금 후하게 평가하자면 충분히 성공적이었다고 생각한다. 다른 글에서 정리하겠지만, Ver와 소프트 스킬적인 측면에서의 궁합도 좋았고, 결과물도 완벽하진 않지만 여러 시도와 피드백을 거쳐 그 결과가 나왔다는 점에서 만족스럽다.

이번 프로젝트 경험에서 어렴풋이 알게 된 지식, 특히 함수형 프로그래밍 같은 경우는 이론적으로도 더욱 공부해서 이후 프로젝트에 계속 적용시켜보고 싶다. '그냥 남들 하니깐'이 아니라 명확한 이유를 가지고, 더 좋은 품질의 코드와 서비스를 위해서.

좋은 웹페이지 즐겨찾기