회고

엘리스 SW트랙 최종 프로젝트 회고

엘리스 SW트랙에 지원했던 이유

지금 2기를 모집하고 있는듯?

엘리스 SW트랙에 지원했던 가장 큰 이유는 팀 프로젝트에 대한 기대 때문이었다.(물론 팀 프로젝트 경험 이외에도 많은 것들을 배울 수 있었다.) 지난 코드숨 과정을 통해 앞으로의 학습 태도나 방향에 대해 어느 정도 가닥을 잡긴 했지만, 개발자가 되기 전에 팀 프로젝트를 꼭 경험해보고 싶었다. 대체로 개발이라는 것은 누군가와 함께 하는 것이고 혼자 공부를 해오던 내가 팀에 얼마나 기여할 수 있는 사람인지 점검할 수 있는 기회가 필요하다고 느꼈기 때문이다. 엘리스의 커리큘럼과 두 번의 팀 프로젝트가 매력적으로 느껴졌다. 결론적으로 두 번의 팀 프로젝트는 모두 만족스러웠다. 내가 잘했기 때문에 만족스러웠던 것은 아니었다. 부족했던 부분을 느끼고 배울 수 있었기 때문이다.

 

기획의 중요성

 

최종 프로젝트에는 4주라는 기간이 주어진다. 오피셜하게는 설 연휴 주간을 제외하고 3주라는 기간이다. 시간이 넉넉한 편은 아니었다. 팀원들과 함께 이른 시점부터 기획 단계에 돌입했다. 결과적으로는 팀장님의 아이디어가 주제로 선정됐다. threejs를 활용하여 유저에게 사진을 공유할 수 있는 3D 공간을 제공하자는 상당히 흥미로운 아이디어였다. 단순한 CRUD를 구현하는데 그치지 않고 재밌게 해볼 수 있는 프로젝트라고 생각했다. 나를 포함한 팀원들의 큰 호응을 얻었다. 프로젝트를 진행하면서 가장 크게 느꼈던 부분 중 하나가 기획의 중요성이었다. 주제 자체가 새롭고 재밌게 느껴지다 보니 개발 과정이 지루하지 않았다. 기획 자체가 설득력이 있고 구체적이다 보니 작업에 속도를 내기에도 좋았다. 기획은 좀 더 진전되어 아마추어 작가들에게 온라인 3D 전시공간을 제공하는 Net + Studio = Nedio라는 서비스 기획으로 확정됐다.

 

기간 내에 완성할 수 있을까?

 

2주차는 방학인데 방학이 아니다

프로젝트 초기에 가장 우려했던 부분은 계획의 실현 가능성이었다. 4주라는 시간동안 과연 해낼 수 있을 것인지 우려하지 않을 수는 없었다. 자발적으로 선정했던 기술 스택이지만, 팀원들 모두 타입스크립트, threejs, Nest 등을 사용해봤던 경험이 없었기 때문에 너무 마음이 앞서지 않았나란 생각도 들었다. 하지만 끝내 기획을 축소하더라도 우선 부딪혀 보기로 했다. 나중에 더 얻는 것이 많을 것이란 판단이었다.

 

프로젝트 기간을 반으로 나누어 두 구간으로 진행하기로 했다.

 

첫 번째 구간: 디자인부터 시작하여 전시관을 제외한 나머지 부분을 구현

두 번째 구간: threejs로 전시관을 구현하고 프로젝트를 마무리하는 단계

 

미지의 영역이라고 할 수 있는 threejs를 처음부터 누군가가 먼저 학습하고 작업까지 병행해야 하는 것이 아닌지에 대한 고민이 있었다. 하지만 팀원 모두가 프로젝트의 핵심기능 구현하면서 새로운 것을 경험하려면 모두가 같이 뛰어들어야 한다고 판단했다. 결과적으로 예상보다 더 탁월한 판단이었다. threejs의 러닝커브는 각자 공부한 내용을 적극적으로 공유하면서 극복할 수 있었고 오히려 학습에 필요한 시간을 단축시킬 수 있었다.

 

와이어프레임 & 디자인

허접하지만 내가 만든 와이어프레임(balsamiq 활용)
내가 맡았던 메인페이지 디자인 (figma 활용)

 

확실히 와이어프레임 - 디자인 - 구현 단계를 거치다 보니 구현 도중에 디자인에 대해 큰 고민을 하지 않아도 된다는 점이 장점으로 다가왔다. 지난 팀프로젝트에서 백엔드를 담당했기 때문에 경험하지 못했던 부분이다. 팀원님들 덕분에 새로운 툴을 경험할 수 있었다.

 

