함께 공부하는 즐거움을 주어 공부에 지속적인 동기부여 를 주고,
게임화(Gamification)를 더하여 경쟁심을 자극해 자발적인 공부를 유도 하며,
자기주도 학습능력 향상을 통한 목표지향적 능력 개발 도모 하는 서비스를 만들고 싶었습니다.
공부포인트를 쌓아 물고기캐릭터도 모으고, 랭킹 시스템을 통해 재밌게 공부해보세요!
SeaTUDY 바로가기
팀노션 바로가기
8기 5조 - '역전의 명수' GitHub
FrontEnd GitHub
BackEnd GitHub
MVP중간발표 영상
프로젝트 최종발표영상
2022/8/26 - 2022/10/7(6주간)
사용 기술 | 기술 설명 |
---|---|
TypeScript | 코드에 목적을 명시하고 목적에 맞지 않는 타입의 변수나함수들에서 에러를 발생시켜 버그를 사전에 제거하기위해 도입하였습니다. |
Redux | 코드에 목적을 명시하고 목적에 맞지 않는 타입의 변수나 함수들에서 에러를 발생시켜 버그를 사전에 제거하기위해 도입하였습니다. |
Redux-toolkit | 코드에 목적을 명시하고 목적에 맞지 않는 타입의 변수나 함수들에서 에러를 발생시켜 버그를 사전에 제거하기위해 도입하였습니다. |
Axios | 별도의 모듈을 설치해야하지만 브라우저 호환성과 자동으로 JSON데이터 형식으로 바꿔준다는 점,promise API를 사용할 수 있다는 점을 고려햐여 도입하였습니다. |
Styled-components | 조건부 스타일링과 CSS-in-JS 방식으로 자바스크립트 환경을 최대한 활용하기 위해 도입했습니다. |
Stomp | 프론트엔드와 백엔드의 효율적인 협업을 위해, 자동배포를 진행했습니다. |
Redis | 각 채팅방에 입장한 유저들의 인원수를 카운트하기 위해 세션ID와 채팅방 정보를 저장해둘 필요가 있었고, RDS를 사용하기에는 잦은 채팅방 입장/퇴장 이벤트가 일어나기 때문에 쿼리문을 계속해서 보내줄 필요없는 in-memory DB ‘Redis’가 최적이라고 생각하였습니다. |
Oauth 로그인 | 사용자들이 회원가입의 번거로움을 덜고, 편리하게 서비스를 이용할 수 있도록 하기위해 사용하였습니다. |
Nginx | DDos와 같은 공격으로부터 보호하고(Https SSL 인증서), 좀 더 빠른 응답을 위해 적용하였습니다. |
Github Actions | 프론트엔드와 백엔드의 효율적인 협업을 위해, 자동배포를 진행했습니다. |
MySQL | 프론트엔드와 백엔드의 효율적인 협업을 위해, 자동배포를 진행했습니다. |
타이머는 헤더에 존재하고 타이머 버튼은 형제컴포넌트인 메인페이지에 존재한다. 타이머를 작동시키기 위해 리덕스에 isStudy란 state를 만들고 이 조건을 이용해 타이머를 작동시켰다.
또한 새로고침시에도 체크인 상태라면 서버에서 isStudy :true 값을 받아 타이머가 자동으로 시작해야하는 문제를 해결하였다.
타이머는 setInterval을 사용하였는데 처음엔 서버에서 받아오는 시,분,초를 각각 setInterval을 이용해 시간을 늘렸으나 시간이 흐를수록 각각 오차가 생겨 아예 다른 시간이 되는 문제가 생겼다.
setInterval의 오차를 없애기 위해 시,분,초 중에 초만 setInterval을 이용하여 1초에 한번씩 +1 을 해주고 분은 초가 60씩 돌때마다(60의 배수가 될때마다) +1 을 시켜준다.
마찬가지로 시 또한 분이 60씩 돌때마다(60의 배수가 될때마다) +1 을 시켜주었다. 즉 setInterval은 초만 이용하여 오차를 없앨수 있었다.
카테고리를 추가하고 투두리스트 입력창을 열어 카테고리에 맞는 투두리스트를 추가할 수 있다.
하지만 .map() 함수를 이용해 그리고 있어 하나의 투두리스트 입력창만 열고 싶지만 모든 투두리스트 의 입력창이 열린다. 이를 해결하기위해 처음으로 .map()의 Index 파라미터를 제대로 사용해보았다.
const [todoInputShow, setTodoInputShow] = useState<any>([ false, false, false, false, ]);
function onSubmitHandler() { if (dateTodos.length < 4) dispatch(__postCategory({ categoryName: category, selectDate: date })); else { alert("4개까지만 생성가능"); } setCategory(""); }
function todoBoxIndex(index: number) { let temp = [...todoInputShow]; temp[index] = !temp[index]; setTodoInputShow(temp); }
물고기 이미지는 map을 이용해 그리고 있다. 당연하게도 물고기 한마리씩 드래그 되지 않고 모두 한번에 움직였다. 이것을 해결하기 위해 지난 주 사용했던 index 파라미터를 이용해야겠다 생각했다. 25개의 초기값 0,0 (물고기의 위치가 될 좌표 left,top)배열을 가진 state를 만들어주었고 이곳에 물고기 하나씩 데이터를 저장해주며 인라인스타일로 물고기의 position left,top에 그대로 넣어준다.
const [fishPos, setFishPos] = useState( Array.from({ length: 25 }, (v, i) => { return [0, 0]; }) );
function dragHandler (e: any, i: number) { let tempData = [...fishPos]; tempData[i][0] = e.target.offsetLeft + e.clientX - clientPos.x; tempData[i][1] = e.target.offsetTop + e.clientY - clientPos.y; setFishPos(tempData); const clientPosTemp = { ...clientPos }; clientPosTemp["x"] = e.clientX; clientPosTemp["y"] = e.clientY; setClientPos(clientPosTemp); };
function dragEndHandler (e: any, i: number) { let tempData = [...fishPos]; tempData[i][0] = e.target.offsetLeft + e.clientX - clientPos.x; tempData[i][1] = e.target.offsetTop + e.clientY - clientPos.y; setFishPos(tempData); dispatch( __postFishPosition({ fishNum: i, left: fishPos[i][0], top: fishPos[i][1], }) ); // 캔버스 제거 const canvases = document.getElementsByClassName("canvas"); for (let i = 0; i < canvases.length; i++) { let canvas = canvases[i]; canvas.parentNode?.removeChild(canvas); } // 캔버스로 인해 발생한 스크롤 방지 어트리뷰트 제거 document.body.removeAttribute("style"); document.body.style.overflow = "hidden"; };
<InvenLayout ref={containerRef}> {fishImages.map((data: any, i: number) => { return ( <Label key={i}> <FishImg draggable={userPoint >= data.point ? true : false} onDragStart={(e) => dragStartHandler(e)} onDrag={(e) => dragHandler(e, i)} onDragOver={(e) => dragOverHandler(e)} onDragEnd={(e) => { dragEndHandler(e, i); }} style={{ left: positionData.find((x) => x.fishNum === i)?.left === 0 ? "0.5em" : positionData.find((x) => x.fishNum === i)?.left, top: positionData.find((x) => x.fishNum === i)?.top === 0 ? "0.5em" : positionData.find((x) => x.fishNum === i)?.top, }} src={data.image} alt="" />
물고기 좌표(positionData)를 그대로 서버에 저장하고 받아와서 받아온 데이터를 그대로 물고기 position에 넣어주었지만 넣는 순간 내가 서버로 보내는 물고기 좌표값 자체가 이상하게 틀어진다. 이를 해결하기 위해 서버에서 받아온 데이터를 다시한번 State에 저장해 해결했다.
useEffect(() => { let tempData = [...fishPos]; positionData.map((v) => { tempData[v.fishNum] = [v.left, v.top]; }); setFishPos([...tempData]); }, [positionData]);
<Label key={i}> <FishImg draggable={userPoint >= data.point ? true : false} onDragStart={(e) => dragStartHandler(e)} onDrag={(e) => dragHandler(e, i)} onDragOver={(e) => dragOverHandler(e)} onDragEnd={(e) =>dragEndHandler(e, i)} style={{ left: fishPos[i][0] === 0 ? "0.5em" : fishPos[i][0], top: fishPos[i][1] === 0 ? "0.85em" : fishPos[i][1], }} src={data.image} alt="" onContextMenu={(e) => FishDeleteHandler(e, i)} />
채팅에 글자마다 혹은 send 등 이벤트가 일어날때마다 소켓이 실행된다.
useEffect와 의존성배열을 이용해보기도하고 데이터를 저장하는 onChange 함수를 최적화 시도했지만 채팅을 보낼때엔 무조건 이벤트 즉 렌더링이 일어나고 소켓이 실행되었다.
소켓을 실행하는 함수를 함수 바깥에서 실행해서 페이지가 렌더링 되더라도 한번만 연결되게 수정하였다.
audio.play()가 최초엔 실행이 되지만 audio.pause() 함수가 정상적으로 작동하지 않는다.
처음엔 api를 받아오는 무료 라이센스 서버에서의 통신이 오래걸려 타임아웃 에러가 난다고 생각하여 우리 백엔드 서버에 오디오를 저장해 불러왔으나 역시나 실패했다.
생각보다 나와 같은 문제를 겪는 글들이 굉장히 많았다. 스택오버플로우를 참조하여 URL을 바로 변수에 할당하는 것이 아닌 useState를 이용해 저장하여 사용하였고 문제가 해결되었다.
const Router = () => { const token: string = process.env.REACT_APP_TOKEN as string; // const token: string = getCookie("token") as string; return ( <BrowserRouter> <Header /> <Routes> <Route path={EnumPages.HOME} element={<PrivateRoute token={token} component={<Home />} />} /> <Route path={EnumPages.INTRO} element={<Intro />} /> <Route path={EnumPages.MAIN} element={<PrivateRoute token={token} component={<Main />} />} /> <Route path={EnumPages.CHATROOM} element={<PrivateRoute token={token} component={<ChatRoom />} />} /> <Route path={EnumPages.STATISTICS} element={<PrivateRoute token={token} component={<Statistics />} />} /> <Route path={EnumPages.UNLOCK} element={<PrivateRoute token={token} component={<UnLock />} />} /> <Route path={EnumPages.LOGIN} element={<Login />} /> <Route path={EnumPages.KAKAOLOGIN} element={<KakaoLogin />} /> <Route path={EnumPages.NAVERLOGIN} element={<NaverLogin />} /> <Route path={EnumPages.GOOGLELOGIN} element={<GoogleLogin />} /> <Route path={EnumPages.WAVE} element={<Wave />} /> </Routes> </BrowserRouter> ); }; export default Router;
PrivateRoute({ token, component: Component }) { return token ? (Component) : (<Navigate to="/login" {...alert("로그인이 필요한 페이지입니다.")} />); } export default PrivateRoute;
프라이빗라우터를 사용하여 토큰이 있어야 홈 화면에 입장이 가능한데 로그인시 토큰을 저장해도 페이지 접근이 불가능했다.
setTimeout을 이용해 토큰을 저장하고 몇초 후 페이지에 접근하게 만들어 보았으나 실패 , 이후 디버깅 결과 로그인을 하기 전에도 라우터는 렌더링되므로 이미 토큰을 읽고 undefind 가 들어간 상태이다. 로그인을 해서 토큰을 저장해도 토큰이 업데이트 되지 않았다.
라우터에서 검증을 삭제하고 페이지 컴포넌트에서 쿠키에 저장한 토큰을 불러와 useEffect를 이용 토큰이 없을경우 접근이 불가능하게 만들었다.
useEffect(() => { if (token === undefined) { navigate("/login"); alert("로그인이 필요한 페이지입니다."); } document.body.style.overflow = "hidden"; }, [token]);
위의 프라이빗라우터와 마찬가지인 이유로 로그인 단계에서 이미 렌더링이 일어나 헤더에 있는 통신들이 useEffect에 의해 시작된다. 하지만 아직 로그인을 하여 토큰을 저장하지 않았으므로 token은 undefind 가 되어 당연하게 에러가 난다.
위의 프라이빗라우터를 해결하며 힌트를 얻어 로그인전 불필요한 통신과 에러를 방지하기 위해 useEffect에 조건과 의존성배열에 token을 추가해 에러를 없앴다.
useEffect(() => { if (token !== undefined) { dispatch(__getDayMyRank()); dispatch(__getWeekMyRank()); dispatch(__getUserProfile()); return () => { dispatch(__getCheckOutTimer()); }; } }, [token]);
각 채팅방에 입장한 인원수 카운트를 할 때, 만약 한 유저가 두 브라우저를 띄우고 같은 채팅방에 입장시 2명으로 인원수가 카운트 되어지는 문제
중복허용이 되지 않는 Set을 이용: 입장시 세션ID(키)와 닉네임(밸류)을 저장한 후, 닉네임(밸류)을 기준으로 Set에 저장하여 중복카운트 방지 → 입장은 중복없이 카운트가 되었으나, 퇴장시 아직 하나의 브라우저가 남아있음에도 인원이 -1명이 됨(유저가 여전히 채팅방에 있음에도 나간 것으로 확인)
입장시, 1번과 동일하게 value 기준으로 Set을 만들어 카운트(중복제거)하고, 퇴장시 세션ID(키)를 찾아 제거 후 다시 value기준 Set으로 카운트
공부시간 기록서비스이다보니 자정을 기준으로 하루를 초기화하게되면 유저가 공부하는 중간에 체크인/체크아웃을 다시 해줘야하는 번거로움이 발생하는 경우가 많음
(팀회의를 거쳐 항해의 시스템과 동일한 새벽5시에 하루초기화 결정)
만약 자정이 넘은 시간에 체크인을 하게 된 경우, 체크인 시간값에서 -5시간을하여 DB에 저장하여 같은 하루의 공부량으로 기록하도록 함.
하지만, 체크인/아웃을 할 때마다 -5시간을 적용해줘야해서 성능의 저하가 우려
서버시간자체를 자정에 하루를 초기화하는 것이 아닌 오전5시에 초기화되도록 로직 수정. 체크인/체크아웃 시간에서 -5시간을 할 필요없이 현재 시간 자체를 DB에 저장하여도 같은 하루로 설정되도록 함
자동배포 성공 후에도 서버가 작동하지 않는 문제
GitHub Action으로 자동배포시에 .gitignore 로 설정된 application.properties 파일은 커밋되있지 않기 때문에 빌드시에 값들이 지정되지 않아서 작동이 되지않음.
따라서, GitHub 시크릿에 값을 하나하나 설정하여 빌드 때에 이 설정값들이 지정될 수 있도록 하여 빌드성공. 하지만, 설정값이 많은데 일일이 값을 시크릿 설정으로 해줘야하는 번거로움이 있음
GitHub 시크릿에 하나하나 설정값을 지정하지 않고 전체 설정값을 GitHub 시크릿에 저장해두고, CI/CD를 진행할 때 application.properties 파일을 생성한 후 이 값 전체를 넣어주도록 설정함
- 컴포넌트의 분리.
- React.memo()를 사용한 메모이제이션.
- 리렌더링이 자주 일어나지 않는다면 굳이 사용할 필요가 없다. (메모리에 불필요하게 남아있을 필요 없음)
- 흩어진 useEffect 의존성배열 활용.
- 온클릭시 일어나는 함수는 화살표함수에서 funtion 함수로 변경.
- 화살표 함수, 특히 축약형 화살표 함수는 기존 함수에 비해 실행하는데 더 많은 시간이 걸린다.
- 불필요한 통신이 일어나는건 전부 막기.
- debounce와 throttle를 이용한 최적화.
이름 | 포지션 | 분담 | GitHub |
---|---|---|---|
이중표🔸 | FE, DE | 소셜로그인, 체크인체크아웃시스템, 달력모달(Calendar & TodoList & D-day), 통계페이지, 물고기드래그앤드랍, Asmr기능 | https://github.com/leejpsd |
은예찬 | FE, DE | 실시간채팅, 체크인체크아웃시스템, 도감페이지, 프로필관리, 프라이빗라우터 | https://github.com/eunyechan |
유동건 | FE | 기술고문 | https://github.com/peppermintt0504 |
김명수🔸 | BE | Todo카테고리 조회/생성/수정/삭제, TodoList 조회/생성/수정/삭제, 물고기 위치 변경/조회 | https://github.com/PaulKim330 |
김혜림 | BE | 소셜로그인(카카오/네이버/구글), 실시간채팅, CI/CD, 엔티티 연관관계설정 | https://github.com/hlim9022 |
박민정 | BE | Todo카테고리 조회/생성/수정/삭제, TodoList 조회/생성/수정/삭제 | https://github.com/minjpark3 |
박민정 | BE | D-day 조회/생성/수정/삭제, 체크인/체크아웃 기능, 전체랭킹(주간/일간) & 개인랭킹(주간/일간) 조회 | https://github.com/ghwo68 |
A study schedule is a time-management plan that will help you achieve your learning goals. In this plan, you'll schedule your study sessions, as you would your work or social commitments. By setting aside dedicated time to study, you'll be able to break down tasks and assignments into manageable chunks. Monkey App
멋있습니다!! 응원하겠습니다 ㅎㅎ