[프로젝트 회고] 자사몰 회고

최관수·2025년 1월 9일

회고

목록 보기
1/8

혹시 찾으시는 게 쇼핑몰이라면 하단 링크를 클릭!

https://mubeatmall.shop/

들어가며

이 프로젝트의 회고는 개발 자체보다 프로젝트 조율 및 불쾌하고 지저분한 익숙하지 않은 환경에서 개발 친화적인 환경을 구축해나간 과정을 이야기하고 싶었다. 그리고 커리어적으로 큰 도움이 되기 어려운 프로젝트에서 무엇을 배울 수 있을지 고민했고, 그 과정 또한 유의미한 경험이었다.

자체 개발에서 카페24로

자체 개발

처음 레퍼런스 체크 및 회의를 진행할 땐 자체 개발로 방향이 잡혀 있었다. 아무래도 쇼핑몰 특성상 검색 엔진을 통한 유입이 많을 수 있기 때문에 SEO가 중요하겠다 생각했고, 이미지 리소스가 많을 것으로 예상되었기에 최적화가 수월한 Next.js 사용을 고려했다. SSR 기반이다 보면 리소스가 많이 발생할 수 있어 캐싱을 위한 라이브러리로 TanStack Query 정도로 그림을 그려두고 있었다.

네..? 카페24요..? 🤔

그 이후 카페24 솔루션이 도입 방안으로 거론되기 시작했다. 이유는 기존 운영 중인 쇼핑몰이 카페24 솔루션을 사용하고 있기도 했고, 운영팀에서 택배와 결제 모두 해당 시스템을 사용하는 것에 익숙하다 보니 큰 틀을 바꾸기 어려웠다. 그리고 택배 관리를 포함한 어드민 및 백엔드 개발도 동일하게 기획 및 개발을 해야 하는데 그럴 만한 공수를 들이기 애매한 부분도 있었다.

카페24 솔루션 기반의 작업은 경험이 있었다. 어쩔 수 없이 해야 했지만 유쾌한 경험은 아니었다. 카페24에서 제공하는 스마트디자인 편집창은 도통 스마트하지 않고, 버전 관리와 흡사하게 ‘히스토리’라는 메뉴를 제공하고 있지만 최대 10개까지만 제공된다. 그 외 다양한 버그와 안타까운 사용성 등 단점이 많다. 비개발자 기준으로 간단한 HTML 소스를 수정하긴 나쁘지 않지만, IDE 기반으로 개발을 쭉 해왔던 사람이라면 개발자 경험이 좋지 않다.

카페24는 카페24만의 문제도 아니다. 카페24 모듈을 기반으로 스킨을 제작하는 제작사들은 대체로 SI 업체들이고, 모든 스킨이 그렇지는 않겠지만 내가 여지껏 접했던 스킨의 소스는 대체로 난잡했다. 어떤 컨벤션이 안 보이는 건 둘째치더라도 !important를 떡칠해 놓은 스킨도 있었고, 선택자를 명확히 잡아서 수정하지 않고 CSS property를 계속 덮어씌우는 식의 override도 적지 않게 봐왔다. 나도 그들과 비슷한 환경에서 업무를 했던 경험이 있기 때문에 그들의 사정도 이해 못 하는 건 아니지만, 그런 소스를 커스텀하는 것 자체는 꽤 스트레스받는 일이었다.

특정 스킨을 베이스로 시작하다 보니 디자인 자유도 또한 떨어지고, 동시에 카페24 내부 모듈을 기반으로 기능이 동작하다 보니 개발 자유도도 낮아질 수밖에 없다. 개발 자유도가 떨어진다는 건 장기적으로 봤을 때 결국 기획의 확장성에 한계가 있고 동시에 프로젝트의 기술 확장성이 제한된다. 단순히 개발자 경험에 한정된 단점이 아니라는 의미이다.