고민했던 지점들

 

상태 관리, 이대로 좋은가?

 

나는 갤러리 생성 및 수정 페이지를 담당했다. 유저마다 원하는 수만큼 갤러리를 생성할 수 있다. 갤러리마다 제목, 카테고리, 오픈일, 종료일, 갤러리 설명, 포스터를 갖는다. 또한 하나의 갤러리는 최소 한 개 이상의 전시관을 포함해야 한다. 전시관에는 이름, 테마, 그리고 최대 10개의 작품을 등록할 수 있다. 그리고 각 작품마다 저마다의 제목, 설명, 이미지를 갖는다.

받아야 하는 인풋이 너무 많다..

한 페이지에서 관리해야 하는 상태가 많아지다 보니 초기 상태를 어떻게 잡아야 좋을지 판단이 잘 서질 않았다. 하나의 갤러리에 여러 전시관이 생성될 수 있고 각각 10개의 작품 데이터를 포함하기 때문에 그 깊이가 너무 깊어질 것이 우려됐다. 그래서 전시관의 상태는 나머지 부분과 분리시키기로 했다. 전시관에 표현되는 작품의 경우에는 그 위치가 보장되어야 하기 때문에 작품의 상태 배열 또한 그 순서가 보장되어야 한다. 그래서 전시관이 생성될 때마다 빈 값을 가진 10개의 작품 리스트를 생성해 주는 방법으로 순서를 보장시켜 주었다.

 

생성 페이지가 수정 페이지로도 활용되기 위해서는 서버와 주고 받는 데이터 모양과 컴포넌트에서 관리하는 상태의 모양이 어느 정도 일치하는 것이 좋을 거라 판단했다. 수정 페이지의 경우 갤러리의 id params를 포함하기 때문에 그것을 읽어와서 API 통신을 해주는 방식으로 구현했다. 이 부분에서 나중에 예기치 못한 오류가 발견되었다. 전시관의 내용을 수정하고 나면 이전의 전시관이 그대로 남아있고 수정된 내용으로 하나의 전시관이 새롭게 생성되는 문제였다. 이는 서버에서도 전시관의 db 컬렉션을 갤러리와 별도로 관리해주기 때문에 발생하던 문제였다. mongdb에서 주입된 전시관의 objectId가 전시관 상태에 새롭게 주입되는 현상을 발견했다. 생성 단계에서는 id가 없지만 수정할 때 데이터를 받아오는 과정에서 상태 관리 대상이 아님에도 objectId가 상태에 주입되는 것을 확인할 수 있었다. 어쨌든 자료를 수정하거나 삭제하기 위해서는 클라이언트 측에서도 서버로 전송할 id가 필요했기 때문에 reducer에서 적절하게 spread 연산자를 활용함으로써 예상보다 쉽게 문제를 해결할 수 있었다.

// reducer에서 활용하던 util 함수만 변경해주었다

// 배열과 인덱스, 객체를 받아 해당 인덱스의 객체를 주입된 객체로 대체한다.
export const updateArrayByIndex = (array, index, obj) => {
  return [
    ...array.slice(0, index),
        obj,
    ...array.slice(index + 1),
  ];
};

// 변경 전 객체에서 변경된 부분만 대체한다.
export const updateArrayByIndex = (array, index, obj) => {
  return [
    ...array.slice(0, index),
    { ...array[index], ...obj },
    ...array.slice(index + 1),
  ];
};

 

재사용할 수 있는 컴포넌트

 

수정할 땐 작품 정보가 포함되어 있어야 한다

작품 생성 단계에선 문제될 게 없었지만 역시나 수정이 필요할 땐 해당 작품을 클릭하면 작품 등록 모달이 작품 정보와 함께 출력되어야 한다. 처음에는 전시관마다 10개의 작품이 매핑되는 것과 같이 10개의 모달을 매핑했다. 하지만 이 방식은 매우 비효율적인 방식이다. 작품마다 모달 컴포넌트를 갖기 때문에 모달 컴포넌트에서 작품을 식별하기 위한 로직이 필요하지 않지만, 만약 전시관이 3개라면 30개의 보이지 않는 모달 컴포넌트가, 10개라면 100개의 보이지 않는 모달 컴포넌트가 존재하는 셈이다. 하나의 모달로 분리하기 위해서는 해당 전시관과 해당 작품을 식별하기 위한 정보가 필요했다. 하지만 각각의 전시관과 작품은 id를 포함하지 않기 때문에 새로운 식별 기준이 필요했는데 결국 인덱스로 식별하기로 했다. 이렇게 할 수 있었던 이유는 전시관과 작품 모두 상태로서 관리되는 배열 내 요소이기도하고 그 길이와 순서가 보장되기 때문이다.

