굿즈 스토어 프로젝트 13 - 프로젝트 마무리, 후기.

이유승·2023년 7월 23일
0

국비지원학원을 다녔을 때, 파이널 프로젝트를 진행하면서 당시 강사분은 이런 조언을 해주셨다.

여러분이 이 프로젝트에서 구현하고 싶은 모든 것을 기획서에 넣으세요. 여러분의 힘으로 도저히 감당이 불가능하다고 생각될 정도의 분량이 들어가야 합니다.

감당을 할 수 없는 분량까지 집어넣으라니? 처음에는 이게 도대체 무슨 말인지 이해할 수가 없었다. 그러자 강사님은 이렇게 덧붙여주셨다.

기획은 방대하게 만들고, 실제 구현을 하면서 내가 할 수 있을 정도의 규모로 줄여나가야 합니다. 시작부터 내가 이 정도는 할 수 있겠다 하는 생각으로 기획을 한다면 전체적인 프로젝트의 규모가 정말 보잘것 없는 수준이 되어버립니다.

그런 이유로 이번 프로젝트를 처음 기획했을 때는 내가 실제로 구현할 것보다 더 방대한 내용들이 들어가 있었다. 기능 구현 단계에 들어서 지금 내 지식으로는 구현이 힘들거나 하는 등의 이유로 여기저기를 잘라낸 결과물이 지금까지의 내용. 이 포스트에서는 이번 프로젝트를 마무리하면서 구현하지 못했던 내용이나 다음에는 이렇게하지 말아야겠다 하는 내용들을 정리해보려고 한다.

좋은 코드를 짜야한다.

코드는 소프트웨어의 근간이다. 제대로 된 코드를 사용해 소프트웨어를 구축하지 않으면 언젠가 문제에 부딪힐 수 밖에 없다.

사실 좋은 코드를 추구하는 것보다 먼저 나쁜 코드를 지양해야 한다. 당장의 기능 구현에 급급해 코드의 품질에 대한 관심을 놓치게 되면, 나의 코드는 점차 무거워지고 혼란스러워진다.

이번 프로젝트에서 장바구니 기능 구현에 이르렀을 때, 코드의 확장과 수정이 어려운 수준까지 도달했다. "나중에 리팩토링을 해야지"라는 생각으로 일단 넘어갔지만, 다른 작업과 일상의 바쁨 때문에 그 기회는 잘 오지 않았다. 이번 프로젝트는 여러 가지로 배움과 성취감을 주었지만, 결국 마지막 단계에서는 나쁜 코드로 인해 결과물이 엉망이 되고 말았다.

다음 프로젝트에서는 '관심사의 분리(Seperation of Concerns)'와 같은 기본 원칙을 명심하며 같은 실수를 반복하지 않을 생각이다.



1. React-Redux의 사용 방법을 더 배워야한다.

어떤 원리로 전역관리 라이브러리가 동작하는지, 어떻게 사용해야하는 것인지까지는 이해를 했다. 그러나 실제 기능들을 구현하면서 전역으로 관리해야 하는 내용들이 어떤 구조와 순서를 가지고 컴포넌트와 Store를 오가야하는지 등의 실제 사용 능력은 완전히 이해하지 못했다.

하나의 컴포넌트에서만 사용하는 State가 Redux로 관리되어야 할 필요는 없다. Redux를 처음 사용하다보니 이번 프로젝트에서는 비동기 통신과 관련된 State 모두가 Redux에서 관리하게 되었는데, 사실 하나의 컴포넌트에서만 사용되는 내용까지도 Redux에서 관리하게 만든 탓에 Store의 구조가 점점 복잡해지게 되었고 프로젝트 후반부에 들어서는 기능에 에러가 발생했을 때는 내가 Action이나 State를 잘못 사용하고 있는게 아닌지 의심할 정도였다.

Redux에서 플래그 변수를 관리하고 있는데 프론트에서 useState로 플래그 변수를 또 만들어서 굳이 setState한다던지, 하나의 Store State에서 여러 값들이 저장되었다가 삭제되고 수정되고 하면서 에러가 계속 발생한다던지 등등..

Redux의 사용 방법에 대해서 더 공부하고, 더 고민해볼 필요가 있다.



2. 웹 보안을 신경쓰자.

이 프로젝트를 시작할 때는 애초에 웹 보안이라는 것을 생각해보지 않았다. 특히 백엔드와 데이터베이스쪽은 파이어베이스를 사용하기 때문에 보안은 파이어베이스에서 알아서 한다는 생각으로 더 신경쓰지 않는 것도 있었다.