영원히 안 보고 살 줄 알았는데 다시 카페24 솔루션이라니. 내 개인의 입장에서도 사실 커리어에 큰 도움이 안 된다고 생각했다. 애초에 React 기반의 프로젝트 경험을 더 쌓아도 모자를 시간에 카페24 솔루션 작업이라니. 어쨌든 어른들의 사정으로 정해진 건 돌이킬 수 없기에 내가 할 수 있는 것을 해내고 그 과정에서 경험을 얻어내야 했다.

내가 할 수 있는 것

기획자, 디자이너와 작업 컨벤션 정하기

불필요한 공수를 줄여야 했다. 앱 내 들어가는 웹뷰이기도 하고 기간 내에 반응형을 챙길 여유가 없었기 때문에 모바일 퍼스트 디자인을 제안했다. 최근 모바일 디바이스의 점유율을 생각해 보면 합리적인 제안이었고, 모두의 동의를 얻어 모바일 퍼스트의 스킨을 베이스로 커스텀하는 것으로 결정되었다. 내 경험상 자체 개발과 달리 솔루션 기반의 커스터마이징 작업은 기획자, 디자이너와의 초기 소통이 중요하다. 카페24가 제공하는 기능을 크게 벗어나는 기능을 추가하거나 기존 스킨과는 너무 다른 디자인 시안이 작업되면 이후 소통 비용이 배 이상으로 증가할 수밖에 없다. 모두의 소통 비용을 줄이기 위해 회의를 자주 잡게 되었고, 초기 회의 자리에서 두 분에게도 “아무래도 제가 많이 괴롭힐 거 같다”는 농담조의 멘트를 드리기도 했다. 어쨌든 기능 기획은 카페24에서 제공하는 기능 기반으로, 디자인은 구매한 스킨의 구조를 참고하면서 구현하고, 만약 구현이 어려우면 추가 논의를 진행키로 하였다. 두 분 모두 소통이 원활하셔서 큰 갈등 없이 프로젝트를 진행할 수 있었다.

IDE 사용하기

기존처럼 웹 기반의 스마트디자인 편집창을 사용하고 싶진 않았다. 사용성이 떨어지는 건 둘째 치고 별도로 관리 가능한 VCS이 필요했다. 처음엔 단순히 자체 개발 프로세스와 비슷하게 버전 관리를 하고 특정 시점에 FileZilla 같은 FTP 솔루션을 통해 서버에 파일을 전송하려 했다. 찾다 보니 IDE 내부에서도 익스텐션을 통해 FTP 전송을 처리하는 게 가능했다. VS Code + 익스텐션을 고려했는데 WebStorm은 자체적으로 FTP 전송과 더불어 로컬 파일과 서버 파일의 diff까지도 출력하는 기능이 내장되어 있었다.

WebStorm을 통한 diff와 FTP 전송은 생각보다 간단하다.

  • 카페24 어드민에서 디자인 FTP 신청
    • 서버에 직접 업로드를 위한 권한 신청
    • FTP 주소 및 SFTP 접속 포트 등 부여받음
    • 7일이 최장이라 만료 후 추가 신청 필요
  • WebStorm 내부에서 SFTP 세팅을 통해 부여받은 Host, Port, Username, Password 등 입력
  • 특정 폴더를 맵핑하는 것도 가능하나 이번 서비스의 경우 국문/영문 별도의 스킨으로 나뉘어 있어서 맵핑하지 않음

라이브러리 확인하기

npm을 통한 패키지 관리를 할 수 없어서 현재 사용 중인 라이브러리의 종류와 버전을 직접 확인해야 했다. 로컬 파일을 들고 있거나 CDN으로 연결된 라이브러리의 종류는 아래와 같았다.

