넘블 | OZ 프로젝트 회고록

개발 log·2022년 5월 15일
0

프로젝트

목록 보기
6/6
post-thumbnail

수행한 역할

1. 영상 업로드/업데이트 기능 구현

임베드/개인 영상, 업로드/업데이트로 구분된 총 4가지의 폼을 하나의 컴포넌트로 재사용성 있게 구현

어려웠던 점 | 비슷하지만 다른 역할을 하는 4가지 폼 컴포넌트 화

사실 이 4가지 폼을 한번에 컴포넌트화하지는 못하고 한번의 시행착오를 겪고 난 뒤 컴포넌트화할 수 있었다.

4가지 폼은 폼 데이터를 기반으로 요청을 보낸다는 부분은 모두 같았지만 각 폼 마다 사용되는 폼 데이터도 달랐고, 요청 역시 POST or UPDATE였기 때문에 이들을 모두 한번에 공통화 시키기는 어렵다고 생각했다.

때문에 이런 점은 차라리 완전히 같이 사용되는 부분만 공통화 하고 나머지는 따로 컴포넌트를 만들어 관리하는 것이 유지보수 관점에서 더 좋지 않을까? 라는 생각을 하게 되어 처음에는 완전히 공통으로 사용되는 제목, 설명, 태그, 썸네일만 컴포넌트화 시킨 후 다른 4가지 폼은 각각의 컴포넌트를 구현하여 총 4개의 컴포넌트를 구현하게 되었다.

처음에는 컴포넌트 명으로 수정해야하는 로직을 빠르게 찾아갈 수 있어 장점이 있다고 느꼈다.

하지만 이는 빠르게 잘못되었다는 것을 알게 되었는데, 문제점은 아래와 같다.

각 폼 마다 겹치는 부분이 있었기 때문에 예를 들어 임베드 영상 업로드 컴포넌트에서 임베드 영상 관련 로직을 수정하면 임베드 영상 업데이트 컴포넌트에서도 임베드 영상 관련 로직을 수정해야하고, 임베드 영상 업로드 컴포넌트에서 업로드에 대한 로직을 수정하면 개인 영상 업로드에 대한 로직도 수정해야하는 아주 번거로운 상황이 생겼다.

즉, 함수명으로 분기처리를 가독성있게 했다 해도 이것이 유지보수와 직결되지는 않는다는 것이다.

때문에 나는 이를 하나의 컴포넌트로 관리하되 폼 내부의 상태가 아닌 전달받아야 하는 prop이 무엇이 있을지부터 정리했다.

우선 임베드 영상과 개인 영상의 차이는 간단하다 임베드 영상이면 임베드 링크를 전달받는 input을 제공하고 개인 영상이면 영상 파일을 전달받는 input을 제공한다는 것이다.
이 부분은 formType이라는 prop으로 분기처리 했다.

까다로운 것은 업로드와 업데이트였다.
업로드와 업데이트는 초기 폼 데이터도 다르고 사용되는 api도 차이가 있기 때문에 단순히 타입으로 분기처리하기는 어려웠다.

때문에 타입 string으로 분기처리하기보다는 업로드와 업데이트의 가장 큰 차이인 초기 데이터의 유무로 업로드 폼인지 업데이트 폼인지 구분하기로 했다. 그 이유는 업데이트는 반드시 초기 데이터가 존재해야하고 업로드의 경우 초기 데이터가 반드시 없어야하기 때문이다.

이렇게 4가지 폼의 큰 차이였던 임베드 영상 or 개인 영상, 업로드 폼 or 업데이트 폼, POST or UPDATE에 대한 api로직만 props로 전달받고 나머지는 공통화 시키니 컴포넌트를 4개 만들었을 때보다 훨씬 편하게 유지보수 할 수 있게 되었다.

어려웠던 점에 대한 후기

사실 처음부터 이렇게 컴포넌트를 구현하라고 했다면 하지 못했을 것이라고 장담한다.
당시 개인 영상 업로드 api, 영상 업데이트 api는 구현되어 있지 않은 상태였고 나 역시도 이 4가지 경우를 한번에 압축시킬 수 있는 방법은 너무 복잡하여 구현 과정을 거쳐봐야겠다고 생각했기 때문이다.

개발에 있어 중요한 것은 도전정신이라고 생각한다.
만약 처음부터 4가지 경우의 수에 대한 대비를 하고 개발했다면 가장 베스트였겠지만 그렇게 했다면 초기 구현이 늦었을 것이다.