그런데 프로젝트를 마치고 코드 리뷰를 하다보니, 중요한 개인정보가 프론트단에서 아무런 보안조치 없이 무방비하게 사용되고 있다는 점을 깨달았다. 이게 아무도 신경쓰지 않을 개인 프로젝트라서 그렇지, 진짜 상용 온라인 쇼핑몰이었다면 해킹이든 뭐든 바로 개인정보가 유출되었을 것이고 난 유출된 사실도 몰랐을 것이다..

과거 Express.js와 MySQL로 백엔드를 만드는 프로젝트를 진행했을 때는 그래도 암호화 모듈이라도 써봤는데 이번에는 오히려 퇴보해버린 것. 차기 프로젝트에서는 이 부분을 더 신경써줄 필요가 있다.



3. 더 세련되고 사용자 친화적인 UI를 만들줄 알아야 한다.

내 편의성을 위해서 프로젝트의 경고창은 그냥 alert 함수를 가져다가 사용했고, 사용자의 확인을 받는 기능은 confirm 함수를 가져다가 사용했다.

에러 모달창을 만들어본 것은 이전보다 발전한 것이긴 한데, alert 함수나 confirm 함수 등의 사용은 최대한 지양하는 것이 좋다.



4. 에러! 에러! 에러! 에러좀 신경쓰자.

이 프로젝트의 문제점이 한 두가지가 아니지만 가장 먼저 기억해두어야 할 것은 에러 처리 방법. 프로젝트 다 끝내놓고 나니까 잘못 한걸 알아서 따로 포스트로 만들어두긴 했지만 여기서도 한 번 더 언급해야 할 정도로 중요한 문제이다!

    createUserWithEmailAndPassword(appAuth, userData.email, userData.password)
        .then((userCredential) => {

            // 과정이 정상적으로 진행되었는데, userCredential 값이 존재하지 않으면 과정을 중단하고 에러.
            if (!userCredential.user) {
                throw errorCode.userSignInError.ThereIsNoUserCredential;
            }

            // 계정 생성이 완료되고, userCredential 값이 정상적으로 존재하면 displayName 정보를 업데이트 해준다.
            updateProfile(appAuth.currentUser, {
                displayName: userData.displayname,
            })
                .then(() => {
                    // displayName 업데이트가 정상적으로 완료되면 DB에 유저 정보를 저장한다.

                    addData()
                        // 작업이 정상 완료되면 SIGN_UP_SUCCESS Action을 실행하고 완료 메시지 출력, 이후 메인 페이지로 이동.
                        .then(() => {
                            addAdditionData()
                                .then(() => {
                                    emailVerifiedProcess(appAuth.currentUser)
                                        .then(() => {
                                            signOut(appAuth)
                                                .then(() => {
                                                    dispatch({ type: 'COMPLETE' });
                                                    dispatch({ type: 'SIGN_UP_SUCCESS' });
                                                    alert('인증 메일이 발송되었습니다. 이메일 함을 확인해주세요.');
                                                    alert('모든 과정이 완료되었습니다. 회원가입을 환영합니다!');
                                                })
                                                .catch((error) => {
                                                    dispatch({ type: 'ERROR', payload: createErrorData(error) });
                                                    navigate('/', { replace: true });
                                                });
                                        })
                                        // 이메일 인증 메일 발송에 문제가 생겼을 경우.
                                        .catch((error) => {
                                            dispatch({ type: 'ERROR', payload: createErrorData(error) });
                                            alert('인증메일 발송에 에러가 발생했습니다.');
                                            navigate('/', { replace: true });
                                        });
                                })
                                // 포인트 기록 저장에 에러가 발생.
                                .catch((error) => {
                                    dispatch({ type: 'ERROR', payload: createErrorData(error) });
                                    alert('포인트 기록 저장에 에러가 발생했습니다.');
                                    navigate('/', { replace: true });
                                });
                        })
                        // 계정 정보 파이어스토어 저장 - 에러 발생 catch 구문.
                        .catch((error) => {
                            dispatch({ type: 'ERROR', payload: createErrorData(error) });
                            alert('계정 정보 저장에 에러가 발생하였습니다.');
                            navigate('/', { replace: true });
                        });
                })
                // 파이어베이스 계정 생성 후 사용자 정보 업데이트 - 에러 발생 catch 구문.
                .catch((error) => {
                    dispatch({ type: 'ERROR', payload: createErrorData(error) });
                    alert('사용자 정보 등록에 에러가 발생하였습니다.');
                    navigate('/', { replace: true });
                });
        })
        // 파이어베이스 계정 생성 - 에러 발생 catch 구문.
        .catch((error) => {
            dispatch({ type: 'ERROR', payload: createErrorData(error) });
            alert('계정 생성에 에러가 발생하였습니다.');
            navigate('/', { replace: true });
        });
    