deprecated 된 Moment.js를 포함해서 상당히 오래된 버전들이긴 했지만 뭐 이미 적용된 걸 어쩌겠나. 걷어내기엔 당장 크리티컬한 이슈를 발생시키는 라이브러리들도 아니고 추후 대응할 수 있게끔 파악하는 정도로 그쳤다. 최초 적용한 라이브러리들이 레거시가 되고 deprecated 되는 와중에도 스킨을 계속 재가공해서 팔고 있는 그 생산성이 대단하다..

배포 과정 정하기

다른 프로젝트의 경우 master, develop, feature 등의 branch로 나누고 git의 tag를 통해 운영 버전을 나눠서 Jenkins를 통해 관리하는데, 일단 Jenkins가 아닌 SFTP를 통해 직접 배포하기 때문에 조금 다르게 정할 필요가 있었다. 우선 서비스 개시 전에는 developmerge 후 배포하는 형식을 취했다. 다만 서비스 개시 후에는 특정 기점을 잡고 tag로 동일하게 관리하고, 롤백이 필요할 때는 git checkout을 통해 해당 tag로 HEAD 이동 후 적용하기로 했다.

나름의 컨벤션 정하기

혼자 작업하긴 하지만 간단한 컨벤션은 필요하다고 생각했다. 다른 것보다 기존 코드와 새로 작성되는 코드가 구분점 없이 혼용되는 건 유지보수가 어렵다고 판단했고, 이미 override가 여기저기 발생한 스킨이었지만 새롭게 작성되는 override는 별도의 파일 작성과 더불어 특정 부모에 종속될 수 있게 작업하기로 했다.

  • 추가 요소의 class는 케밥 케이스로 작성
  • 기존 작업물은 스네이크 케이스와 카멜 케이스가 혼용되어 있어서 기존 작업물에 맞추기보다 추가 요소를 케밥 케이스로 구분 짓는 방향으로 정리
  • 추가되는 모듈 및 CSS는 vd_import 디렉토리에 추가
  • 추가된 HTML은 vd_import 하위에 위치
  • vd_import/css/style.css - 별도 추가된 class 선택자를 통해 추가된 styling
  • vd_import/css/override.css - 기존 class의 styling의 override를 의해 선언하는 styling, override는 부모 선택자와 함께 사용, 다른 모듈에 공통 적용되지 않도록 주의
    • override의 경우 최상단의 .app-vlending-mubeatmall 별도로 부여한 클래스를 부모 선택자로 선택하고 작업하는 방법을 추천

주요 작업 요소

카페24의 먼지 쌓인 모듈과 스킨의 아득한 선택자를 찾아 작업하는 CSS 수정이 절반 이상의 작업이었는데, 다소 귀찮고 시간이 오래 걸리긴 했지만 난이도가 높은 작업은 아니어서 기술적으로 특별히 언급할 만한 내용은 없었다. 가장 논의와 테스트를 길게 했던 건 앱 내에서 자동 로그인이 정상적으로 이루어지는가였다.

웹에서의 로그인

자사 서비스 계정과의 연동이 필요했기 때문에 SSO 로그인을 선택하게 되었는데, Authorization Code나 Access Token을 주고 받는 과정을 서버에서 처리해 줬기 때문에 생각보다 프론트는 수월했다. 카페24에서 제공하는 전체 과정은 아래와 같다.

웹뷰에서의 로그인

다만 앱과의 연동 작업은 생각보다 더 챙겨야 할 부분이 많았다. 우선 파라미터 정의가 필요했다. 페이지 로드가 끝나는 등 어떤 특정 시점에 협의된 URL 스키마를 호출해야 되기도 했다.

// 페이지 로드 후 앱 내 로딩 요소 제거를 위한 선언부
window.onload = () => {
    window.location.href = 'mbmall://onUserStateChange?data=login_finish';
};

해당 URL 스키마는 웹뷰에서만 사용하기 때문에 불필요한 콘솔 에러를 보고 싶지 않다면 예외 처리가 필요했다.