하지만 공통화 작업은 조금 뒤로 미룬채 하나의 경우의 수를 먼저 개발하다보니 기획과정에서 예상하지 못했던 문제도 하나의 경우의 수에서 해결할 수 있었고, 이에 대한 대처가 다른 경우의 수에도 도움이 되며 리스크는 줄인채 개발에 몰두할 수 있었다.

이처럼 초기에 제대로 된 설계를 하는 것도 중요하겠지만 후에 어떤 문제를 마주할지 모두 예상하기는 힘드니 도전하는 자세를 가지고 문제를 마주해가며 해결해나가는 역량이 개발자에게 필요하다고 느끼게 된 경험이었다.

배운점 | 상태관리 라이브러리 vs propsDrilling

이번 프로젝트에서 우리팀은 상태관리 라이브러리로 react-queryrecoil의 조합을 사용했다.
내가 이 프로젝트를 진행하기 전 전역상태(store)와 지역 상태를 나누는 기준은 아래와 같았다.

  • 전역: propsDrilling이 2~3depth이상 깊어지면 전역으로 관리
  • 지역: 해당 컴포넌트 내부에서만 사용되는 상태일 경우 지역으로 관리

이렇게 기준을 잡은 이유는 상태관리 라이브러리를 사용하면 상태를 전역으로 관리하면서, 부수효과를 최소화할 수 있고, 이로 인해 상태 오염의 위험도가 감소되니 전역 상태와 지역 상태의 큰 차이점이 없다고 생각했고 불필요한 리렌더링을 발생시킬 수 있는 propsDrilling이 깊어진다면 상태관리 라이브러리를 사용하여 상태를 전역으로 관리하는게 더 효율적이라고 느꼈기 때문이다.

이번 프로젝트에서 영상 업로드/업데이트 기능을 맡아 수행하며 폼 데이터에 대한 상태를 recoil을 통해 전역으로 관리했는데 그 이유는 개인 영상 업로드, 임베드 영상 업로드에 대한 분기처리, 업로드, 업데이트에 대한 분기 처리, file input, tag input 등 하나의 폼에서 많은 컴포넌트를 조합하며 깊은 propsDrilling이 발생할 것이라 생각했기 때문이다.

하지만 전역으로 폼데이터를 관리하면서 컴포넌트의 재사용성이 떨어지는 문제를 직면하게 되었다.
폼 컴포넌트는 개인 영상 업로드, 임베드 영상 업로드, 개인 영상 업데이트, 임베드 영상 업데이트 총 4가지 경우로 재사용되고 있다.
하지만 폼 데이터에 대한 상태를 전역으로 관리하니 폼 페이지를 벗어날 때마다 폼 데이터를 reset해줘야했는데 이 reset한다는 행동 자체가 컴포넌트와 상태의 재사용성을 무너뜨려버렸다.
때문에 폼과 전혀 상관없는 페이지 뒤로가기 버튼에 reset로직을 넣어두는 등 코드의 가독성과 응집도를 낮추는 현상까지 경험하며 전역 상태에 대한 단점을 몸소 깨닫게 되었다.

이에 상태를 나누는 기준에 대하여 다시 생각하게 되었는데, 우선 propsDrilling이 왜 나쁜가에 대해 다시금 생각해보기로 했다.
propsDrilling이 단점으로 작용하는 경우는 사실 prop을 사용하지 않는 컴포넌트에 전달하게 되었을 때이다.
이는 불필요한 렌더링과 코드 예측성을 떨어뜨리는 단점을 가져다 주는데, 그렇다면 반대로 propsDrilling을 사용하더라도 memoization을 적절히 활용하고 사용되는 컴포넌트에만 제대로 넘겨줄 수 있다면 propsDrilling을 사용하더라도 성능면에서나 가독성측면에서나 큰 문제가 없다는 것이 된다.

즉, propsDrilling을 피하고자 상태 관리 라이브러리를 사용하는 것은 잘못된 기준이었다.

그렇다면 언제 상태 관리 라이브러리를 통해 상태를 전역으로 관리해야할까?

사실 아직 이 부분에 대해서도 더 많은 공부가 필요하지만 현재 내가 내린 기준은 SPA에서 URL이 변경되더라도 즉, 페이지가 바뀌더라도 유지해야하는 상태면 전역으로 관리해야 하는 것이 맞다라고 기준을 정의했다.