then-catch문의 남용은 이렇게 참담한 코드의 작성을 야기한다.. 회원가입 코드는 then-catch문을 6중첩.. 엉망진창이다.

심지어 가장 중요한 문제는 트랜잭션을 전혀 염두해두지 않았다는 것! 지금 내가 구현한 코드에서는 작업 중간에 에러가 발생해도 이전까지 진행된 작업을 취소하지 않는다. 나중에서야 깨달았지만 빼먹어서는 안되는 것을 빼먹은 것.. 기능을 테스트할 때 기능 하나를 만들고 테스트해서 동작을 확인하고 나중에 하나로 합쳐놨더니 기능이 동작하다가 중간에 에러가 발생하는 상황을 대비하지 못했고, 파이어베이스에서 제공해주는 서비스가 이미 완성되어있다보니 이런 쪽의 문제를 생각해보지 못했다.

사실 지금 내가 구현한 코드를 기준으로 한다면 try-catch으로 에러 핸들링을 해두기만 해도 반은 해결되는 문제이다. 다만 백엔드 기능은 사실상 파이어베이스에서 모두 알아서 하고 있어서 이미 실행된 기능을 어떻게 롤백해야하는지는 좀 더 고민해봐야할 것 같다.



5. 구현하지 못한 기능들..

IntersectionObserver API를 이용한 무한 스크롤 기능.

스크롤 이벤트를 이용한 무한 스크롤 기능은 이미 구현해봤고, IntersectionObserver API를 이용해서 CSS Style을 다르게 적용하는 기능도 구현을 해봤다. 프로젝트를 진행하면서 IntersectionObserver API를 이용한 무한 스크롤 기능을 구현해봤는데, 추가로 렌더링 되야할 요소들이 2개 혹은 3개씩 복사된다던지 스크롤이 움직이지 않았는데 이벤트가 무한 동작한다던지 문제가 계속 발생해서 시간적 문제로 인해 결국 구현을 중단해었다. 다음 프로젝트를 시작하기 앞서서 연습용 미니 프로젝트에서 구현을 연습해보고 나중에 진짜 제대로 만들어볼 생각이다.

CSS Animation.

슬라이드쇼가 동작할 때, 슬라이드가 좌우로 부드럽게 움직인다던지 다수의 슬라이드가 화면에 출력되어 둥글게 회전하는 것처럼 보인다던지 등의 세련된 CSS Animation을 구현하고 싶었다. 구현하다보니 계속 어디가 안되서 포기했지만 이 부분도 연습해서 구현할 생각이다.

장바구니 기능.

프로젝트 막판에 장바구니 기능을 구현해보긴 했는데, 이미 Redux Store의 구조 등이 꼬여버린데다가 다른 기능에 맞춰서 완성된 DB의 구조가 장바구니 기능을 구현하는데 계속 방해가 되서 제대로 구현하지 못했다.. 다음 프로젝트를 시작하기 전에 이 부분에 대해 더 고민해봐야 한다.

리액트에서 Checkbox 다루기.

등록한 제품 목록을 관리할 때, Checkbox를 이용해서 여러 개의 제품 혹은 특정 제품을 삭제하거나 수정하는 기능을 구현하려고 했다. 그런데 상품 갯수에 따라서 Checkbox의 숫자가 변화해야하는데 이 가변 Checkbox를 어떻게 관리하고 해야하는지 도저히 감이 잡히지 않아서 골머리를 앓다가 시간 문제로 구현을 보류하였다.. Checkbox 정도는 기본 기능에 속하는데 이걸 못한다는 건 말이 되지 않으니 더 고민해서 실제로 구현해볼 필요가 있다.

이메일 / 전화번호 인증.