</Container>
    // ...중략
    {modalOn && (
            <Modal
              halls={halls}
              hallIndex={hallIndex}
              pieceIndex={pieceIndex}
              closeModal={closeModal}
              onChange={onChangePieceField}
              onChangeNotification={onChangeNotification}
            />
          )}
</Container>

 

메모이제이션이 필요한가?

 

성능 최적화를 위해 리액트에서 제공하는 메모이제이션 도입을 고려하던 중에 갤러리 생성 페이지에 이를 적용하는 것이 과연 효과적일지 의문이 들었다. 아래 포스팅에 따르면 React.memo()를 사용하기 가장 좋은 케이스는 함수형 컴포넌트가 같은 props로 자주 렌더링될거라 예상될 때라고 한다. 

 

React.memo() 현명하게 사용하기

유저들은 반응이 빠른 UI를 선호한다. 100ms 미만의 UI 응답 지연은 유저들이 즉시 느낄 수 있고, 100ms에서 300ms가 지연되면 이미 유저들은 상당한 지연으로 느낀다. UI 성능을 증가시키기 위해, React

ui.toast.com

컴퍼넌트가 같은 props로 자주 렌더링되거나, 무겁고 비용이 큰 연산이 있는 경우, React.memo()로 컴퍼넌트를 래핑할 필요가 있다.

 

즉 위와 같은 상황이 아니라면 메모이제이션의 이점을 얻기 힘들고, 오히려 불필요하게 Props를 비교하는 작업과 렌더링 하는 작업 모두를 수행할 수도 있다. 갤러리 생성 페이지에서 관리하는 거의 모든 상태는 사용자의 인풋에 따라 변하고 Props로 전달된다. 그렇게 하위 컴포넌트로 전달되는 Props는 대체로 매번 다를 것이다. 체감상 React.memo()와 useCallback()의 활용 전후로 유의미한 차이가 나타나지 않았지만 좀 더 이 내용에 대해 고민해보고 적용했던 부분을 다시 드러내야 할지 결정해야겠다.

 

관심사 분리의 미흡함

 

프로젝트 중간 회고에서도 남겼던 내용이지만 이번 프로젝트의 컴포넌트 구조는 Container/Presentational로 구분하여 관리하기로 했다.  Container 컴포넌트는 상태에만 관심을 갖고 Presentational 컴포넌트는 Props를 받아 UI를 그리는 것에만 관심을 갖는다. 대략적으로 Route > Page > Container > Presentational의 순서로 컴포넌트가 구성된다.

 

나중에 테스트 코드를 작성하게 되면서 깨닫게 된 사실인데 내가 작성했던 부분에 큰 실수가 있었다. 갤러리 생성 페이지의 경우 Container가 약칭 A라는 단 하나의 Presentational 컴포넌트를 갖고 A가 여러 컴포넌트를 갖도록 구성했다. 그러다 보니 모든 props는 A를 무조건 거치게 되고 결국 A가 그 자체로서 중간에 존재하는 또 하나의 Container처럼 된 것이다. 이 구조는 분리된 테스트 코드를 작성하기에도 좋지 않고 불필요한 계층이 하나 더 늘어난 모양이었다.

 

디스패치하는 함수를 한 곳에 모을 수 있어서 더 깔끔할거란 생각에 적용했던 방식인데 A가 내려받고 내려주는 props가 늘어나면서 점점 잘못된 방식임을 감지했고 테스트를 작성하면서 이를 어느 정도 확신했다. 외부 의존도만 강하고 존재 이유를 찾을 수 없는 컴포넌트라고 생각했다. 아직 수정하진 못했는데 빠른 시일 내에 가장 고치고 싶은 부분 중 하나이다. 불필요한 컴포넌트를 제거하고 다른 컴포넌트들도 하나의 역할을 하는 컴포넌트로 더 분리시켜 좀 더 추상화시키는 작업을 시도해보려고 한다.

 