이때까지 전역 상태에 대한 단점만을 얘기했는데 반대로 장점은 어디서나 사용가능하다는 것이다.
컴포넌트는 해당 컴포넌트를 벗어나면 지역적으로 관리하는 상태는 모두 초기화된다. 페이지 역시 페이지를 벗어나면 페이지 내부에서 관리하던 상태가 모두 초기화된다.
하지만 정말 드물게 페이지를 벗어나더라도 유지하고 싶은 상태가 있을 것이다.

이때 사용하고 싶은 상태가 있다면 이 상태는 전역으로 관리할 수 밖에 없을 것이다.

결론적으로 이 프로젝트 이후 내가 상태를 나누는 기준은 아래와 같이 바뀌었다.

  • 전역: SPA에서 페이지 or 컴포넌트를 벗어나더라도 유지하고 싶은 상태가 있을 경우
  • 지역: 이 외의 모든 경우는 컴포넌트의 재사용성을 위해 지역적으로 관리

2. 로그인 상태에 따라 영상 리스트 렌더링

메인 페이지, 검색 페이지, 마이 페이지, 관심 영상 페이지에 사용되는 비디오 리스트 정보를 접근 권한에 맞게 제공

어려웠던 점 | 로그인 권한에 따른 분기 처리

위에서 언급했듯 비디오 리스트 렌더링이 사용되는 경우는 총 4가지이다.
하지만 로그인 권한에 따라 각각 제공해주는 데이터가 다른데 차이점은 아래와 같다.

  • 메인 페이지, 검색 페이지
    • 게스트: 유저정보가 반영되어 있지 않은 비디오 리스트
    • 유저: 유저정보가 반영된 비디오 리스트(영상 수정/삭제 등)
  • 마이 페이지, 관심 영상 페이지
    • 게스트: 로그인 유도
    • 유저: 유저정보가 반영된 비디오 리스트(영상 수정/삭제 등)

우리는 interceptor를 사용하여 로그인 권한이 유효한지 여부를 판단했는데 여기서 403에러를 캐치하여 로그인으로 유도하는 것 까지는 어렵지 않았다.

하지만 완전 처음 접속하는 게스트뿐만 아니라 토큰이 만료된 유저 역시 게스트로 취급하기로 했는데, 이 과정을 처리하는 것에 어려움을 겪었다.

여기서 어려움을 겪었던 대표적인 이유는 서버측에서 게스트와 토큰이 만료된 유저를 판별할 수 없다고 하여 프론트측에서 이 문제를 해결해야하는 것이었다.

때문에 나는 interceptor에서 403에러를 캐치하면 게스트 처리해줘야하는 api만 분기처리하여 게스트임을 명시하는 정보를 담은 값을 헤더에 담아 원래 요청을 하는 방식으로 이 문제를 해결하였다.

배운점 | Access Token과 Refresh Token의 차이

이번 프로젝트에서 우리 팀은 JWT를 사용하여 로그인 기능을 구현했는데, 이 과정에서 프론트엔드는 interceptor를 사용하여 토큰 관련한 에러의 후처리를 수행했다.

여기서 JWT에는 Access TokenRefresh Token 두 가지 종류가 있는데 Access Token에 대해서는 클라이언트에서 토큰을 안전하게 보관하기 위한 기술이란 것을 이해했지만 Refresh Token에 대해 이해하는데에 어려움을 겪었다.

우선 이 두 토큰을 사용하여 클라이언트 정보를 안전하게 관리하는 로직은 아래와 같다.

  1. 유저가 로그인하면 서버에서 Access TokenRefresh Token을 발급해준다.
  2. 클라이언트에서 로그인 권한이 필요한 Data FetchingAccess Token을 담아 보낸다.
  3. 서버에서 Access Token이 유효한지 확인한 후 유효하다면 권한에 맞는 데이터를 내려주고 유효하지 않다면 401에러를 반환한다.
  4. interceptor에서 401에러를 캐치하면 Refresh Token까지 담아 토큰을 재발급 받는 api로 요청을 보낸다.
  5. 서버에서 Refresh Token이 유효한지 확인한 후 클라이언트에게 새로운 Access TokenRefresh Token을 보낸다.
  6. 해당 api에서 성공적으로 토큰을 재발급 받았다면 새로 발급 받은 유효한 Access Token을 다시 담아 원래 요청을 보낸다.
    • 이 과정을 통해 유저는 본인의 Access Token가 만료되었다는 것을 알지 않아도 된다.
  7. 만약 Refresh Token이 만료되었다면 서버는 403에러를 반환하고 이를 캐치하면 유저의 모든 로그인 권한이 만료된 것이므로 로그인을 유도한다.

