-
Notifications
You must be signed in to change notification settings - Fork 26
[양재영] sprint 6 #108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[양재영] sprint 6 #108
The head ref may contain hidden characters: "React-\uC591\uC7AC\uC601-sprint6"
Conversation
|
스프리트 미션 하시느라 수고 많으셨어요. |
| return; | ||
| } | ||
|
|
||
| const reader = new FileReader(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이미지 미리보기 기능에서 서버 송신을 위해서 FileReader() 을 사용하였는데 URL.createObjectURL()을 사용하는 것이 더 적절한지 궁금합니다.
FileReader
The FileReader interface lets web applications asynchronously read the contents of files (or raw data buffers) stored on the user's computer, using File or Blob objects to specify the file or data to read.
이 FileReader인터페이스를 사용하면 웹 애플리케이션이 사용자 컴퓨터에 저장된 파일(또는 원시 데이터 버퍼)의 내용을 비동기적으로 읽을 수 있으며, File또는 Blob객체를 사용하여 읽을 파일이나 데이터를 지정할 수 있습니다.
FileReader.readAsDataURL()
Starts reading the contents of the specified Blob, once finished, the result attribute contains a data: URL representing the file's data.
지정된 파일의 내용을 읽기 시작하고 Blob, 완료되면 속성에 파일 데이터를 나타내는 URL이 result포함됩니다 .
저장된 파일의 버퍼 혹은 원시 데이터의 내용을 읽을 수 있게 해주는 API군요.
이렇게 사용하여도 URL을 지원되긴 하지만 오버헤드가 있을 수도 있겠어요.
오버헤드: 어떤 처리에 필요한 간접적인 시간이나 자원을 의미합니다. 즉, 주된 작업 외에 추가적으로 발생하는 비용이나 노력을 말합니다.
직접 테스트를 통해서 얼만큼의 시간 비용 절약이 있는지 확인해보면 좋겠으나, 문서만 보기에는 말씀 주신대로 '미리보기'에는 createObjectURL 사용하시는게 더 나을 수 있을 것 같아요. 😉😉
| const handleFileChange = (e) => { | ||
| const file = e.target.files[0]; | ||
| if (!file) return; | ||
|
|
||
| if (previewImg) { | ||
| SetShowError(true); | ||
| inputRef.current.value = ""; | ||
| return; | ||
| } | ||
|
|
||
| const reader = new FileReader(); | ||
| reader.onloadend = () => { | ||
| setPreviewImg(reader.result); | ||
| }; | ||
| reader.readAsDataURL(file); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(이어서) 변경된다음 다음과 같이 변경될 수 있겠군요 ! 😉
| const handleFileChange = (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| if (previewImg) { | |
| SetShowError(true); | |
| inputRef.current.value = ""; | |
| return; | |
| } | |
| const reader = new FileReader(); | |
| reader.onloadend = () => { | |
| setPreviewImg(reader.result); | |
| }; | |
| reader.readAsDataURL(file); | |
| }; | |
| const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |
| const file = e.target.files?.[0]; | |
| if (!file) return; | |
| // 예: 타입/용량 검증 | |
| if (!file.type.startsWith("image/")) { | |
| setError("이미지 파일만 업로드할 수 있습니다."); | |
| e.target.value = ""; | |
| return; | |
| } | |
| setError(null); | |
| fileRef.current = file; | |
| // 이전 URL 정리 | |
| if (previewUrl) URL.revokeObjectURL(previewUrl); | |
| // 미리보기 URL 생성 | |
| const url = URL.createObjectURL(file); | |
| setPreviewUrl(url); | |
| }; |
| const ProductAddHeaderButton = styled.button` | ||
| font-weight: 600; | ||
| font-size: 1.6rem; | ||
| color: var(--gray-100); | ||
| background-color: ${({ disabled }) => | ||
| disabled ? "var(--gray-400)" : "var(--blue)"}; | ||
| border-radius: 0.8rem; | ||
| width: 7.4rem; | ||
| height: 4.2rem; | ||
| border: 0.1rem solid var(--gray-400); | ||
| cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")}; | ||
| `; | ||
|
|
||
| export default function ProductAddHeader({ isFormValid }) { | ||
| return ( | ||
| <ProductAddHeaderDiv> | ||
| <ProductAddHeaderTitle>상품 등록하기</ProductAddHeaderTitle> | ||
| <ProductAddHeaderButton type="submit" disabled={!isFormValid}> | ||
| 등록 | ||
| </ProductAddHeaderButton> | ||
| </ProductAddHeaderDiv> | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
굿굿 ! button의 disabled 속성에 접근한 점 훌륭합니다 !
다만, css도 상태 선택자를 통해서 관리해볼 수 있을 것 같아요 !:
| const ProductAddHeaderButton = styled.button` | |
| font-weight: 600; | |
| font-size: 1.6rem; | |
| color: var(--gray-100); | |
| background-color: ${({ disabled }) => | |
| disabled ? "var(--gray-400)" : "var(--blue)"}; | |
| border-radius: 0.8rem; | |
| width: 7.4rem; | |
| height: 4.2rem; | |
| border: 0.1rem solid var(--gray-400); | |
| cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")}; | |
| `; | |
| export default function ProductAddHeader({ isFormValid }) { | |
| return ( | |
| <ProductAddHeaderDiv> | |
| <ProductAddHeaderTitle>상품 등록하기</ProductAddHeaderTitle> | |
| <ProductAddHeaderButton type="submit" disabled={!isFormValid}> | |
| 등록 | |
| </ProductAddHeaderButton> | |
| </ProductAddHeaderDiv> | |
| ); | |
| const ProductAddHeaderButton = styled.button` | |
| font-weight: 600; | |
| font-size: 1.6rem; | |
| color: var(--gray-100); | |
| border-radius: 0.8rem; | |
| width: 7.4rem; | |
| height: 4.2rem; | |
| border: 0.1rem solid var(--gray-400); | |
| &:enabled { | |
| background-color: var(--blue); | |
| cursor: pointer; | |
| } | |
| &:disabled { | |
| background-color: var(--gray-400); | |
| cursor: not-allowed; | |
| } | |
| `; | |
| export default function ProductAddHeader({ isFormValid }) { | |
| return ( | |
| <ProductAddHeaderDiv> | |
| <ProductAddHeaderTitle>상품 등록하기</ProductAddHeaderTitle> | |
| <ProductAddHeaderButton type="submit" disabled={!isFormValid}> | |
| 등록 | |
| </ProductAddHeaderButton> | |
| </ProductAddHeaderDiv> | |
| ); |
이렇게 하면 삼항연산자를 사용하지 않고 "특정 상태"에 대한 스타일링을 해볼 수 있습니다. 😉
| try { | ||
| const params = new URLSearchParams({ page, pageSize, orderBy, keyword }); | ||
| const response = await instance.get("/products", { params }); | ||
| return response.data; | ||
| } catch (error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
크으 ~ axios를 적용해보셨군요 ! 👍👍
훌륭합니다 ! 이 경험을 살려서 기초 팀 프로젝트에도 채택해보실 수 있겠어요.
| @@ -0,0 +1,33 @@ | |||
| import axios from "axios"; | |||
|
|
|||
| const VITE_BASE_URL = import.meta.env.VITE_BASE_URL; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
굿굿 ! base url을 환경 변수로 잘 설정하셨군요 👍
| (error) => { | ||
| if (error.response) { | ||
| const status = error.response.status; | ||
|
|
||
| if (status === 401) { | ||
| alert("인증이 필요합니다. 로그인 해주세요."); | ||
| } else if (status === 500) { | ||
| alert("서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); | ||
| } | ||
| } else { | ||
| alert("네트워크 오류가 발생했습니다."); | ||
| } | ||
|
|
||
| return Promise.reject(error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
각 상태 코드 별로 예외처리도 해두셨군요. 👍
굿굿. 공통적인 예외처리도 신경쓰시는 모습 보기 좋습니다 ! 👍
다음과 같이 설계해볼 수도 있을 것 같네요:
| (error) => { | |
| if (error.response) { | |
| const status = error.response.status; | |
| if (status === 401) { | |
| alert("인증이 필요합니다. 로그인 해주세요."); | |
| } else if (status === 500) { | |
| alert("서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); | |
| } | |
| } else { | |
| alert("네트워크 오류가 발생했습니다."); | |
| } | |
| return Promise.reject(error); | |
| export const ERROR_STATUSCODE_MESSAGES = { | |
| 401: "인증이 필요합니다. 로그인 해주세요.", | |
| 403: "접근 권한이 없습니다.", | |
| 404: "요청하신 리소스를 찾을 수 없습니다.", | |
| 500: "서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.", | |
| }; | |
| // ... | |
| (error) => { | |
| if (error.response) { | |
| const status = error.response.status; | |
| alert(ERROR_STATUSCODE_MESSAGES[status] || "알 수 없는 오류가 발생했습니다."); | |
| } else { | |
| alert("네트워크 오류가 발생했습니다."); | |
| } | |
| return Promise.reject(error); |
|
|
||
| export default function SelectDropdown({ onChange, value }) { | ||
| const [isOpen, setIsOpen] = useState(false); | ||
| const dropdownRef = useRef(null); | ||
|
|
||
| const options = [ | ||
| { label: "최신순", value: "recent" }, | ||
| { label: "좋아요순", value: "favorite" }, | ||
| ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
options 또한 컴포넌트 외부에 선언해볼 수 있겠네요 😉
현재 options는 컴포넌트 내부의 자원(상태나 props)을 사용하고 있지 않아요.
리렌더링 시 불필요한 재선언이 될 수 있으므로 컴포넌트 바깥에 선언해볼 수 있습니다 😉
| export default function SelectDropdown({ onChange, value }) { | |
| const [isOpen, setIsOpen] = useState(false); | |
| const dropdownRef = useRef(null); | |
| const options = [ | |
| { label: "최신순", value: "recent" }, | |
| { label: "좋아요순", value: "favorite" }, | |
| ]; | |
| const options = [ | |
| { label: "최신순", value: "recent" }, | |
| { label: "좋아요순", value: "favorite" }, | |
| ]; | |
| export default function SelectDropdown({ onChange, value }) { | |
| const [isOpen, setIsOpen] = useState(false); | |
| const dropdownRef = useRef(null); |
이렇게 되면 컴포넌트 내부에는 컴포넌트 자원만 사용하는 코드들만 남게되어 자연스레 가독성도 좋아질거예요. 👍
|
수고하셨습니다 재영님 ! 코드잇에서의 재영님의 첫 협업 프로젝트 응원합니다 👍👍 |
요구사항
Github에 PR(Pull Request)을 만들어서 미션을 제출합니다.
피그마 디자인에 맞게 페이지를 만들어 주세요.
React를 사용합니다
기본
심화
주요 변경사항
스크린샷
멘토에게
FileReader()을 사용하였는데URL.createObjectURL()을 사용하는 것이 더 적절한지 궁금합니다.