props로 전달되는 상태 변경 함수의 경우 의존성 주입을 통해 특정 객체와의 결합도를 느슨하게 해 주려고 노력했다. 예를 들어, 상태를 변경하는 이벤트 핸들러를 상위 컴포넌트로부터 받는 것이 아니라, 하위 컴포넌트에서 선언된 이벤트 핸들러가 상위 컴포넌트로부터 받는 상태 변경 함수에 의존성을 전달하여 실행시켜주는 방식으로 작성했다. 상태 변경 함수는 이벤트 객체에 종속되지 않기 때문에 재사용하기에도 좋다.

 

3D 전시관

 

내가 구현한 돔 형태의 전시관

이번 프로젝트에서 가장 흥미롭게 다루었던 부분이다. 1차 스프린트가 끝나고 각자 YouTube나 구글 서치를 통해 최대한 레퍼런스를 확보하고 학습하는 시간을 갖기로 했다. 학습 중에 알아낸 내용은 팀문서나 스크럼을 통해 공유했다. threejs를 위한 react renderer인 react-fiber를 활용했다. fiber를 통해 threejs를 JSX로 구현할 수 있고 다양한 hook을 활용할 수 있다.

 

라이브러리가 제공하는 인터페이스를 활용하다 보니 예상보다 원하는 것을 구현하는게 용이했다. 플레이어는 카메라를 달고 있는 투명한 구 형체로 구현되어 3D 공간 안에서 움직일 수 있다. 어려웠던 점이 있다면 충돌을 감지하는 부분이었다. 벽이 있다면 충돌을 감지하여 공간 밖으로 나갈 수 없어야 하는데 이를 감지하지 못했다. 플레이어가 벽을 뚫고 나가는 문제를 해결해야 했다. 구글에 Collison Detection으로 검색해보면 대부분 Raycasting을 이용한 해결 방법을 소개한다.

 

참고하고 있던 여러 레퍼런스도 모두 Raycasting을 활용하여 충돌을 감지했다. raycast는 3D 공간에서 Z값을 구할 수 있는 방법을 제공한다. 광원과 오브젝트가 있다면 빛이 오브젝트에 부딪히는 거리를 분석하여 Z값을 구한다고 한다. 이 방식으로 플레이어의 Z값을 구하여 이동을 제한시킬 수 있을 것으로 보였다.

 

하지만 Threejs를 학습하는 과정에서 use-cannon이라는 hook을 활용한다면 보다 더 직관적이고 쉽게 문제를 해결할 수 있을 거란 기대감이 들었다. cannon에서 제공하는 Physics hook을 활용하면 그 내부에 있는 개체들을 물리적인 방식으로 구현할 수 있는 듯이 보였다. mass나 gravity를 부여할 수도 있다. 처음에는 생각대로 되지 않았다. 분명 벽에 있는 위치임에도 뚫고 지나가버리고 엉뚱한 곳에서 보이지 않는 벽에 가로막히기도 했다. 이는 fiber를 통해 화면에 렌더링된 개체와 cannon hook을 통해 정의된 개체 간의 위치가 서로 다르기 때문에 발생했던 문제였다. 양측의 위치를 서로 같도록 조정하니 화면에 보이는 것과 충돌이 감지되는 것을 동기화시킬 수 있었다. 이 문제를 해결하는 과정에서 팀원님의 도움을 많이 받았다.

 

 

여기까지 했으면 사실상 큰 벽은 모두 넘은 셈이나 마찬가지였다. 그 이후에는 각자 원하는 대로 전시관을 꾸몄다. 액자와 그림을 걸고 조명을 설치하고 원하는 텍스쳐를 입혔다. 외부에서 3D 모델링을 불러와 가구를 배치하기도 했다. 기획단계에서는 기대조차 하지 못했던 전시관 테마 항목을 추가했다. 프론트 팀원들 각자가 자신만의 전시관 테마를 완성했다. 사용자는 전시관을 생성할 때 원하는 테마를 골라 자신의 사진을 전시할 수 있다.

 