이 과정이 왜 클라이언트 정보를 안전하게 관리할 수 있는 방법일까?

Access Token이 만료되었다는 것은 무엇을 의미하고 Refresh Token 만료되었다는 것은 무엇을 의미할까?

interceptor를 사용해보며 위와 같은 의문이 들었다.

우선 Access Token의 역할은 이름 그대로 접근 권한에 대한 유무를 판단하는데에 사용된다.
즉, 유효한 Access Token을 갖고 있다면 누구나 서버에서 데이터를 받아올 수 있는 것이다.
때문에 직접적인 접근 권한을 가진 Access Token은 만료 시간을 짧게 하여 혹시나 클라이언트에서 탈취당하더라도 아주 짧은 유효기간동안만 사용할 수 있기 때문에 비교적 안전하게 로그인 정보를 클아이언트에서 관리할 수 있다. (완벽하게 안전하다는 뜻은 아니다.)

그렇다면 Refresh Token의 역할은 무엇일까?
만약 Refresh Token의 유효 여부를 판단하지 않고 무조건 Access Token을 재발급 해준다고 해보자, 우선 유효하지 않은 Access Token이 어느 유저의 정보인지 알 수 없기 때문에 재발급을 해준다는 것이 말이 안된다.

이렇게 유효하지 않은 토큰을 재발급 받으려고 할 때 어느 유저의 Token을 재발급해 줄지 알아야하므로 Refresh Token이라는 이름 그대로 토큰을 새롭게 받아올 때 유저를 확인할 수 있는 용도로 사용된다.

Refresh Token의 유효기간은 Access Token에 비해 상대적으로 많이 길다.
알아본 바 최소 일주일 이상으로 잡는데 기본적으로 주 단위라고 생각하면 될 듯하다.

이렇게 유효기간이 긴데 만약 악의적인 사용자가 클라이언트에서 Refresh Token을 탈취한다면 어떻게 될까?

만약 악의적인 사용자가 Refresh Token을 탈취하여 토큰을 재발급 받는 api로 요청을 보낸다면 당연히 서버는 토큰을 재발급 해줄 것이다.

그렇게 되면 주 단위의 긴 시간동안 유저의 정보가 위험해지는 것인데 어떻게 로그인 정보를 안전하게 관리하는 것일까?

Refresh Token을 안전하게 관리하는 방법은 크게 두 가지가 있다.

1. 악의적인 접근임이 판단되면 두 토큰 모두 파기

어떻게 악의적인 접근을 판단할까?
클라이언트에서 재발급을 요청하는 api에 두 토큰을 담아보내면 서버는 중간 DB에서 토큰을 파싱하여 DB 정보와 일치하는지를 판단한다.
여기서 만약 현재 DB에 저장된 두 토큰이 모두 유효한데 재발급을 받으러 온 경우라면 이 경우를 악의적인 접근이라고 판단한다.
정상적인 유저가 재발급을 받는 경우는 Access Token이 만료되었을 때인데 현재 DB의 Access Token이 만료되지 않았다면 이는 악의적인 사용자가 Refresh Token만 가지고 새로운 토큰을 발급받기 위해 접근한 것이라고 판단하기 때문이다.
때문에 DB에 두 토큰이 모두 유효한데 재발급을 받으러 요청한 경우 두 토큰을 파기시켜 버리며 정상적인 유저의 새로운 로그인을 유도한다.

2. 토큰을 재발급해줄 때 Access Token만 재발급하는 것이 아닌 두 토큰 모두 재발급

그렇다면 만약 유저가 서비스를 사용한지 시간이 지나 Refresh Token은 유효하나 Access Token이 만료된 경우라면 어떻게 될까?
어쩔 수 없다. 서버는 이 이상 판단할 수 없기 때문에 두 토큰을 모두 재발급해줄 것이다.

하지만 두 토큰을 모두 재발급해버렸기 때문에 정상적인 유저의 토큰으로 접근하려할 때 새로운 로그인을 유도할 수 있다.

이렇게 악의적인 사용자가 Refresh TokenAccess Token을 모두 탈취하는데 성공했더라도 정상적인 유저가 다시 로그인해버리면 그 때부터 악의적인 사용자의 토큰은 무효화 되버리므로 로그인 정보를 안전하게 관리할 수 있는 것이다.