// 페이지 로드 후 앱 내 로딩 요소 제거를 위한 선언부
window.onload = () => {
    const queryParams = new URLSearchParams(window.location.search);
    const mallConnectType = queryParams.get('mall_connect_type');
    if(mallConnectType && mallConnectType === 'app') {
        window.location.href = 'mbmall://onUserStateChange?data=login_finish';
    }
};

앱에서 자사몰에 접속하면 사용자가 처음 보는 화면은 로그인된 메인 화면이지만, 내부 로직상 여러 과정을 거쳐야 한다.

  • 최초 자사 웹사이트에 접근해서 앱에서 들고 있던 accessToken 삽입
  • accessToken 삽입이 끝난 후 자사몰 메인으로 redirect
  • mallConnectTypeapp인 경우 자동으로 자사 웹사이트 로그인 페이지 window.open
  • mallConnectTypeapp이면서 accessToken을 자사 웹사이트에서 들고 있는 경우 로그인 시도 및 성공
  • (path가 있는 경우) 협의된 URL 스키마를 호출하여 로딩 인디케이터를 없애고 로그인된 path 경로 페이지 확인
  • (path가 없는 경우) 협의된 URL 스키마를 호출하여 로딩 인디케이터를 없애고 로그인된 메인 페이지 확인

path가 있는 경우 처음엔 window.location.href를 통해 이동시켰지만, 사용자가 path 경로 페이지에서 뒤로가기를 하는 경우 예기치 않은 버그가 생기기도 했다. 그래서 href가 아닌 replace로 수정하여 별도의 히스토리가 남지 않도록 하였다.

if(saved_mall_connect_type && saved_path) {
    // window.location.href = `${MUBEATMALL_BASE_URL}${saved_path}`;
    window.location.replace(`${MUBEATMALL_BASE_URL}${saved_path}`);
    localStorage.removeItem('path');
}

CustomEvent를 통한 로그아웃 분기 처리

앱에서는 로그아웃을 막기 위해 버튼을 숨기려 CustomEvent를 사용하기도 했다. DOMContentLoaded 대신 CustomEvent인 MallConnectTypeSet이 발생했을 때 필요한 로직을 처리하여 좀 더 명확한 연결을 하기 위해서였다. 자세한 히스토리는 이 글에 별도로 정리해두었다.

<script>
    document.addEventListener('DOMContentLoaded', () => {
        const queryParams = new URLSearchParams(window.location.search);
        const mallConnectType = queryParams.get('mall_connect_type');

        if(mallConnectType) {
            localStorage.setItem('mallConnectType', mallConnectType);
        }

        document.dispatchEvent(new CustomEvent('MallConnectTypeSet')); // 수신하기 위한 커스텀 이벤트
        ...
    });
</script>
<script>
    document.addEventListener('MallConnectTypeSet', () => {
        const saved_mall_connect_type = localStorage.getItem('mallConnectType');
        const logoutButton = document.getElementById('customSidebarLogout');
        const slideMultishopList = document.getElementById('slideMultishopList');

        // 앱에서 접근했을 때 로그아웃, 언어 숨김 처리
        if(saved_mall_connect_type === 'app' && logoutButton && slideMultishopList) {
            logoutButton.style.display = 'none';
            slideMultishopList.style.display = 'none';
        }
    });
</script>

MutationObserver를 사용한 이미지 변경

카페24 내부 모듈을 사용하는 경우 소스에 직접 접근할 수 없어서 DOM이 그려진 후에 DOM 조작을 통해서만 변경이 가능했다. 특정 버튼의 이미지를 변경하고자 하는 디자이너의 요청이 있어서 방법을 찾던 와중에 MutationObserver를 사용하기로 했다. DOM 생성 시점에 이미지를 변경하는 데 가장 적합한 방법이라고 판단했고, 그 시점에 img tag의 src, width, height의 값을 변경했다.

