✨ Version 2️ ✨ | Version 1️ | |
---|---|---|
팀원 구성 | FE 1 | FE 2, BE 4 |
구현 기간 | 23.08.26 ~ 23.09.01 (6일) | 23.05.05 ~ 23.05.11 (7일) |
담당 기능 |
- 클라이언트&서버 1인 프로젝트로 구현 - JWT Token을 이용한 회원가입/로그인 - 메인페이지/헤더 - 게시글 CRUD |
- 프론트엔드 리딩 및 전반적인 UI 구현 전담 - JWT Token을 이용한 회원가입/로그인 - 메인페이지/헤더 - 댓글 CRUD |
관련 링크 | refactoring branch (← click!) | before_main branch (← click!) |
✨ React 학습 직후의 첫 협업 프로젝트인 Version 1의
코드 개선 및 CRUD의 TypeScript 적용을 위해 Version 2를 구현하였습니다.
구분 | ✨ Version 2️ ✨ | Version 1️ |
---|---|---|
Library |
|
|
Programming Language |
|
|
Styling |
|
|
State Management |
|
|
Server & DataBase |
|
|
Formatting |
|
|
Version Control |
JavaScript는 변수의 타입을 자동으로 추론해주지만, 때로는 예기치 않은 동작을 할 수 있습니다.
TypeScript는 정적 타입 시스템을 제공해 런타임에 발생하는 타입 관련 버그를 사전에 방지할 수 있으며,
명시적인 타입 어노테이션을 사용해 코드의 가독성을 향상시킵니다.
따라서 기존의 JavaScript가 아닌, TypeScript의 추가적인 학습을 위해 선정하였습니다.
Styled-Components는 CSS-in-JS이므로 컴포넌트 단위로 스타일을 관리할 수 있으며
JavaScript 표현식을 이용해 컴포넌트의 상태나 데이터에 따라 스타일을 동적으로 변경할 수 있습니다.
또한 인라인으로 코드를 작성하는 테일윈드보다 코드 가독성이 더 좋다고 판단하였으며,
꾸준히 사용했던 경험이 있으므로 빠른 구현을 위해 Styled-Components를 선정하였습니다.
ver.1의 기존 코드는 Redux의 추가적인 학습을 위해
Redux의 복잡성을 낮춘 Redux Toolkit을 사용하여 상태 관리를 진행했습니다.
ver.2를 새롭게 구현하며 액션이나 리듀서를 사용하지 않고 atom을 기반으로 한 상태관리의 학습과,
빠른 구현을 위해 상대적으로 간단하게 상태를 관리할 수 있는 Recoil을 선정하였습니다.
Axios를 이용해 HTTP 네트워크 통신이 가능하지만 서버 상태관리나 캐싱을 직접 지원하지 않습니다.
캐싱은 이전의 데이터나 리소스를 임시로 저장해 다음 요청 시 저장된 데이터를 사용함으로써
서버 부하를 줄이고 성능을 향상시키므로, 이러한 기능을 지원하는 Tanstack-Query를 함께 사용했습니다.
짧은 기간 내에 1인 프로젝트를 구현하기 위해 러닝커브가 높지 않고 간결한 Python과,
미니멀한 기능을 제공하는 프레임워크인 Flask를 선정하였습니다.
또한 Flask와 MongoDB는 모두 JSON 데이터 형식을 사용하여 클라이언트단과의 통신이 용이하므로
빠른 개발을 위해 MongoDB를 선정하였습니다.
타입을 잘 넘기고 있는지, 설정해둔 컨벤션을 준수하고 있는지 체킹해주는
ESLint와 Prettier를 지정하여 코드 컨벤션을 일관성있게 유지하고,
타입 에러나 불필요한 변수의 사용 등을 방지할 수 있었습니다.
💡 회원가입
![로그인 회원가입 화면스위치](https://private-user-images.githubusercontent.com/84097192/265241230-172f2377-985e-46b4-ab1d-e6794e33af89.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQ1MzkyNzEsIm5iZiI6MTcyNDUzODk3MSwicGF0aCI6Ii84NDA5NzE5Mi8yNjUyNDEyMzAtMTcyZjIzNzctOTg1ZS00NmI0LWFiMWQtZTY3OTRlMzNhZjg5LmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA4MjQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwODI0VDIyMzYxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTgzMTNmYzdkNjViOWNkYTlhNmY2MmI0Mzk0NGY0NWY5OTVkOTU2NzI3NDFiNWQyZDM3MTkwOTYxNDFiYzhjMWImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.VmL-lCZbXKG5r5f44mOYyYN97XCs1AUViT-1TnZMEcQ)
-
React의 Portal을 이용해 모달을 구현하였습니다.
-
회원가입과 로그인 모두 좌측에 공통되는 영역이 있으므로, useState에 true/false값을 부여해
버튼 클릭에 따라 우측의 회원가입/로그인의 화면이 스위칭될 수 있도록 구현하였습니다. -
또한 화면이 스위칭될 때 기존의 input에 입력했던 값이 초기화될 수 있도록 하였습니다.
![유효성 회원가입](https://private-user-images.githubusercontent.com/84097192/265241304-7adea334-34e4-4b20-8087-867819a31e62.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQ1MzkyNzEsIm5iZiI6MTcyNDUzODk3MSwicGF0aCI6Ii84NDA5NzE5Mi8yNjUyNDEzMDQtN2FkZWEzMzQtMzRlNC00YjIwLTgwODctODY3ODE5YTMxZTYyLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA4MjQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwODI0VDIyMzYxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTZhMTk5MmNmNzZhYjNhMDQ2NTEzODkwZDQ1ZTE1OGQwYTBjNWUyNmZiMzNmMDBhNTI4MjYzN2RjZGI2MTk1YmUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.678biFJGKxKDTMiN9eNgMa1f8nW0VHKkGl0rQQhXSFY)
-
input에 입력하는 onChange값을 실시간으로 감지하여 정규표현식을 통해 유효성 검사를 진행했고,
해당 값이 유효한지의 여부가 안내메시지를 통해 바로 확인될 수 있도록 하였습니다. -
필수값인 아이디, 비밀번호, 닉네임 중 하나라도 값이 입력되지 않았거나,
모든 값은 입력했으나 하나라도 유효성 검증을 통과하지 못할 경우
button의 disabled 속성을 이용해 회원가입 버튼이 비활성화되도록 구현하였습니다. -
비밀번호는 flask_bcrypt를 이용해 암호화하여 DB에 저장하였습니다.
![이미 존재하는 아이디](https://private-user-images.githubusercontent.com/84097192/265241445-3a12b7d1-7e3e-491e-aa40-c9b4d5f90eda.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQ1MzkyNzEsIm5iZiI6MTcyNDUzODk3MSwicGF0aCI6Ii84NDA5NzE5Mi8yNjUyNDE0NDUtM2ExMmI3ZDEtN2UzZS00OTFlLWFhNDAtYzliNGQ1ZjkwZWRhLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA4MjQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwODI0VDIyMzYxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWY0ZThkMTIzMjAxNTg1N2MxZmI5MzFiYzNhZTliZmU2YjRlMDllMjc0YmZkYzMwNDlhODFhNzYxYmIyM2FmYTcmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.YgOLIZq4sEDeDBMGVeVa-Z-X0Q2CfEeVlwPN4VTVcj0)
-
입력한 ID가 DB에 존재하는지 중복 체크를 진행해 중복일 경우 에러 메세지를 출력했고,
동시에 ID input을 초기화시켜 사용자가 명시적으로 알 수 있도록 구현하였습니다.
💡 로그인
ID 유효성 검증 | PWD 유효성 검증 |
---|---|
|
|
-
입력한 ID/PWD가 DB에 존재하지 않는다면 에러 메세지를 출력하고,
해당하는 input을 초기화하여 사용자가 명시적으로 알 수 있도록 하였습니다. -
입력한 비밀번호 값 역시 bcrypt를 이용해 암호화된 값이 일치하는지 확인하였습니다.
![로그인아웃 토큰](https://private-user-images.githubusercontent.com/84097192/265243489-8e09e43c-e0fd-4c35-97a1-53b99d06b2be.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQ1MzkyNzEsIm5iZiI6MTcyNDUzODk3MSwicGF0aCI6Ii84NDA5NzE5Mi8yNjUyNDM0ODktOGUwOWU0M2MtZTBmZC00YzM1LTk3YTEtNTNiOTlkMDZiMmJlLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA4MjQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwODI0VDIyMzYxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPThiOTE2YjcyYWNkMzc1NzRkZGFlNGVkYWM2MWUyNDE3NGRhNWU3ZGJiNzk1ZWM1NGMyNGYzYjRlNGFhYTNlNDYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.yDcC4iwwgRpjxf6ePegeiESGkvFFR7dMzECvmlVPZzY)
-
로그인 시 값이 모두 일치할 경우하여 로그인에 성공했을 경우만 jwt 토큰을 발급하였습니다.
-
서버에서 발급한 토큰은 react-cookie 라이브러리를 이용해 cookie에 저장하고,
response로 받은 id와 nickname은 Local Storage에 저장하였습니다. -
로그아웃 시 unset_jwt_cookies로 발급한 토큰을 삭제하고,
해당 요청이 성공했을 경우 클라이언트에서도 cookie와 storage에 저장된 값을 삭제하였습니다.
![글 작성 인증](https://private-user-images.githubusercontent.com/84097192/265243490-ac03e7ac-b484-4b48-8314-5bae3ecf1d80.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQ1MzkyNzEsIm5iZiI6MTcyNDUzODk3MSwicGF0aCI6Ii84NDA5NzE5Mi8yNjUyNDM0OTAtYWMwM2U3YWMtYjQ4NC00YjQ4LTgzMTQtNWJhZTNlY2YxZDgwLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA4MjQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwODI0VDIyMzYxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTQ2NGNlNmQ1YWEwZjJmZGQ1NjY2ODRhN2U0ZDIxYTEwMzkxOThmZDg2MThiMmI5YmZmYWM1NjY0MDI5ZjIyNjcmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.8thoukI16Fu0NIASFV-LwxadPaw7vH63qxWLU3-RAGo)
-
모든 화면에 공통적으로 출력되는 Header에서 useEffect를 통해 렌더링이 될 때마다
storage와 cookie에 있는 값이 유효한지 검증할 수 있도록 하였습니다. -
사용자가 하나라도 데이터를 임의로 삭제했을 경우 유효하지 않다는 alert를 보내고
로그아웃 될 수 있도록 구현하였습니다. -
게시글 작성 시에는 모든 토큰과 저장된 값이 유효한지 먼저 체크했고,
작성 도중 로그아웃하거나 정보를 임의 삭제할 때도 해당 인가가 유효하지 않도록 처리했습니다.
💡 게시글
마크다운을 이용한 게시글 작성 | 게시글 작성 유효성 검사 |
---|---|
|
|
-
react-md-editor를 이용해 마크다운으로 게시글을 작성 및 출력할 수 있도록 구현하였습니다.
-
작성 API 호출 전 모든 입력값이 존재하는지 유효성 검증을 진행했습니다.
-
특히 썸네일을 담당하는 이미지 URL의 경우 값을 입력할 때마다
new Image()의 이미지 객체를 이용해 해당 url로 이미지를 로딩할 수 있는지 체크하여
유효하지 않은 이미지 URL값의 입력을 제한했습니다. -
사용자가 게시글 작성 페이지에 진입했을 때 바로 토큰과 저장된 정보를 체크하여
토큰 및 정보가 유효하지 않을 경우에도 로그아웃 처리를 진행하지만,
서버단 코드에서도 사용자가 작성 시 보낸 토큰과 DB에 존재하는 유저 정보를 대조하여
유효한 토큰으로 접근하고 있는지, 해당 토큰에 저장된 값이 존재하는지 이중으로 인가를 확인했습니다. -
페이지 진입 시 게시글 작성인지, 수정인지 넘어오는 데이터를 통해 확인하여
게시글 작성, 수정 두 개의 기능을 한 개의 컴포넌트로 재사용하였습니다.
게시글 수정 | 게시글 삭제 |
---|---|
|
|
-
게시글 수정, 삭제는 로그인 후 storage에 저장된 id값과 게시글 작성자의 id값을 비교하여
일치할 경우에만 수정, 삭제 버튼이 출력되도록 하였습니다. -
게시글 수정 시에는 useLocation의 state를 이용해 기존 데이터를 수정 페이지로 넘겨주었으며,
설정한 flag값으로 작성, 수정 기능을 구분하여 한 개의 컴포넌트를 재사용하였습니다. -
게시글 삭제 후에는 메인페이지로 랜딩시킴과 동시에
invalidateQueries를 이용해 삭제된 내역이 반영되어 다시 보여질 수 있도록 하였습니다. -
수정, 삭제에도 서버로 넘어오는 토큰의 검증 및 해당 토큰에 담긴 값이
DB에 담긴 작성자의 id값과 일치하는지 인가 확인을 진행했습니다.
💡 기타
![유효하지 않은 주소](https://private-user-images.githubusercontent.com/84097192/265244285-db7e7f5f-bd57-4942-b755-a577de669af1.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQ1MzkyNzEsIm5iZiI6MTcyNDUzODk3MSwicGF0aCI6Ii84NDA5NzE5Mi8yNjUyNDQyODUtZGI3ZTdmNWYtYmQ1Ny00OTQyLWI3NTUtYTU3N2RlNjY5YWYxLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA4MjQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwODI0VDIyMzYxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTJhNDAwYmY3MTlmMGY2ZjhjN2ZlYjBkZmY0ZjdjZWYxMzg0NDI1ZjBmMWNmNDVjY2YzYjdlOTE4Y2IyNDBkNGImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.3Z4-ydn8YmAxZT9SP5lYXNSSIyGi7rqszcWsW9cEgm8)
-
사용자가 주소창에 유효하지 않은 주소를 입력할 경우
route의 path="*"를 이용해 404 에러 페이지로 랜딩되도록 처리하였습니다.
![유효하지 않은 게시글 및 로딩](https://private-user-images.githubusercontent.com/84097192/265244289-acb93c55-1e40-4827-9aab-3e0b9fe8f5c8.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQ1MzkyNzEsIm5iZiI6MTcyNDUzODk3MSwicGF0aCI6Ii84NDA5NzE5Mi8yNjUyNDQyODktYWNiOTNjNTUtMWU0MC00ODI3LTlhYWItM2UwYjlmZThmNWM4LmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA4MjQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwODI0VDIyMzYxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWQ5Y2NkZDMwYjNjMmQ2NWRiN2FjNzk0YjZjYTNkZmNjYmRkOWJjMDYxNzAxODEyMjdkYTU5NDg4ZWM0MjEyNWQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.UvXu-boO7JUAhZ2-H4cJnF3fCP8JEZayHcksBjZJTtQ)
-
사용자가 주소창에 유효하지 않은 게시글 번호를 입력해 조회를 시도할 경우
유효하지 않은 주소라는 alert를 띄우며 메인페이지로 다시 랜딩 처리하였습니다. -
서버와 연결 중 로딩이 있을 경우 로딩 스피너를 구현하여
조회 대기 중임을 사용자가 명시적으로 알 수 있도록 하였습니다.