그 밖에도...

  • 사용자 인풋에 대해 예외처리를 해주어야 할 것이 많았다. 여전히 미흡한 부분이 남아있어 추후 리팩토링을 통해 사용자의 잘못된 입력을 완벽하게 방지해야 한다.
  • 이미지의 가로세로비율을 고려할 수 있도록 넓이와 높이 속성을 추가했다. Image 인스턴스의 src 속성에 이미지 주소를 할당한 후에 onload를 통해 이미지의 속성 값을 추출할 수 있다. 현재는 단순히 두 길이를 비교하여 액자의 사이즈를 정해주고 있지만 나중에는 좀 더 디테일한 사이즈 조절 기능을 구현해보고 싶다.
  • TDD를 실천하지 못한 것이 아쉽다. 프로젝트가 끝나고 보니 단기간에 원하는 기능을 모두 구현하기 위해서 지금의 실력으로 어쩔 수 없는 선택이었다고 생각했다. 타입스크립까지 적용하다 보니 리덕스를 모킹하는 것조차도 까다로웠다. 현재는 극히 일부에 대해서만 테스트가 작성된 상태인데, 테스트 코드를 추가하려고 한다.
  • 타입스크립트를 좀 더 잘 활용해보고 싶은 욕심이 생겼다. 처음에는 타입 선언을 해주는 것이 불편했지만 시간이 지나고 나니 컴파일 단계에서 성능 좋은 안전장치가 하나 생긴 느낌이 들었다. 익숙해지기만 하면 실무에서 꽤나 좋은 도구가 될 것 같았다. threejs로 구현했던 부분은 여전히 any타입으로 적용한 부분이 남아있어서 좀 더 strict하게 타입을 적용시켜야 하는 숙제가 남아있다.
  • 2개 이상의 인자를 받는 함수의 경우 RORO(Receive an object, return an object) 패턴을 사용하려고 노력했다. 타입 선언이 조금 번거롭긴 하지만 인자를 객체로 감싸주기 때문에 그 순서를 지켜주지 않아도 되고, 좀 더 명료한 방식으로 함수 선언 및 사용이 가능하다는 장점이 있다. 관련된 내용은 아래 포스팅을 참고했다.
 

자바스크립트 디자인 패턴: RORO

해외 사이트를 돌아다니다가 Elegant patterns in modern JavaScript: RORO라는 글을 보게 되었다. 사실 그 글을 쓴 분이 창시한 패턴은 아니고, 이미 예전부터 사용되고 있던 패턴이긴 하다. 유명한 예로는 j

taegon.kim

  • 리덕스 + container/presentational 패턴으로 중앙집중적인 상태관리 방식이  다소 불편하게 느껴질 때도 있었다. 컴포넌트가 분리될수록 props drilling이 심해지기도 하고 값을 전달하기 위해 불필요하게 거쳐야 하는 컴포넌트가 많아질수록 비효율적이란 생각이 들었다. 리덕스를 활용한 커스텀 훅을 만들어서 적용해보고 싶다.
  • 비동기 통신을 위한 상태 관리의 경우 react qeury를 사용하기도 한다는데 이와 관련 내용을 알아보려고 한다. 마침 이전에 결제해두었던 노마드코더 강의가 업데이트되면서 react qeury와 recoil에 대한 내용이 추가됐던데 한 번 확인해보려고 한다.

 

후기

 

엘리스 SW트랙에서 제공하는 온라인 강의, 실시간 강의에는 예상보다 양질의 컨텐츠가 많았다. 참고할 만한 코드 패턴이나 새롭게 알게 된 내용이 꽤 있었다. 굳이 단점을 뽑자면 엘리스에 제공하는 IDE가 불편하게 느껴지는 순간이 더러 있다는 점, 과제의 채점 시스템의 정교함이 조금 떨어질 때가 있고 프로젝트 운영 세부 일정이 조금 지연될 때가 있다는 점 정도인데, 얻은 것에 비하면 매우 사소한 정도에 불과하다.

 

특히 팀 프로젝트를 통해 얻은 것이 많다. 주말 가릴 것 없이 매일 아침마다 스크럼을 진행하면서 함께 협업하고 문제를 해결해 나가는 과정이 재밌기도 했고 느끼는 바가 많았다. 실력있는 팀원분들에게 많이 배우기도 하고 소통하는 방식이나 적극성의 측면에서 스스로에게 다소 아쉬운 부분이 있음을 인지했다. 최종 발표가 끝나고 시상식이 있었다. 기쁘게도 우리 팀은 2등상과 인기상을 수상하는 영광(?)을 누렸다. 내가 좀 더 잘했더라면 1등을 할 수도 있었을 거란 생각과 함께, 주말에 하는 일과 취업에 관련 일정 때문에 좀 더 시간을 잘 활용하지 못한 점이 아쉬움으로 남았다. 더 좋은 코드로 구현하기 위한 리팩토링은 여전히 숙제로 남아있다.

'회고' 카테고리의 다른 글

3월 5주차 주간회고  (0) 2022.04.01
3월 2주차 주간회고  (0) 2022.03.13
엘리스 SW트랙 최종 프로젝트 중간회고  (0) 2022.02.11
2021-01-20 TIL  (0) 2022.01.20
2022-01-15 TIL  (0) 2022.01.16