const initializeMutationObserver = () => {
    const targetNode = document.body;

    const callback = (mutationsList, observer) => {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {

                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        const optionBoxUps = node.querySelectorAll('p img.option_box_up');
                        const optionBoxDowns = node.querySelectorAll('p img.option_box_down');

                        if(optionBoxUps.length > 0 && optionBoxDowns.length > 0) {
                            optionBoxUps.forEach(img => {
                                img.src = '//ecimg.cafe24img.com/pg1231b85688535093/vlending/web/upload/goodymall/common/btn_up_test.png';
                                img.width = '28';
                                img.height = '28';
                            });

                            optionBoxDowns.forEach(img => {
                                img.src = '//ecimg.cafe24img.com/pg1231b85688535093/vlending/web/upload/goodymall/common/btn_down_test.png';
                                img.width = '28';
                                img.height = '28';
                            });
                        }
                    }
                });
            }
        }
    };

    const observer = new MutationObserver(callback);

    const config = {
        childList: true, // 자식 노드 관찰
        subtree: true   // 하위 트리 관찰
    };

    observer.observe(targetNode, config);
}

KEEP

당장 눈앞에 놓인 방법으로 작업하지 않고 주도적으로 개발 환경을 만들어 나간 건 좋은 결정이었다. 카페24 같은 경우 스마트디자인 편집창을 통해 편집하는 경우가 많고 IDE를 통해서 작업하는 건 관련 자료를 찾기가 좀 까다로웠는데, SFTP를 통해 배포를 하지만 기존 환경과 흡사하게 세팅해 두는 편이 다른 개발자가 보기에도 인지하기 쉽다는 판단이었다.

카페24 스킨 수정 작업 경험이 있었지만, 모듈을 추가하거나 이번 작업처럼 수정 범위가 큰 경우는 드물었기에 다소 우려가 있었다. 차분하게 파악하니 어드민과의 결합부, 별도의 모듈 선언부 등 하나씩 파악할 수 있었고 다행히 큰 무리 없이 작업할 수 있었다. 또한 당장 빠르게 수정하는 것보다 유지보수에 이롭거나 사이드이펙트가 적게끔 작업하였다.

앱 개발자와의 소통을 통해 요구사항을 정확히 구현했다. 협의된 URL 스키마를 적절한 시점에 작성하고, localStoragequery parameter를 활용하여 특정 값을 통해 분기 처리를 구현했다.

PROBLEM

localStorage에 값을 담고 계속 해당 값을 들고 있다 보니 예기치 못한 버그가 생기는 경우가 있었다. 값이 늘어날수록 생성 시점을 명확히 인지하지 못하는 경우도 있었고, 해당 값을 앱 클라이언트에서 호출해서 생기는 버그는 내가 바로 파악하기도 어려웠다.

별도의 슬랙 채널을 만들어 소통 비용을 줄이려 했지만, 초기에 모든 것이 완벽히 정해지지 않아서 꽤 많은 소통 비용이 생길 수밖에 없었다. 물론 필요한 소통 비용을 억지로 줄일 순 없지만 히스토리 관리나 간결한 정리 등을 매끈하게 해내지는 못했다.

TRY

특정 시점에 localStorage 값을 만들었다면 그 값을 짧게만 사용하거나 필요 없어지는 시점에는 삭제를 해주는 편이 좋다. 이벤트를 생성하고 해제해 주지 않으면 메모리 누수가 생기듯 불필요한 localStorage 값을 계속 들고 있다는 건 버그가 생길 여지를 만드는 것과 같다.

예상되는 소통 비용을 마음의 준비만 하지 않고, 가급적 가능한 방향으로 불필요한 소통 비용을 줄일 만한 고민도 해봄직하다.

결론

후… 너넨 이런 걸로 개발하지 마라…

참고자료

profile
평소엔 책과 영화와 음악을 좋아합니다. 보편적이고 보통사람들을 위한 서비스 개발을 꿈꾸고 있습니다.

0개의 댓글