- #Project
- #Burgerput
Burgerput의 로딩 로직
Prolip
2024-04-11
우리 프로젝트는 어떻게 데이터베이스를 업데이트하고 있을까?
시작..
이전에 포스팅한 Burgerput 프로젝트에 대한 배경 지식을 조금 더 설명하고 오늘 포스팅을 본격적으로 시작해보려 합니다..
프로젝트의 근본적인 목표
앞서 작성한 글에 적혀있듯 우리 프로젝트는 입력 값 검증을 통해 오기입률을 0%로 만드는 것이 근본적인 목표였습니다.
아하! 그럼 사용자에게 입력할 항목과 온도 범위를 제공하면 되겠군요?? 후에 사용자가 입력한 값을 검증해주면 되는 아주 심플한 프로젝트네요!
그러러면 데이터베이스에 매장에서 사용하는 기기 장비 및 식품 데이터를 입력해야겠어요!
..그런데 문제가 벌써 존재합니다.
주기적으로 추가되는 신메뉴
뭐.. 기기 장비의 경우 3년을 일하면서 바뀐 적이 없습니다. 한 번 시스템에 등록해놓고 추가, 변경이 생길 경우에 값을 업데이트 해줘도 될 정도입니다.
그런데 식품의 경우 전혀 그렇지 않습니다.
버거X에서는 주기적으로 신메뉴가 나오고, 이전에 출시된 제품은 드랍되어 항목이 수시로 변경됩니다.
물론 변경될 경우 데이터베이스를 직접 업데이트 해줘도 되는 문제일 수도 있습니다.
하지만 개발하는 인원들이 매장을 그만두고, 출시되는 메뉴에 대한 소식이 끊기게 되면 업데이트는 중지될 것이며 해당 웹 어플리케이션을 사용하는데에는 문제가 발생할 것입니다.
어떻게 해결했나요??
결론부터 말씀드리자면 셀레니움을 이용해 Zenput 사이트 내의 기기 장비 및 식품 목록을 크롤링하고, 그 값들을 우리의 데이터베이스에 저장하여 사용자에게 제공하고 있습니다.
Selenium은 웹 애플리케이션 자동화 및 테스트를 위한 프레임워크입니다.
다른 웹 크롤러도 존재하지만 그 중 셀레니움을 선택한 데에는 이유가 몇 가지 존재합니다.
- Zenput의 기기 장비 및 식품 목록을 크롤링하기 위해선 제출 페이지 내에서 모든 목록을 크롤링해야만 합니다. 그 과정까지 로그인이 필요한데 셀레니움을 이용해 로그인하고 있습니다.
- 웹 페이지가 모든 렌더링을 마친 후에 해당 웹 페이지의 데이터를 크롤링하기 위함이 있습니다.
- 사용자가 값을 모두 입력한 후 제출할 때 xpath 등을 이용해 해당 요소에 값을 입력한 후 클릭 이벤트를 이용해 제출 처리하고 있습니다.
위의 이유들로 셀레니움을 선택했습니다.
그럼 추가되는 요소들은요??
주기적으로 추가되는 요소에 대한 문제점을 해결하기 위해 시도한 방법은 총 3가지입니다.
최종적으로 채택해 사용 중인 방법은 쉘 스크립트를 작성해 crontab에 등록해 자동화시키는 방법입니다.
해당 방법을 소개하기에 앞서 첫 번째와 두 번째로 시도한 방법들을 왜 사용하지 않았는지 문제점을 다시 짚어보는 시간을 가지려 합니다..
첫 번째 시도 - Cookie
가장 먼저 시도한 방법은 서버 단에서 오늘 날짜에 대한 쿠키를 생성하여 쿠키가 존재하는지 판단, 만약 쿠키가 없다면 크롤링을 통한 로딩 로직을 실행한다.. 였습니다.
- 웹 어플리케이션에 진입하면 서버 단에선 쿠키가 브라우저에 존재하는지 판단한다.
- 쿠키가 존재하지 않거나, 존재한다면 쿠키의 날짜가 오늘 날짜와 부합하는지 판단한다.
- 위의 판단을 통해 로딩 로직을 실행한다.
이런 구조였습니다.
그런데 해당 기능의 문제점은 웹 페이지에 진입할 때 브라우저에 쿠키를 저장해야만 하며, 사용자가 웹 페이지 어느 곳에 접근하던 실행되는 과정이기에 서버에 부담이 갈 수 밖에 없었습니다.
또한 그 당시 서버단에서 만든 쿠키를 브라우저에 저장하였으나 해당 쿠키를 인식하지 못해 무한 로딩을 경험하며 처참하게 약 1주일하고도 절반 정도를 고통 받았습니다..
덕분에 로딩에 대한 기능을 뒤로한채 우선 나머지 기능들을 완성 시키자고 판단하였던 기억이 납니다..
다시 생각해보면..
페이지 처음 마운트 될 때 API 호출로 쿠키 저장하고, 그 때 조회해서 로딩을 돌려도 좋지 않나? 싶기는 한데 사실 사용자 관점에선 크롤링을 위해 로그인하고 해당 페이지를 크롤링하는 시간을 기다리는건 지루할 수 밖에 없다고 판단 되기도 합니다..
두 번째 시도 - 클라이언트 단에서 날짜 체크하기
개발이 거의 완료된 이후에 다시 이 로딩에 대해 이야기했던 기억이 납니다.
만들어가며 스스로 성장을 했는지 이전엔 생각해보지 못했던 여러가지 선택지가 보이더군요..
그 중 한 가지가 지금 설명해드릴 클라이언트 단에서 날짜를 체크하는 방법입니다.
무엇을 고려하며 구현했는지 적어보겠습니다.
1. 우리 웹 어플리케이션에 처음 진입할 때만 로딩을 실행
위의 조건에 부합하기 위해선 단순하게 useEffect를 생각했습니다.
의존성 배열을 비워두면 처음 마운트 될 때만 실행되니까요..
2. 지루하지 않게
한 가지 배경을 설명 드려야 됩니다.
셀레니움 웹 드라이버로 웹 크롤링 진행한다면 로컬 상에선 눈에 보이게 브라우저를 통해 실행이 됩니다만, 리눅스 환경에서 실행할 때에는 headless라는 옵션을 이용해 화면을 띄우지 않게 설정합니다.
그렇기 때문에 사용자는 크롤링이 진행 중인지, 혹은 끝났는지를 판단할 수 없기 때문에 로딩 스피너를 사용하게 됐습니다.
3. 불필요하게 서버와 통신하지 않게
/loading 이라는 API 엔드포인트를 이용해 서버로 로딩을 요청하고 있습니다.
만약 페이지가 마운트 될 때마다 항상 요청을 보낸다면 불필요할 것이라고 판단했습니다.
더불어 Zenput은 하루에 오전, 오후 총 두 번 입력하게 되어있습니다.
그렇기 때문에 오전에 방문한 사용자가 오후에 또 로딩 화면을 본다면 사용자 경험 좋지 않을 뿐더러 불필요한 과정이라고 판단해 당일 로딩 기록이 존재한다면 로딩을 실행하지 않도록 구현하려 했습니다.
그래서 제가 생각한 방법입니다.
- 서버에 로딩을 요청할 때 추가로 오늘 날짜를 년-월-일까지만 잘라서 localStorage에 저장한다.
- 후에 페이지가 다시 마운트 되어 useEffect이 실행된다면?
- 함수가 실행되어 서버에 로딩을 요청하기 이전에 현재 날짜에 해당하는 변수를 생성하고, localStorage에 저장되어 있는 날짜를 꺼내 비교한다.
- 만약 저장된 날짜가 현재 날짜와 같다면 실행을 종료한다.
- 혹은 현재 날짜가 더 빠르다면(하루가 지났다면) 1번을 실행.
이렇게 생각을 하고 구현하게 되었습니다.
4. 매장 오픈 시간은 오전 8시
매장은 항상 8시에 오픈합니다.
그렇기 때문에 만약 날짜가 지났더라도 8시 이전에 접속했다면 로딩이 되지 않도록 구현하려 했습니다.
이유는 마감 시간인 자정 시간을 넘겨 새벽 시간에도 입력을 할 경우가 존재하기 때문입니다.
- 페이지가 마운트 되어 useEffect의 함수가 실행된다.
- 날짜를 체크하기 이전에 가장 먼저 현재 시간이 오전 8시를 넘었는지 판단한다.
- 오전 8시 이전이라면 종료
- 오전 8시 이후라면 날짜를 체크하는 함수 실행
종합해보자면..
// useDateCheck.jsx import { useState } from "react"; import { getCurrentItems } from "../api/Loading"; export function useDateCheck() { const [loading, setLoading] = useState(false); const [result, setResult] = useState(false); // 현재 날짜 객체를 반환하는 함수 function setCurrentDate() { return new Date(); } // 오전 8시에 해당하는 시간을 설정해 반환하는 함수 function getComparisonDate() { const comparisonDateHour = new Date(); comparisonDateHour.setHours(8, 0, 0, 0); // 시, 분, 초, 밀리초 설정 return comparisonDateHour; } // 8시 이전, 이후를 비교해 boolean 값을 반환하는 함수 function isAfter8AM() { const currentDate = setCurrentDate(); const comparisonDate = getComparisonDate(); return currentDate.getTime() >= comparisonDate.getTime(); } // 현재 시간을 년,월,일로 배열에 저장해 반환하는 함수 function getCurrentDate() { const currentDate = setCurrentDate(); const year = currentDate.getFullYear(); const month = +("0" + (1 + currentDate.getMonth())).slice(-2); const date = +("0" + currentDate.getDate()).slice(-2); return [year, month, date]; } // 로컬 스토리지에 현재 시간을 저장하고, loading 페이지로 이동하는 함수 function saveCurrentDate() { setLoading(true); const currentDate = getCurrentDate(); localStorage.setItem("currentDate", JSON.stringify(currentDate)); getCurrentItems() .then((res) => { !res && setResult(true); }) .finally(() => setLoading(false)); } // 날짜 비교 함수 function checkDate() { const savedDate = JSON.parse(localStorage.getItem("currentDate")); const currentDate = getCurrentDate(); // 저장된 날짜 배열을 reduce로 순회함 현재 날짜가 저장된 날짜 보다 년,월,일 중 하나라도 클 경우 result에 1을 추가함 const result = savedDate ? savedDate.reduce((result, date, idx) => { if (date < currentDate[idx]) { return (result += 1); } return result; }, 0) : 1; if (result === 0) { return; } if (result > 0) { if (isAfter8AM()) { saveCurrentDate(); } else { return; } } } function reLoad() { saveCurrentDate(); } return { checkDate, reLoad, setResult, loading, result }; } // App.jsx function App() { const { checkDate, reLoad, setResult, result, loading } = useDateCheck(); useEffect(() => { checkDate(); }, []); return ( {loading && ( <Banner type={"loading"} text={ <section> 로딩스피너~~ </section> } /> )} {result && ( <Moadal title={"로딩 실패"} component={"로딩에 실패했습니다. 다시 시도하시겠습니까?"} setResult={setResult} error={true} submit={reload} /> )} ) }
사용하는 기능에 따라 분리해 함수로 만들었으며, 훅으로 만들어 관리하고 있습니다.
혹시라도 년, 월, 일 중 무엇 하나라도 빨라진다면 result에 1을 추가하도록 구현해봤는데 reduce는 정말 만능이구나.. 싶습니다.
그런데 문제점..
사실 구현은 잘 됐으며 한 일주일 정도는 잘 사용했습니다.
다만.. 이용자들의 피드백으로 로딩 스피너도 귀엽고 좋은데 기다리는 시간이 길다는 것이 문제였습니다.
기기 장비 및 식품 데이터를 크롤링하기 위해서 진입하는 과정이 다소 길다는 것이 문제였습니다.
로그인, 해당 페이지까지 클릭하며 들어가는 과정을 무시할 수 없었기에 다른 방법을 찾게 되었습니다.
마치며..
이번 글에서 마지막 세 번째 시도인 cron과 curl에 대해 적으려하니 호흡이 너무 길어져 끊고 다음 글로 이어가려 합니다..
다음 글에서 최종적으로 채택한 cron과 curl을 이용한 방법을 기록해보려 합니다..