- 중고 거래 서비스 웹 구현 프로젝트 Frontend 섹션
- Team GitHub Repository
🎯 기획서와 디자인에서 요구하는 중고 거래 서비스 웹을 구현하는 프로젝트 입니다.
- 팀 구성 : FE 2명, BE 2명, iOS 2명
- 개발 기간 : 2023.06 ~ 2023.08 (10주)
- 나의 작업 기여도 : 88% (FE 77개의 PR중 68개를 담당)
- 기술 스택 : React, TypeScript, styled-components
Trouble 1 : 이미지 재업로드 실패 문제
- input 태그를 통해 파일을 입력받을 때 onChange 이벤트를 통해 받게 된다.
- onChange는 실질적인 데이터가 바뀔때만 반응하므로 기존의 파일을 다시 업로드하면 작동하지 않는다.
- 때문에, onChange 이벤트를 통해 데이터를 등록 후, value 값을 reset 해주어야 한다.
-
event.target.value값을 reset 하여 문제 해결
const handleUploadImage = (event: ChangeEvent<HTMLInputElement>) => { const files = Array.from(event.target.files || []); // 중략 event.target.value = ""; };
Trouble 2 : 동일 이미지의 중복 업로드 문제
- Trouble 1의 문제 해결 단계에서 업로드 데이터 값을 초기화 했기 때문에 중복으로 업로드 되는 문제 발생
-
동일한 데이터의 업로드를 막아주는 로직 추가가 필요
-
업로드 이미지는
URL.createObjectURL()
값으로 등록되는데 reset으로 중복 처리에 사용 불가 -
동일한 이미지 파일인지 구분을 위해 file.name, file.size값을 적용
const handleUploadImage = (event: ChangeEvent<HTMLInputElement>) => { const files = Array.from(event.target.files || []); // 중략 // 동일한 이미지 파일인지 구분하기 위해 file.name, file.size 값 추가 files.forEach((file) => { const imageUrl = URL.createObjectURL(file); const id = uuidv4(); const fileName = file.name; const fileSize = file.size; // 중복 체크 로직 추가 const isDuplicate = uploadedImages.some( (image) => image.fileName === file.name && image.fileSize === file.size ); if (!isDuplicate) { const newUploadedImage: UploadedImageType = { id, imageUrl, fileName, fileSize, }; setUploadedImages((prevImages) => [...prevImages, newUploadedImage]); } }); event.target.value = ""; };
Trouble 3 : 페이지 전환 시 작성 중인 데이터 손실 문제
- React는 페이지 이동시 현재 페이지의 컴포넌트가 언마운트 되고, 새로운 페이지의 컴포넌트가 마운트 된다.
- 언마운트 과정에서 컴포넌트의 상태는 메모리에서 해제된다.
- 다시 이전 페이지로 돌아오더라도 해당 페이지의 컴포넌트는 다시 마운트 되기에 초기상태로 시작된다.
- 본 프로젝트에서는 전역 상태 관리 라이브러리를 사용하지 않고 있기에 언마운트 때 해제되는 상태를 보존하기 위해서는
로컬스토리지
나세션스토리지
같은 클라이언트 측 저장소를 사용해야 한다. - 브라우저를 실수로 닫아도 데이터가 유지될 수 있도록 로컬스토리지를 활용해서 해결해보자.
-
상품 등록 최상위 컴포넌트에서 상태(postObject)를 관리한다.
-
마운트 되었을때 로컬스토리지에 저장된 값이 존재한다면 불러온다.
const storedPostObject = localStorage.getItem("postObject"); // 최상위 컴포넌트에서 관리되고 있는 상태 postObject const [postObject, setPostObject] = useState<PostObjectType>( storedPostObject ? JSON.parse(storedPostObject) : initialPostObject );
-
상품 등록이 완료된다면 로컬스토리지에 저장된 값을 제거해야한다.
const handleUploadComplete = async () => { // 중략 await postProducts(formData, accessToken); localStorage.removeItem("postObject"); navigation(-1); };
-
상품등록 컴포넌트가 언마운트 되더라도 로컬스토리지에 데이터 값을 저장하여 보존할 수 있다.
-
하위 컴포넌트들에서 입력되는 데이터값들을 로컬스토리지에 저장 해준다.
const { title, price, content, categoryId, locationId, files } = postObject; useEffect(() => { const postObjectToStore = { title, price, content, categoryId, locationId, files, }; localStorage.setItem("postObject", JSON.stringify(postObjectToStore)); }, [title, price, content, categoryId, locationId, files]);
Trouble 4 : 이중 렌더링 문제
-
Trouble 3과 이어지는 과정으로 상품등록의 입력 컴포넌트들(자식)은 postObject를 전달 받고 있다.
-
postObject에 존재하는 데이터가 있다면 초기값으로 셋팅하기 위해 useEffect를 사용함이 원인이다.
const { postObject, setPostObject } = useContext(postSalesItemContext); const [inputComment, setInputComment] = useState<string | null>(null); useEffect(() => { if (postObject.content) { const storedContent = postObject.content; setInputComment(storedContent); } }, []);
-
사용자 입력으로 postObject가 변경되면 부모 컴포넌트로 부터 변경된 props가 전달되면서 렌더링이 일어난다. (1번)
-
렌더링 후 useEffect가 호출되면서 렌더링이 일어난다. (2번)
-
보존 데이터를 업데이트 하기 위한 용도로 useEffect를 사용한 문제다.
-
렌더링을 위해 데이터를 변환하는 경우 useEffect는 필요하지 않다.
-
부모 컴포넌트로부터 전달 받은 props를 자식 컴포넌트 초기값으로 설정한다.
const [inputComment, setInputComment] = useState<string | null>( postObject.content ? postObject.content : null );
-
부모 컴포넌트에서 localStorage에 값을 보존하기 위한 useEffect를 제외한 모든 자식 컴포넌트들의 useEffect를 제거한다.
-
두 번씩 일어나던 렌더링 해결.
-
수정 내용 성능 테스트 결과 사용자가 체감 하기에는 미비한 수치지만 개선됨을 확인할 수 있다.
Trouble 5 : 변경 없음에도 useEffect 호출 문제
- 의존성 배열에 객체를 두었기 때문에 발생하는 문제
- 자바스크립트의 객체는 평가될 때마다 새로운 객체를 생성한다.
useEffect(() => { const postObjectToStore = { ...postObject }; localStorage.setItem("postObject", JSON.stringify(postObjectToStore)); console.log("변경사항 렌더링 체크"); }, [postObject]);
-
의존성 배열에서 객체를 제거한다.
-
useEffect 외부에서
객체의 원시값
을 읽어준다. -
useEffect 의존성 배열에서
원시값
을 비교한다.const { title, price, content, categoryId, locationId, files } = postObject; useEffect(() => { const postObjectToStore = { title, price, content, categoryId, locationId, files, }; localStorage.setItem("postObject", JSON.stringify(postObjectToStore)); console.log("렌더링 체크"); }, [title, price, content, categoryId, locationId, files]);
Trouble 6 : 동일 사용자 로그인의 일관되지 않은 성공/실패 문제
- 개발자 도구 네트워크를 확인하니 로그인 요청이 두 번 발생하고 있다.
- 요청이 두 번 발생하기에 잘못된 인증코드가 서버로 가는 경우가 발생
엄격모드
는 컴포넌트의 부수효과를 두 번 호출한다.- 이는 상용 환경에서는 발생하지 않는 문제이지만, 엄격모드를 제거한다면 개발 단계에서 잠재적 문제를 발견하기 어렵다.
- useEffect에
클립업
함수를 추가해서 두 번째 요청을 무시하도록 해야한다.
- 웹에서 "GitHub으로 로그인" 버튼 제공
- 사용자 로그인 버튼 클릭
- GitHub의 OAuth 인증 페이지로 리다이렉트
- 사용자는 GitHub에 로그인하고 애플리케이션에 필요한 권한 부여
- 인증 및 권한 부여가 성공하면 GitHub는 사용자를 웹으로 다시 리다이렉트
- 인증 코드(AUTHORIZATION_CODE)가 URL의 쿼리 파라미터로 전달
- 리다이렉트 되면서 클라이언트에서 서버측으로 AUTHORIZATION_CODE 코드와 함께 로그인 요청
- 서버는 AUTHORIZATION_CODE코드와 "client_secret"을 함께 GitHub OAuth 서버로 전송하여 액세스 토큰 획득
- 서버는 액세스 토큰을 이용하여 GitHub API를 통해 사용자 정보를 가져온다
- 사용자 정보가 "신규" 회원인지 "기존회원" 인지 서버에서 판단하고 클라이언트에게 응답
- 서버로 부터 "응답"과 함께 JWT을 받는다
- 응답이 "신규" 이면 JWT로 회원가입 진행
- 응답이 "기존회원" 결과를 받으면 JWT로 로그인 진행
const Callback = () => {
const searchParams = new URLSearchParams(window.location.search);
const code = searchParams.get(AUTHORIZATION_CODE);
const { data } = useAsync(() => postLogin(code));
const { handleLogin } = useAuthContext();
const navigate = useNavigate();
useEffect(() => {
if (data?.status === 'FORBIDDEN') {
const { nickname, profileUrl, oauthId } = data.data;
navigate(
`${REGISTER}?nickname=${nickname}&profileUrl=${profileUrl}&oauthId=${oauthId}`,
);
}
if (data?.status === 'OK') {
const { jwt } = data.data;
handleLogin(jwt);
navigate(HOME);
}
}, [data]);
return (
// 중략
const { data } = useAsync(() => postLogin(code))
이 원인으로 판단
function useAsync<T>(
// 중략
const fetchData = async (): Promise<void> => {
dispatch({ type: 'LOADING' });
try {
const response: AxiosResponse<T> = await callback();
dispatch({ type: 'SUCCESS', data: response.data });
} catch (e) {
dispatch({ type: 'ERROR', error: e as AxiosError });
}
};
useEffect(() => {
if (skip) return;
fetchData();
}, deps);
// 중략
}
- 두 번째 로그인 요청에서는
fetchData()
가 호출되지 않도록클립업
함수를 추가한다.
let ignore = false;
useEffect(() => {
if (skip) return;
if (!ignore) {
fetchData();
}
return () => {
ignore = true;
};
}, deps);
노아[iOS] | 에이든[iOS] | 만쥬[BE] | 시레[BE] | 사랑대디[FE] | 시저[FE] |
---|---|---|---|---|---|
noah0316 | wnsqhs | JeonHyoChang | dltpwns0 | sarangdaddy | zlx454545 |
💡 우리팀의 가장 중요한 가치는? ☝️ 하나를 하더라도 확실히! ⇒ 근거있는 맛있는 코드 ✌️ 적극적인 공유 및 협업 ⇒ 상황공유 및 일정공유 확/실히!
- 데일리 스크럼 시작 시간은 오전 10시 10분
- 코어타임: 10시 10분 ~ 17시
- 밤 시간에 슬랙을 통한 의사공유도 자유롭게!
- 게더도입 고민!
- 지각비 3,000원
- 서버 비를 우선으로 하되, 남는다면 회식 비로!
- 협업 포인트가 생긴다면, 오전 스크럼시간에 타 클래스와 함께 요청하기
- 긴급 요청의 경우 자유롭게 물어보기
- 만약 상대방이 빡 집중을 하고 있는 경우 한 템포 쉬고 물어보기
- GitHub Orgazination Wiki에 정리하기
- ex: ) 코딩 컨벤션, 커밋 가이드, 구조 가이드
- 매 주 금요일은 공간에서 같이 식사!
KPT 회고 프레임 워크를 이용하여 매주 금요일 회고 시간에 회고 진행하기 참고:
```