ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [사이드 프로젝트] 미니 와인 쇼핑몰
    프로젝트 2023. 4. 12. 14:39

    프로젝트에서 고민하고 경험한 것들


    1. Typescript

     Typescript에 대한 갈증과 열망이 많이 올라와있었다. 개인적으로도, 회사에서도 Javascript를 주 언어로 개발을 하다보니 '분명 배열 맞는데.. 왜 null 이라고하지?' '분명 타입이 맞는데 왜 에러가 발생하지?' 와 같은 에러 해결에 고초를 겪기도 했다. 이에 전 회사에서는 Typescript를 도입을 고려했으나, 이미 비대해져버리고 얽히고 섥힌 구조들을 변화시키는 건, 당시 우선순위가 아니었기에 회사에서는 사용하지 않았다. 따라서, 이번 사이드 프로젝트에 Typescript를 적용해보고 적극적으로 사용해보고자 한다.

     Typescript를 사용하는데 느꼈던 단점은, 생각보다 런닝 커브가 있고 처음에는 타입 체크 에러 때문에 개발을 나아가지 못하고 낑낑거린 적도 많다. 특히, React에서 제공하는 훅, 컴포넌트 props, Redux의 RootState 등의 타입들을 선언해주는 것도 꽤 귀찮고 불편한 작업이었다.

     그러나, 점점 타입들이 손에 익어가 리팩토링과 디버깅 작업을 거칠 때는, 더 빠른 속도로 작업할 수 있었다. 개발하면서 느낀 장점들은, 자동완성과 가이드 기능이 강력하다. 예를 들어, 선언된 변수 값의 타입을 이해하고 메서드들을 자동완성 해주기 때문에 사용하고 있는 IDE를 더 효율적으로 사용할 수 있었다. 또, 의도치 않은 에러를 방지하고 타입 체크를 통한 오류를 바로 확인할 수 있으니 Typescript가 없으면 오히려 더 불편한 느낌이 들었다.

     

    타입 작성에 어려움을 느낄 때는 Typescript Handbook 한글 문서를 참고 했다.

     

    2. 전역상태 관리

     전역상태 관리 툴은 ContextAPI, Recoil, MobX, React-Query, Redux 등 많이 존재한다. 이 중에서 Redux를 선택했다. 이전 회사에서는 Apollo Client를 통해 전역상태 관리를 했고 이전 프로젝트에서 Redux를 사용했던 경험이 있어 비동기 처리를 기반으로 하는 Recoil을 사용하려고 했다. 프로젝트 전역상태 구조가 복잡하지 않아 빠르고 효율적으로 처리할 수 있기 때문이다. 그러나, Recoil은 어느 컴포넌트에서도 전역상태(state)에 직접 접근하여 상태를 관리한다는 점 때문에 상태를 완벽하게 보장한다고 할 수 없다. 아직 현업에서도 많이 쓰이지도 않는다. 여전히 전역상태 관리 툴 사용량 부동의 1위를 자랑하는 Redux를 사용하여 연습하고 구조를 만들고, Redux 비동기 처리 라이브러리 경험을 많이 못해봤기 때문에 미들웨어를 처리할 수 있는 라이브러리도 다뤄보기로 했다.

    Redux

     Redux는 Ducks 패턴으로 관리했다. 하나의 기능을 위해서 action, reducer, middleware 등 구조 중심으로 정렬하는 것이 아니라, 하나의 파일에 하나의 기능을 모두 넣어 더 직관적으로 관리하도록 했다. 

     이번 프로젝트에서는 굳이 Redux를 사용했기 때문에, 이후 프로젝트에서는 Recoil 또는 React-Query로 전역 상태관리를 해보고 싶다는 생각을 했다. 사실 이전 회사에서 Redux를 사용할 때에도 비대해진 폴더 구조들, 불필요한 전역 상태 관리(구조화하기 위해 action, reducer 등 생성...) 때문에 탈 Redux를 했던 경험이 있었지..

    Redux-Thunk

      Redux에서 비동기 작업을 처리하기 위한 미들웨어이다. 비동기 작업을 가장 많이 처리하는 라이브러리이기도 하고, 단순한 구조에서 Redux-Saga 보다 러닝커브가 낮게, 빠르게 효율적으로 처리할 수 있을것 같아서 선택했다. Redux-Thunk의 코드를 열어보면 고작 14줄 밖에 안 된다고 한다... 어메이징.. 사용하는 방법도 매우 간단하다. 액션 타입이 포함된 액션 객체를 디스패치하지 않고, 함수를 디스패치 할 수 있다. 이 함수에서 async/await를 사용해서 비동기 작업을 처리할 수 있다.

     장바구니 DB에 아이템 정보뿐만 아니라, 유저정보도 API 통신을 통해서 CRUD 해주어야 유저별 장바구니를 관리할 수 있기 때문이다. 또한, 미들웨어를 사용한다는 것은 디버깅에 매우 강력한 힘을 갖고 있다. 미들웨어에서 액션과 상태를 체크해주면 문제점을 찾아내고 또, 뷰 단에서도 더 나은 UX를 처리할 수 있기 때문이다. 이뿐만 아니라, Google Analytics에 트래킹 이벤트 관리도 해줄 수 있겠다.

    요청(Pending) / 성공(Success) / 실패(Failure)로 Status 관리를 할 수 있다.

    Redux-Persist

     유저 정보를 유지하기 위해 로컬 스토리지에 상태를 저장하기 위하여 사용했다. 서버로부터 받은 토큰을 서버 요청때마다 헤더에 담아서 보내주기 위하여 브라우저 로컬 스토리지에 저장한다.

     

    3. Mock API server

     빠르고 짧은 기간 내에 혼자 프론트엔드 기술만 집중해서 구현하고 싶었기 때문에, 서버에 들이는 작업과 비용을 최대한 줄이고 싶었다.  

    이에 처음에는 외부 API server를 이용하려고 했으나, 데이터 필터링과 같은 작업을 하는 가공 작업에서 어려움을 겪고, 불필요한 데이터들이 많아서 json-server 라이브러리를 통해 Mock server를 구축했다. DB는 외부 API에서 가져온 DB를 스키마에 맞춰서 임의적으로 만들어냈다. 또한 json-server-auth 라이브러리로 JWT Access Token을 Mock server에서 처리해서 응답해주기 때문에 사용했다.

     

    빠르고 간단하게 개발용으로 잘 구축된 서버를 활용할 수 있다.

     

     

    주요 기능들과 시행착오들


    1. 장바구니 🛒 

     redux-persist로 리덕스와 로컬 스토리지로만 관리했던 장바구니 상태들을 리덕스 미들웨어에서 유저 정보를 가져와 db에 접근해 CRUD 작업을 하고 장바구니 상태들을 관리해야했다. 사실, 처음 기획에는 로그인 / 회원가입 기능이 추가 기능이었는데, 생각보다 json-server-auth를 통해 쉽게 구현하면서 더 이상 로컬 스토리지로 장바구니 상태들을 관리하는 것은 의미가 없어졌다. 로컬 스토리지에 관리하는 것은 유저가 로그인 / 로그아웃 / 비회원 행동과 관계 없이 브라우저에 저장하기 때문이다.이 과정에서 여러 난관에 부딪히기도 했는데,

     첫 번째는 장바구니 db를 어떻게 구성해야 서버에서 라우트 가드를 안전하게 처리할 수 있을까 고민이 됐다. 서버에 해당 유저의 토큰과 필터링 요청을 할 값(유저 id)들을 요청하면 안전하게 해당 유저의 장바구니 데이터를 받을 수 있을 것이라고 생각했기 때문이다. 그러나 아쉽게도 json-server에서는 지원되지 않는 기능이었다. 따라서, 유저 id에 해당하는 값들을 응답받을 수 있도록 필터링 값에 요청을 보내는 것에 그쳤다. 아무래도 토큰과 유저 id 값이 아닌, 유저 id 요청만으로 데이터에 접근하기 때문에 해당 유저가 아닌 다른 유저의 데이터에 접근할 가능성이 생겼기 때문에 클라이언트 단에서 조금 더 신경써야 했다. 유저 정보가 바뀔때 렌더링이 잘 일어나고 있는지, 이에 따라 상태가 잘 변화되고 있는지!

     두 번째는 redux-thunk 미들웨어를 적용하면서 상태를 로깅하고 디버깅할 수 있으니, 디버깅과 생산성 향상에 효율적이었다. 또한, 이번 프로젝트에서는 스낵바를 통해 장바구니 담기 결과를 유저에게 전달하였는데 서버단 에러, 클라이언트단 에러, db 에러 등을 구분해 사용자들에게 더 나은 안내 모달을 개발하면 현업에서는 유저로부터 전달받은 버그 제보 -> 빠른 원인 분석과 해결을 할 수 있겠다는 생각이 들었다.

    2. 회원가입과 로그인 🙋‍♂️

     이번 프로젝트에서는 인증에서 Access Token만을 사용했다.  json-server-auth는 JWT Access Token을 발급해주나 유효시간이 1시간이다. 토큰을 브라우저에 저장하고 있기 때문에 xss, csrf과 같은 공격에 보안이 취약하다. 그렇기 때문에 access token 유효 시간을 짧게하는 것이 좋다. 이를 보완하기 위해 Refresh Token을 사용하는데,  비교적 긴 유효 기간을 갖고 만료된 access token을 재발급해주는 역할을 한다. 물론, refresh token을 어떤 방식으로 생성하고, 어떤 방식으로 보관하느냐에 따라 보안 취약성은 더 보완될 수 있다. 인증과 관련된 부분은 필자가 작성한 글을 참고하면 도움이 될 것이다.

     토큰 방식을 이용하여 인증을 처리하였고, 이러한 토큰을 헤더에 담아 라우트 가드를 해주어 자동로그인 기능을 추가하였다.

    3. 아티클 CRUD

     아티클 작성은 웹 에디터 라이브러리인 React-Quill을 사용하였는데, 글을 작성하게 되면 HTML 태그 형식의 string 값들이 저장된다. 이를 보여주기 위해서는 클라이언트 단에서 직접적으로 HTML을 삽입시켜줘야 한다. React 공식 문서에서 'dangerouslySetInnerHTML은 브라우저 DOM에서 innerHTML을 사용하기 위한 React의 대체 방법이라고 명시되어 있다. XSS 공격에 쉽게 노출될 수 있기 때문에 위험하다는 것을 상기시키기 위해 dangerouslySetInnerHTM을 작성하고 __html 키로 객체를 전달해주어야 한다고 한다.

    html 태그 요소 부분에 악의적인 스크립트를 포함시켜 공격할 수 있다고하니, 이를 보완하기 위해서 DOMPurify 라이브러리를 사용했다. DOM Purify는 HTML을 삭제하고 XSS 공격을 방지할 수 있다고 한다.

     또한 mock db에 직접 접근해서 데이터를 업데이트하고 삭제하는 기능을 사용했는데, 사실 현업에서는 데이터에 접근해서 직접 조작하는 경우는 흔치 않을 것 같다. 그래서 스키마에 deleted_at, updated_at 등을 추가하여 삭제하거나 업데이트 여부별로 체크해서 클라이언트에서 처리를 할 것이나, 사실 mock db라 데이터가 많아지면 성능상 문제를 초래할 것 같았다.

    4. 무한 스크롤

     무한 스크롤이란 유저가 페이지 특정 하단에 도달했을 때, API를 호출하여 원하는 내용을 계속 로드시키는 것이다. 특히, 내용이 많거나 이미지 등이 포함된 데이터들을 한 번에 모두 불러오는 것보다는 무한 스크롤 방식으로 일부 데이터만을 특정 이벤트에 지속적으로 로드시키는게 성능상, 사용자 경험에 큰 이점을 준다.

     라이브러리와 같이 다른 모듈에 의존하지 않고 만들기로 했다. 처음 고려한 방식은 스크롤 이벤트의 감지와 변화를 통한 방식이다. 여러 방식 중 하나는, innerHeight (뷰포트 내부 높이 픽셀 단위)와 scrollTop(스크롤 세로 방향 픽셀 단위)를 offsetHeight(패딩, 스크롤 바, 테두리가 포함된 요소 높이 픽셀 단위)와 함께 계속 스크롤 이벤트를 감지하고 비교하며 기대하는 특정 부분에서 API를 지속적으로 요청한다. 하지만, 이 방법은 스크롤이 변화할 때마다 함수를 실행시켜 성능을 저하시키는 문제때문에 Throttling 방식을 적용해 일정 주기에 맞춰 이벤트 발생을 보장받도록 하려고 했다. 그런데 이 Throttling은 setTimeout 함수 기반으로 실행되기 때문에 콜 스택 상태에 따라 원하는 시기에 값을 완전히 보장받을 수 없다고 생각했다. '아 또 이를 보완하기 위해 rAF 방법을 도입해야하나….'

     결국 스크롤 이벤트로 원하는 시기에 값을 온전히 보장받지 못하는 문제(기대하는 동작을 하지 않음), 성능 문제를 해결하기 위해 도입한 것이 바로 Intersection Observer API이다.  기본적으로 브라우저 뷰포트와 설정한 요소의 교차점을 관찰하며, 요소가 뷰포트에 포함되는지 포함되지 않는지를 구별한다. 스크롤 이벤트와 다르게 교차시에 비동기적으로 수행하며  과적으로는 성능적으로 나은 무한스크롤을 구현할 있게 되었다. (성능적으로는 후자 방식이 월등할 것이라고 생각했기 때문에, 성능 비교는 하지 않았는데.. 다음번엔 구체적인 성능 비교를 통해 개선 점을 가시적으로 돋보이게 해야겠다..)

    import { useState, useEffect } from "react"
    
    export const useInfiniteScroll = targetEl => {
        const [intersecting, setIntersecting] = useState<boolean>(false)
        const observer = new IntersectionObserver(entries => setIntersecting(
        	entries.some(entry => entry.isIntersecting)
        ))
        
        useEffect(()=>{
        	if (targetEl.current) observer.observe(targetEl.current)
            
            return () => {
            	observer.disconnect()
            }
        }, [targetEl.current])
        
        return intersecting
    }

     먼저, 특정 부분 도달시에 useInfiniteScroll 훅에 useRef target 요소를 인자로 넘겨서 호출을 해주면 IntersectionObserver 객체를 생성하고 observe() 메서드 호출로 요소를 관찰하고 있다가 setState를 시키고, 타겟 불리언 값을 저장해놓기 위해 선언해두었던 intersecting을 리턴해준다. 이때 useEffect가 언마운트 될 때는 cleanup을 통해 disconnect를 하여 관찰을 중지한다.

     그런데, 새로 고침 할 때나 새로운 데이터를 가져올 때 customHook이 연거푸 실행됐기 때문에 안전 장치를 마련하여 에러 방지와 성능 저하 방지를 위해리팩토링을 했다.

    Intersection Observer API를 사용한 Custom Hook

     

    Intersection Observer API로 구현한 리스트 무한 스크롤

     

    우측 상단 가격 안내 창이 뷰포트에 원하는 요소가 교차할 때, 스크롤에 맞춰 떨어지는 모습

    5. 정적 웹사이트 배포하기 (With Heroku)

     이번 프로젝트는 production용으로 개발된 것은 아니다. json-server 자체가 fake-rest api이기 때문에 무료로, json-server를 어떻게 호스팅을 해야할것인가가 관건이었다. 

     json-server free hosting site 글을 참고했다. 프론트 서버는 vercel 또는 heroku로 배포하고 데이터 서버는 glitch 또는 heroku로 배포하는 것이 무료로 가격부담 없이 쉽게 배포할 수 있는 것 같다. heroku로 모두 한 번에 배포하는 것이 관리나 완성 측면에서 쉽고 빠르고 좋을 것 같아서 결정했다.

     배포 하는 과정은 직접 작성한 글을 참고할 수 있다. 배포를 마치고 위기(?)를 겪게 되었다. *정적 웹 사이트이기 때문에 백엔드 서버와 API 호출을 통해 '동적 '리소스'를 불러오지 않는다.

     그래서, 로컬에서 구현이 되던 것들이 정적 웹사이트로 배포 이후에는 API 호출을 통해서 변경된 DB를 그대로 가져와 렌더링하는 클라이언트 단에서는 전혀 작동하지 않았다. heroku를 이용해 정적 웹사이트로 호스팅 한다는 개념을 이해하지 못했다. 관련된 문서를 읽다보니, '리액트로 빠르게 해결할 수 있겠는데?'라는 생각이 들었다.

     나의 해결 방법은 리액트의 '상태'를 이용하는 것이다. 브라우저에서 프론트 서버에 변화하지 않는 정적 리소스들을 요청해서 렌더링을 처리한다면, 데이터 서버를 정적 리소스로 올리고 리액트는 전역 상태관리로 API를 통신하고 이를 렌더링한다. 즉, API 호출을 통해서 응답받은 값들을 렌더링하는 것이 아니라, redux를 통해서 변화된 '상태'를 중앙 저장소에서 저장하고 필요한 페이지에서 가져와 렌더링하는 것이다. redux와 미들웨어(redux-thunk)를 통해서 해결할 수 있었다. 가상 돔을 사용하는 리액트의 매력이다.

     

    *정적 웹 사이트: 정적 저장소에 있는 정적 리소스(HTML, CSS, JS 파일 등)들을 가져와 전송해주는 프론트 서버로서 기능

    *동적 웹 사이트: 정적 저장소에 저장하지 않고 별도로 백엔드 서버를 통해 전송한다. 왜냐하면 API 호출을 통해 '동적 리소스'들을 불러와 전송해주어야 하기 때문이다.

    즉, 정적 웹 사이트를 호스팅한다는 것은 정적 파일들의 코드들이 정해진 채로 올라간다는 것이다.

     

    가장 아쉬운 것들


     역시.. 완성된 사이드 프로젝트 결과물을 내려면 좋은 팀원들과 함께 해야 한다는 것을 느꼈다. 서버, 인프라를 직접 구성할 수 있었더라면 성능적으로나 보안적으로나 많은 아쉬운 점을 해결했을 텐데, db 스키마 설계, 데이터 가공 처리 등을 하면서 진행했으면 생산성을 크게 향상시켰을 텐데, 하는 마음이 큰 것 같다. 하지만, 프론트 엔드 개발자로서 다양한 기술을 시도해보고, 실패해보며 본질적인 이해를 하는데 도움이 되었다.  혼자서 미니 프로젝트를 계속 시도하면서 경험해볼 것이고, 앞으로 풀스택 개발자로서 역량도 쌓아야하기 때문에 백엔드 공부도 열심히 해야겠다는 생각을 했다.

    '프로젝트' 카테고리의 다른 글

    heroku로 React + json-sever 정적 웹 사이트 배포하기  (0) 2023.04.15
Designed by Tistory.