이메일 인증은 구현해보긴 했는데, 파이어베이스에서 제공해주는 함수를 그냥 가져다가 사용한 것 뿐이고. 제대로 된 인증 시스템을 만들지는 못했다. 이게 단순히 프론트-백만을 오가는 기능이 아니라 외부 이메일이나 전화번호를 이용해야하는 시스템이다보니 지금 내 실력으로는 구현이 어렵거나 보안까지 염두해서 제대로 만들수가 없다고 해서 구현을 포기했다. 이 부분도 기억해 둘 것!

결제 시스템.

이건 진짜로 돈이 오가는 쪽의 문제라서 생각은 해봤지만 사실 진지하게 할 생각은 없었다. 결국 간단한 포인트 결제 방식으로 대신했는데, 웹 보안에 대한 문제를 생각해보면서 이 것도 기억해두었다가 언젠가 직접 구현해볼 필요가 있다.

더 정밀한 검색 기능.

파이어베이스 query 함수의 한계로 인해 검색 기능은 내가 검색하고 싶은 내용을 한 글자도 틀리지 않고 '정확하게' 입력해주어야 한다. 그게 아니면 첫 글자를 기준으로 순차적으로 글자를 늘려가는 검색 방식을 구현해야한다고 하는데, 아무튼 제대로 된 검색 기능을 구현할 방법을 고민해봐야한다. (사실 이 부분은 파이어베이스에서도 외부 서비스를 사용하라고 권고하고 있으니 직접 구현은 불가능할 것 같다. 나중에 외부 서비스를 사용하는 쪽으로 기능을 개선시켜야 겠다.)

원신 DB.

예시 1) / 예시 2) / 예시 3)
원래 굿즈 스토어 구현 이후에는 위와 같은 서비스를 제공하는 페이지를 구현할 생각이었다. 회원가입을 시작으로 실제 기능을 구현하면서 시행착오가 늘어나고 소모하는 시간이 많아지면서 최초 기획에서 기능이 하나씩 빠지더니 결국 구현 자체가 취소되어 버렸다. CSS Style을 사용하는 방법을 공부하는 것을 시작으로 다양하고 방대한 데이터를 체계적으로 다루는 방법을 공부할 생각이었던 만큼, 이 부분에 대한 것도 나중에 다시 시도해봐야 한다.

별점 리뷰, 별점 평균 계산.

리뷰 기능을 구현했을 때, 내가 원하는 것은 사용자가 별점을 입력하면 이를 계산해서 화면에 별점 평균이나 점수별 별점 등을 계산해주는 기능을 만드는 것이었다.

이런 식으로.. 그런데 별점의 평균을 계산하는 것이 생각보다 쉬운 일이 아니었다. 리뷰는 삭제할 수도 있는데, 리뷰를 삭제했을 때는 별점 평균도 다시 계산해야 한다. 그런데 DB에 어떤 순서로 계산을 다시 해야하는지 기존에 구현한 코드를 수정하면서 어딘가 계속 꼬이기 시작했고, 다른 기능을 먼저 구현하기로 하면서 결국 구현이 보류되었다. 사실 이건 이미 구현한 기능에 새로운 기능을 추가하거나 코드를 수정했다는게 더 문제가 되었는데, 다음에는 처음부터 제대로 계산이 가능하도록 기능을 잘 만들 필요가 있다.

컴포넌트 최적화.

이쪽은 처음부터 깊게 공부한 적이 없어서 useMemo와 useCallback의 존재, 컴포넌트 리렌더링의 최소화 정도만 알고 있었다. 일단 기능 구현을 먼저 하는데 급급하다보니 최적화 쪽은 거의 신경쓰지 못했는데, 이 부분은 나중에 더 공부해서 포스트로 정리해둘 예정이다.

파이어베이스 스토리지 사용방법.

파일을 저장하고 다운로드 받는 방법은 앞서 마이 블로그 프로젝트를 진행했을 때 다루긴 했었지만, 이건 그냥 기초 중의 기초적인 방법일 뿐이다. 스토리지에 저장된 이미지 파일을 프론트로 가지고와서 용량 최적화 등을 마치고 정확하게 사용하는 방법에 대해서 더 연구할 필요가 있다. 이번 프로젝트에서 이미지 파일을 사용할 때는 스토리지에서 제공하는 웹 이미지 URL을 적용했는데, 링크 URL을 그대로 사용하기에는 애로사항도 많았고 보안 문제도 걸려있어서 다음 프로젝트에서는 도저히 다시 사용할 방법이 못된다.

profile
프론트엔드 개발자를 준비하고 있습니다.

0개의 댓글