-
Notifications
You must be signed in to change notification settings - Fork 35
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
[최영선] sprint10 #311
The head ref may contain hidden characters: "Next-\uCD5C\uC601\uC120-sprint10"
[최영선] sprint10 #311
Conversation
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.
마지막 과제 제출하시느라 고생 많으셨습니다 🙏
<form onSubmit={handleSubmit}> | ||
<StyledTopSection> | ||
<StyledTitle>댓글달기</StyledTitle> | ||
<StyledButton disabled={!checkAllInputsFilled()}>등록</StyledButton> | ||
</StyledTopSection> | ||
<label htmlFor="comment" /> | ||
<StyledTextArea | ||
id="comment" | ||
name="comment" | ||
value={commentValue.comment} | ||
placeholder="댓글을 입력해주세요" | ||
onChange={handleInputChange} | ||
/> | ||
</form> |
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.
이런 폼 같은 경우, 별도 아래처럼 컴포넌트로 분리하면 board[id] 페이지 내 jsx가 줄면서 가독성이 좋아지고, 다른 코드에 더 집중할 수 있습니다. 나눌 수 있는 컴포넌트가 있으면 나누는 것도 좋아요. 무조건 나눠서 방해가 되지 않게 해야되지만, 보통 이런 폼은 별도로 나눕니다.
function CommentForm({ onSubmit }: { onSubmit: (comment: string) => void }) {
const [comment, setComment] = useState("");
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit(comment);
setComment("");
};
return (
<form onSubmit={handleSubmit}>
<StyledTopSection>
<StyledTitle>댓글달기</StyledTitle>
<StyledButton disabled={!comment}>등록</StyledButton>
</StyledTopSection>
<label htmlFor="comment">댓글 입력</label>
<StyledTextArea
id="comment"
name="comment"
value={comment}
placeholder="댓글을 입력해주세요"
onChange={(e) => setComment(e.target.value)}
aria-label="댓글 입력"
/>
</form>
);
}
const [Detail, setDetail] = useState<Article>(); | ||
const [commentValue, setCommentValue] = useState({ comment: "" }); | ||
const router = useRouter(); | ||
const { id } = router.query; |
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.
여기 타입 처리에 어려움이 있어서 결국 아래 컴포넌트에 props로 전달할 때 any
가 사용되었는데요,
router.query
의 기본 타입 값은 string | string[] | undefined
입니다.
- string: 단일 값일 때 (예: /article/123)
- string[]: 여러 값이 있을 때 (예: /article/123?id=456)
- undefined: 페이지가 처음 로드될 때 또는 id 파라미터가 없을 때
결국 아래처럼 정해볼 수 있어요.
const { id } = router.query as { id?: string | string[] };
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.
뿐만 아니라, 실제 사용할 때는 아래와 같은 이유로 타입 가드를 만들어 타입을 좁힌 후 사용하기도 합니다.
// 타입 가드 없이 사용할 경우
function fetchArticle(id: string | string[] | undefined) {
// 이 함수 내에서 id를 어떻게 안전하게 사용할 수 있을까요?
// id가 undefined일 수도 있고, 배열일 수도 있어서 바로 사용하기 어렵습니다.
}
// 타입 가드를 사용한 경우
if (typeof id === 'string') {
fetchArticle(id); // 여기서 id는 확실히 string 타입입니다.
} else if (Array.isArray(id)) {
fetchArticle(id[0]); // 여기서 id는 확실히 string[] 타입이며, 첫 번째 요소를 안전하게 사용할 수 있습니다.
} else {
console.log('id가 없습니다.'); // id가 undefined인 경우를 명확히 처리할 수 있습니다.
}
<StyledWriterImg | ||
src={ | ||
comment.writer.image | ||
? comment.writer.image | ||
: "/image/profile_img_none.png" | ||
} | ||
alt="프로필 이미지" | ||
/> |
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.
웹 접근성 관점에서 alt 속성 값에 이런 경우 사용자닉네임도 넣어서 값을 만들면 대체 텍스트를 보다 더 구체적으로 만들 수 있어요.
alt={`${comment.writer.nickname}의 프로필 이미지`}
const [comments, setComments] = useState<Comment[]>([]); | ||
|
||
async function getArticleComments() { | ||
const query = { | ||
limit: 10, | ||
}; | ||
const res = await axios.get( | ||
`/articles/${id}/comments?limit=${query.limit}` | ||
); | ||
const nextComments = res.data.list; | ||
setComments(nextComments); | ||
} | ||
|
||
useEffect(() => { | ||
getArticleComments(); | ||
}, [id]); | ||
|
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.
이 부분은 아래처럼 useArticleComments
커스텀 훅 같은걸 만들어서 재사용 가능한 훅을 만들어 볼 수 있겠어요. 😸
function useArticleComments(id: string, limit: number) {
const [comments, setComments] = useState<Comment[]>([]);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
async function fetchComments() {
setLoading(true);
try {
const res = await axios.get(`/articles/${id}/comments?limit=${limit}`);
setComments(res.data.list);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error occurred'));
} finally {
setLoading(false);
}
}
if (id) {
fetchComments();
}
}, [id, limit]);
return { comments, error, loading };
}
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.
그 이후에는 아래처럼 사용해보면 되겠죠?
function ArticleCommentList({ id, limit = 10 }: ArticleCommentListProps) {
const { comments, error, loading } = useArticleComments(id, limit);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error.message} />;
if (comments.length === 0) return <EmptyCommentState />;
...
{comments.map((comment) => ( | ||
<StyledListItem key={comment.id}> | ||
<StyledContent>{comment.content}</StyledContent> | ||
<StyledBottomSection> | ||
<StyledWriterImg | ||
src={ | ||
comment.writer.image | ||
? comment.writer.image | ||
: "/image/profile_img_none.png" | ||
} | ||
alt="프로필 이미지" | ||
/> |
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.
보통 이렇게 map을 사용할 수 있는 부분은 별도의 컴포넌트로 분리하는걸 추천드려요. UI가 분리되고, 가독성이 좋아지거든요.
다른 곳에 재사용도 가능해지구요.
function CommentItem({ comment }: { comment: Comment }) {
return (
<StyledListItem as="article">
<StyledContent>{comment.content}</StyledContent>
<StyledBottomSection>
<StyledWriterImg
src={comment.writer.image || "/image/profile_img_none.png"}
alt={`${comment.writer.nickname}의 프로필 이미지`}
/>
<StyledInfo>
<StyledNickname>{comment.writer.nickname}</StyledNickname>
<StyledTime>
<FormatRelativeTime time={comment.updatedAt} />
</StyledTime>
</StyledInfo>
</StyledBottomSection>
</StyledListItem>
);
}
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.
최종적으로 아래처럼 바꿀 수 있어요.
return (
<CommentSection>
<CommentList>
{comments.map((comment) => (
<CommentItem key={comment.id} comment={comment} />
))}
</CommentList>
</CommentSection>
);
요구사항
기본
게시글 등록 페이지 주소는 “/addboard” 입니다.
게시판 이미지는 최대 한개 업로드가 가능합니다.
각 input의 placeholder 값을 정확히 입력해주세요.
이미지를 제외하고 input 에 모든 값을 입력하면 ‘등록' 버튼이 활성화 됩니다
게시글 상세 페이지 주소는 “/board/{id}” 입니다.
댓글 input 값을 입력하면 ‘등록' 버튼이 활성화 됩니다.
자유게시판 페이지에서 게시글을 누르면 게시물 상세 페이지로 이동합니다
게시글 상세 페이지 주소는 “/board/{id}” 입니다.
댓글 input 값을 입력하면 ‘등록' 버튼이 활성화 됩니다.
심화
주요 변경사항
스크린샷
멘토에게