물론 유저가 새로 로그인하기 전까지 악의적인 사용자에게 Refresh Token이 노출된다는 단점은 있지만 클라이언트에서 로그인 정보를 유지하며 서버에 정보를 저장하지 않는 방법으로서는 현재 JWT가 많은 장점을 가지고 있다는 것을 알게 되었다.


프로젝트에서 발생한 이슈

배포 시 cannot find module "파일명" or its corresponding type declarations

프로젝트를 진행하던 중 개발자들만 모여 실서버 배포를 진행하기로 했는데, ec2 서버에 빌드 파일을 올리기 위해 빌드하는 도중 위와 같은 에러를 만났다.

배포는 백엔드 개발자분의 PC에서 ec2 서버를 사용했는데 백엔드 개발자분께서 빌드를 하는 도중 만난 에러였다.

우리는 이 문제를 해결하는데에 상당한 시간을 쏟았는데, 에러를 해석하지 못한게 아니라 프론트엔드 개발자들의 PC에서는 발생하지 않는 에러가 백엔드 개발자분의 PC에서만 계속해서 발생했기 때문이다.(실제 이 문제를 디버깅하기 위해 오전 8시까지 잠을 자지 못했다...)

당시 모듈을 찾을 수 없다는 에러에 대한 내용은 import 구문에서 Common폴더 경로를 찾을 수 없다는 것이었다.

우리는 분명히 Common폴더는 존재하고 프론트측에서 경로에 대한 이슈는 한번도 발생한 적이 없는데 왜 이런 문제가 생기는 것인지 알 수 없었다.

구글에 검색해보니 타입 문제일수도 있다고 하여 타입에 대한 디버깅도 진행해보았지만 해당 문제는 해결할 수 없었다.

하지만 폴더명을 전부 소문자로 바꾸는 작업을 진행해보니 빌드가 되었는데 우리는 이 과정이 왜 성공한 것인지 궁금했다.

첫째, 경로에 대한 인식을 소문자로만 한다?
일단 이 전제는 당연히 틀렸다, 나는 이전에도 폴더 경로를 대문자로 하여 배포해본 경험이 있는데 이런 문제는 발생하지 않았다.

둘째, 정말 경로가 잘못되었던 것이라면?
사실 경로가 잘못되었던 것이 정답일 수 밖에 없었다.
하지만 왜 경로가 잘못되었던 것일까?

우리는 Git Commit으로 디버깅을 해보며 이 문제를 알 수 있었다.

초기 프로젝트 세팅 때 폴더명에 대한 컨벤션을 정하는 과정에서 Common 폴더를 common으로 하여 git에 올린적이 있었는데 이 때의 기록이 계속 남아 백엔드 개발자 분께서 클론을 받을때 common 폴더로 클론하게 된 것이었다.

당시 프론트측에서 초기 세팅 후 각자 포크를 떠갔을때는 당연히 컨벤션이 맞춰져있는 상태였고, 프론트는 Common폴더로 사용하고 있었기 때문에 각자의 로컬에서는 문제가 없었던 것이었다.

우리는 이 현상이 도대체 왜 발생한 것인가에 대해 찾아보았고 서치결과 git은 대/소문자 구문을 하지 않는다는 것을 알게되었다.

때문에 pull받을때도 PR을 날릴때도 문제없이 프론트측에서는 동작할 수 있었던 것이었다.
(사실 이 이유를 알고나서 git에 조금 배신감을 느꼈다 ...😥)

이 문제에 대한 해결법은 크게 어렵지 않았다 git 설정만 해주면 되는 것이었는데 이는 아래와 같다.

// 대소문자 구분함
git config core.ignorecase false

// 대소문자 구분 안함
git config core.ignorecase true

우리는 이 설정을 통해 대소문자 구분을 하기로 했고 이를 설정한 뒤 커밋을 확인해보니 Common폴더가 새로 만들어져 있는 것을 알 수 있었다.

다만 common 폴더는 그대로 github에 남아있었는데 이는 레포에서 직접 지워주는 방식으로 이 문제를 해결할 수 있었다.

겨우 대/소문자 구분이 개발자를 이렇게 힘들게 할 수 있다는 것을 처음 알게되며 개발에 있어 명확함이 얼마나 중요한지 다시금 깨달을 수 있는 기회였다!

profile
개발자 지망생

0개의 댓글