[유선향]-sprint8#71
Hidden character warning
Conversation
|
스프리트 미션 하시느라 수고 많으셨어요. |
global.d.ts 파일에 전역 타입과 주석으로 된 질문이 있습니다넵넵 ! 확인해보겠습니다 ! |
| tags: Tag[]; | ||
| images: [string]; | ||
| description: string; | ||
| [key: string]: any; |
There was a problem hiding this comment.
(질문에 대한 답변)혹시 [key: string]: any;를 지우는건 어떨까요?
| [key: string]: any; |
string| boolean| [string]|number로 하셔도 마찬가지입니다. 어떤 리소스가 어떤 타입인지 알 수가 없어지므로 타입스크립트를 쓰는 의미가 퇴색될 것 같아요.
해당 라인([key: string]: any;)을 지우시는게 좋을 것 같아요.
| price: 0, | ||
| tags: [], | ||
| }; | ||
| const REQUIRED_INPUT = ["name", "content", "tags", "price"]; |
There was a problem hiding this comment.
(질문에 답변을 이어서) as const를 통하여 string이 아닌 유니온 타입으로 강제해볼게요.
| const REQUIRED_INPUT = ["name", "content", "tags", "price"]; | |
| const REQUIRED_INPUT = ["name", "description", "tags", "price"] as const; |
이렇게 하면 해당 타입은 readonly ["name", "description", "tags", "price"]이 됩니다.
그리고
content라고 작성주셨는데 코드 전문을 읽어보니description이 맞는 것 같아서 임의로 수정하였습니다 😊
이렇게 진행하시면 에러가 나지 않을 거예요 ! 😊
There was a problem hiding this comment.
(참고) git diff를 통한 수정사항:
@@ -14,7 +14,7 @@ const INITIAL_DATA: ProductForm = {
price: 0,
tags: [],
};
-const REQUIRED_INPUT = ["name", "content", "tags", "price"];
+const REQUIRED_INPUT = ["name", "description", "tags", "price"] as const;
//
function AddItem() {
const [tag, setTag] = useState<string>("");
diff --git a/src/types/global.d.ts b/src/types/global.d.ts
index bfd0b61..c120cc4 100644
--- a/src/types/global.d.ts
+++ b/src/types/global.d.ts
@@ -16,7 +16,7 @@ declare global {
tags: Tag[];
images: [string];
description: string;
- [key: string]: any;
+ // [key: string]: any;
//원래 any 부분에 string| boolean| [string]|number 를 적었었는데,
//유니온 타입에 이터러블이 아닌 값이 섞여있어서 includes랑 스프레드가 안되더라고요... ㅠㅠ
//인덱스 시그니쳐를 지우면 addItem 에서 에러가 나서 방법을 찾아보다가 도저히 모르겠어서 any 로 지정했습니다! (addItem에 에러 부분 주석 남겨놨습니다!)| export async function getProductComments({ productId }: Params, limit = 3) { | ||
| try { | ||
| const res = await axios.get( | ||
| `${BASE_URL}/products/${productId}/comments?limit=${limit}` | ||
| ); | ||
| if (!res) { | ||
| throw new Error("리뷰 불러오기 실패"); | ||
| } | ||
| return res.data; | ||
| const data: Comment[] = res.data.list; | ||
| return data; |
There was a problem hiding this comment.
다음과 같이 제네릭을 통해서 타입을 정의할 수 있어요 !
| export async function getProductComments({ productId }: Params, limit = 3) { | |
| try { | |
| const res = await axios.get( | |
| `${BASE_URL}/products/${productId}/comments?limit=${limit}` | |
| ); | |
| if (!res) { | |
| throw new Error("리뷰 불러오기 실패"); | |
| } | |
| return res.data; | |
| const data: Comment[] = res.data.list; | |
| return data; | |
| export async function getProductComments({ productId }: Params, limit = 3) { | |
| try { | |
| const res = await axios.get<Comment[]>( // <-- 여기요 ! | |
| `${BASE_URL}/products/${productId}/comments?limit=${limit}` | |
| ); | |
| if (!res) { | |
| throw new Error("리뷰 불러오기 실패"); | |
| } | |
| const data = res.data.list; | |
| return data; |
| const res = await axios.get( | ||
| `${BASE_URL}/products/${productId}/comments?limit=${limit}` | ||
| ); |
There was a problem hiding this comment.
또한 axios를 사용중이시라면 instnace를 생성하셔서 사용해보실 것을 권장드립니다 !
BASE_URL을 매번 입력하셔야 하며, 각기 다른 엔드포인트와 그에 따른 설정이 필요할 때에 유지보수가 어려울 수 있어요:
어떻게 세팅하면 될까? 🤔
instance를 만들어서 export를 하고 사용해보는 것 정도로 시도해보면 좋을 것 같아요. axios-instance 파일을 만들어서 instance를 생성하고 export한 후 사용해보는건 어떨까요?
다음과 같이 만들어볼 수 있어요:
const baseURL = process.env.NEXT_PUBLIC_LINKBRARY_BaseURL;
const instance = axios.create({
baseURL: baseURL,
headers: {
'Content-Type': 'application/json',
},
});
export default instance인가에 필요한 accessToken을 localStorage가 있다면 axios의 인터셉터를 활용할 수 있습니다 !
인터셉터는 혼자 해결해보시는 것을 권장드립니다. 혹시 모르시겠으면 다음 위클리 미션에 질문해주세요. 😊
사용 방법 🚀
사용 방법은 정말 간단해요. 다음과 같이 사용할 수 있습니다:
instance.get(`/user/${userId}`)딱 보니. 마이그레이션도 정말 쉽게 할 수 있겠죠? 😊
| const order = selectedOrder === "최신순" ? "recent" : "favorite"; | ||
| const pageSize = device === "mobile" ? 4 : device === "tablet" ? 6 : 10; | ||
| const query = `?orderBy=${order}&page=${page}&pageSize=${pageSize}`; |
There was a problem hiding this comment.
쿼리는 URLSearchParams로 손쉽게 사용할 수 있어요 !
| const order = selectedOrder === "최신순" ? "recent" : "favorite"; | |
| const pageSize = device === "mobile" ? 4 : device === "tablet" ? 6 : 10; | |
| const query = `?orderBy=${order}&page=${page}&pageSize=${pageSize}`; | |
| const order = selectedOrder === "최신순" ? "recent" : "favorite"; | |
| const pageSize = device === "mobile" ? 4 : device === "tablet" ? 6 : 10; | |
| const query = new URLSearchParams({order, page, pageSize}).toString(); |
URLSearchParams와 함께 객체로 손쉽게 핸들링할 수 있습니다 !
객체로 구성할 수 있어 가독성이 좋고, URL 인코딩을 자동으로 처리하여 특수 문자나 공백이 포함된 값에서도 안전하게 동작합니다 !
URLSearchParams:
URLSearchParams인터페이스는 URL의 쿼리 문자열을 대상으로 작업할 수 있는 유틸리티 메서드를 정의합니다.
쿼리를 생성하실 때에 참고해서 사용해보세요 😊
| const [EditingId, setEditingId] = useState<number>(0); | ||
| const [DeleteId, setDeleteId] = useState<number>(0); |
There was a problem hiding this comment.
(선택/의견) 초기 값이 넘버일 경우 자연스레 number 타입으로 추론됩니다 😊
| const [EditingId, setEditingId] = useState<number>(0); | |
| const [DeleteId, setDeleteId] = useState<number>(0); | |
| const [EditingId, setEditingId] = useState(0); | |
| const [DeleteId, setDeleteId] = useState(0); |
| interface Props { | ||
| data: Comment; | ||
| } | ||
| export default function CommentCard({ data }: Props) { |
| interface Prop { | ||
| value: string; | ||
| items: Item[]; | ||
| } | ||
| interface ListItemProps { | ||
| value: string; | ||
| item: Item; | ||
| } | ||
| function ListItem({ value, item }: ListItemProps) { | ||
| const navigate = useNavigate(); | ||
| const device = useWindowSize(); | ||
| return ( | ||
| <S.Item onClick={() => navigate(`./${item.id}`)}> | ||
| <S.ProductImg | ||
| value={value} | ||
| $device={device} | ||
| src={item.images[0]} | ||
| alt="이미지" | ||
| /> | ||
| <S.FlexContent> | ||
| <S.Title>{item.name}</S.Title> | ||
| <S.Price>{item.price} 원</S.Price> | ||
| <BtnHeart $items value={item.favoriteCount} /> | ||
| </S.FlexContent> | ||
| </S.Item> | ||
| ); | ||
| } | ||
| // | ||
| export default function ItemsList({ value, items, ...props }: Prop) { |
There was a problem hiding this comment.
(제안/주관적 의견) 관련된 타입은 사용될 코드와 가까이 두는게 어떨까요?
| interface Prop { | |
| value: string; | |
| items: Item[]; | |
| } | |
| interface ListItemProps { | |
| value: string; | |
| item: Item; | |
| } | |
| function ListItem({ value, item }: ListItemProps) { | |
| const navigate = useNavigate(); | |
| const device = useWindowSize(); | |
| return ( | |
| <S.Item onClick={() => navigate(`./${item.id}`)}> | |
| <S.ProductImg | |
| value={value} | |
| $device={device} | |
| src={item.images[0]} | |
| alt="이미지" | |
| /> | |
| <S.FlexContent> | |
| <S.Title>{item.name}</S.Title> | |
| <S.Price>{item.price} 원</S.Price> | |
| <BtnHeart $items value={item.favoriteCount} /> | |
| </S.FlexContent> | |
| </S.Item> | |
| ); | |
| } | |
| // | |
| export default function ItemsList({ value, items, ...props }: Prop) { | |
| interface ListItemProps { | |
| value: string; | |
| item: Item; | |
| } | |
| function ListItem({ value, item }: ListItemProps) { | |
| const navigate = useNavigate(); | |
| const device = useWindowSize(); | |
| return ( | |
| <S.Item onClick={() => navigate(`./${item.id}`)}> | |
| <S.ProductImg | |
| value={value} | |
| $device={device} | |
| src={item.images[0]} | |
| alt="이미지" | |
| /> | |
| <S.FlexContent> | |
| <S.Title>{item.name}</S.Title> | |
| <S.Price>{item.price} 원</S.Price> | |
| <BtnHeart $items value={item.favoriteCount} /> | |
| </S.FlexContent> | |
| </S.Item> | |
| ); | |
| } | |
| // | |
| interface ItemsListProps { | |
| value: string; | |
| items: Item[]; | |
| } | |
| export default function ItemsList({ value, items, ...props }: ItemsListProps) { |
기존 Props이라는 타입은 ItemsList의 props에만 사용되는 것 같아요. 관련된 컴포넌트 바로 위에 선언하여 가독성을 좋게하고, 해당 코드에는 두 props 타입이 존재하므로 이름을 명확히하는게 어떨까 제안드립니다 !
| type PageCountProps = Omit<Props, "onClick"> & { | ||
| onClick: (btn: number) => void; | ||
| }; |
| interface Props extends S.ButtonProps { | ||
| onClick: onClick; | ||
| children: ReactNode; | ||
| disabled?: boolean; | ||
| } | ||
| export default function Button({ onClick, children, ...props }: Props) { | ||
| return ( | ||
| <> | ||
| <S.Button onClick={onClick} {...props}> | ||
| {children} | ||
| </S.Button> | ||
| </> | ||
| ); | ||
| } |
There was a problem hiding this comment.
(제안) React에서 이미 선언된 기본 HTML 타입을 사용해보실 수도 있어요. 😉
| interface Props extends S.ButtonProps { | |
| onClick: onClick; | |
| children: ReactNode; | |
| disabled?: boolean; | |
| } | |
| export default function Button({ onClick, children, ...props }: Props) { | |
| return ( | |
| <> | |
| <S.Button onClick={onClick} {...props}> | |
| {children} | |
| </S.Button> | |
| </> | |
| ); | |
| } | |
| import * as S from "./Button.style"; | |
| import { ButtonHTMLAttributes } from "react"; | |
| interface Props extends ButtonHTMLAttributes<HTMLButtonElement> { | |
| children: React.ReactNode; | |
| } | |
| export default function Button({ children, ...props }: Props) { | |
| return ( | |
| <> | |
| <S.Button {...props}>{children}</S.Button> | |
| </> | |
| ); | |
| } |
만약 html 버튼에 있는 type, onBlur, clssName 등이 필요하게 button.tsx를 새로운 props가 필요할 때 마다 지속적으로 수정해야 될거예요. 만약 협업중이었다면 공통 컴포넌트이니 만큼 다른 개발자들도 사용하고 있으므로 충돌이 날 수도 있지요 !
기존
onClick을 지웠어요. 전달 역할만 수행하며 별도의 추가 로직이 없기에...props내에onClcik이 전달될거예요 😊
|
크으... 정말 빠릅니다 선향님. 타입 스크립트도 멋지게 해내셨군요. 특히 유틸리티 타입을 사용하시는 것 보면 배우신 것들을 정말 요목조목 잘 사용하시는 것 같아요.
|
요구사항
기본
심화
주요 변경사항
멘토에게