첫 계획 후 정확히 3개월 동안 달려서 겨우 프로젝트를 끝냈다.
프로젝트를 진행하면서 배운 내용들과 어려웠던 점들을 해결한 내용을 정리해 보려고 한다.
두 번째 프로젝트
첫 번째 프로젝트가 끝나고 약 1달 반 정도를 알고리즘과 개인 공부를 하고 있을 때 타입스크립트 관련해서 공부를 조금 해보고 싶었다. 그러던 찰나에 고등학교 때 친한 친구가 같이 프로젝트를 하자고 제안했었고 가벼운 고민 끝에 타입스크립트를 공부해보고자 프로젝트를 진행하기로 했다.
기획과 마이레코드
처음 친구와 프로젝트를 하기 위해 회의를 하면서 어떠한 웹페이지를 만들까 고민을 했었다. 친한 친구와 했던 회의였기에 재미있는 아이디어가 많이 나왔다. 그 안에서 우리 둘 다 취업을 위해 포트폴리오를 준비하는 학생이었고 각자의 기술 스택을 위해서 기술 블로그를 운영하고 있었다. 여기에서 우리는 기술 스택 적립을 위한, 각자의 분야를 위한 블로그를 만들어보기로 결정하게 되었다!
그렇게 우리는 벨로그와 티스토리를 모티브로 해서 필요한 기능이 무엇일지 생각해보았다.
- 게시물의 CRUD
- 카테고리에 따라 볼 수 있는 다양한 분야
- 댓글과 대댓글 기능
- 로그인 회원가입
- 마이페이지
기본적인 기능들을 생각해 낸 후 우리는 각자 필요한 기술들을 정리하고, 목업 디자인을 짜기로 하고 바로 작업에 들어갔다!
프로젝트 My Record
프로젝트를 시작하기 앞서서 만들 웹페이지의 이름을 정하는 시간을 가졌다. 둘 다 이름이 예쁘게 정하는 게 프로젝트 진행에 반을 차지한다는 생각을 가지고 있기에 정말 여러 가지 의견들이 나왔다.
Idea Cloud, E. T Each Trace, 취업어때? consult Log....
이 밖에도 약 20개 이상이 더 나왔는데, 그중에서도 기록이라는 의미의 Record가 가장 우리 프로젝트의 의미와 어울려서 My Record라는 이름이 탄생하게 되었다!!
로고 및 UI 선정
"기록이라는 의미에 맞게 필기체가 좀 있었으면 좋겠어"
![]() |
![]() |
웹 서비스의 이름이 나왔으므로 다음으로 로고와 디자인을 선정했다. 기록이라는 뜻과 같이 로고는 약간 필기체 형식으로 되었으면 좋겠어서 필기체의 로고와 파비콘까지 해서 두 개의 로고를 만들었다!
블로그에 맞게 UI는 최대한 단순한 게 좋을 것 같아!
우리 둘의 공통된 의견으로 블로그는 최대한 간편한 디자인이었으면 좋겠다는 의견이 나와서 필요한 디자인적 부분만 넣고 애매한 것들은 전부 빼기로 했다.
그렇게 나온 화면 디자인!
우리는 이대로 개발에 들어가기로 한다.
개발 중 발생한 트러블 이슈
Login page를 Modal로 보여주고 싶은데 Modal의 화면 크기가 맞지 않는 이슈
//Tobbar.tsx
function Topbar() {
const [Keyword, setKeyword] = useState("");
const onchange = (e: React.ChangeEvent<HTMLInputElement>) => {
setKeyword(e.currentTarget.value);
};
const onclick = () => {
//클릭 시 함수
console.log(Keyword);
};
return (
<div className="topbar">
<div className="bar_logo">
<FaAdversal className="logo" />
<a>MyRecord</a>
</div>
<div className="bar_search">
<input type="text" placeholder="Search..." value={Keyword}
)
}
로그인 페이지를 모달로써 띄우고 싶었는데 모달의 화면 크기가 맞지 않아서 뒷부분이 잘리는 이슈가 발생했다.
그렇기에 나는 TopBar에 모달을 지정하고 커스텀 스타일로 바꾼 후에 불러오는 방식을 택해서 위 이슈를 해결했다.
function Topbar() {
const [Keyword, setKeyword] = useState("");
const [OpenModal, setOpenModal] = useState(false)
const onchange = (e: React.ChangeEvent<HTMLInputElement>) => {
setKeyword(e.currentTarget.value);
};
const onOpen = () => {
setOpenModal(true)
}
const onclick = () => {
//클릭 시 함수
console.log(Keyword);
};
const handleCloseModal = () => {
setOpenModal(false)
}
return (
<div className="topbar">
<div className="bar_logo">
<FaAdversal className="logo" />
<a>MyRecord</a>
</div>
<div className="bar_search">
<input type="text" placeholder="Search..." value={Keyword} onChange={onchange} />
<button className="search_logo">
<FaSearch className="logo" onClick={onclick}/>
</button>
</div>
<div className="bar_info">
<button className="small_btn" onClick = {onOpen}>Login</button>
<button className="small_btn">Logout</button>
<button className="account_logo">
<FaUserCircle className="logo" />{" "}
</button>
</div>
<Modal className = "Modal" isOpen ={OpenModal} ariaHideApp= {false} onRequestClose = {handleCloseModal}>
<Login setopenmodal = {setOpenModal} openmodal = {OpenModal}/>
</Modal>
</div>
);
}
export default Topbar;
대댓글 기능 구현 중 대댓글 작성 완료 시 리 렌더링이 되지 않는 이슈
function Reply(props: IReply) {
useEffect(() => {
async function CallComment(postId: string) {
await axios.get(`/api/post/${postId}/comment`).then((res) => setPostCommentData(res.data));
},[])
//대댓글 작성 함수
const onsubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setbtnLoading(true);
await axios
.post(
`/api/comment/${props.MainPostId}`,
{
comment: recomment,
parentCommentId: props.commentId,
},
{
headers: { Authorization: `Bearer ${sessionStorage.getItem("token")}` },
},
)
.then((res) => {
console.log(res.data);
setrecomment("");
setbtnLoading(false);
});
};
//댓글 삭제 함수
const DeleteCommentHandler = async () => {
axios
.delete(`/api/comment/${props.MainPostId}/${props.commentId}`, {
headers: { Authorization: `Bearer ${sessionStorage.getItem("token")}` },
})
.then(() => {
dispatch(OpenModalHandler("댓글 삭제가 완료되었습니다."));
});
};
const Recomment = props.commentList.map((item) => {
return (
<li className="childReply_item" key={item.commentId}>
<div className="comment_1">
<Link to="" className="reply_thumb">
<img
src={
item.userImage === null || item.userImage === "string"
? "https://myrecord.s3.ap-northeast-2.amazonaws.com/7e1436db-68ea-45c5-b997-6de46f17280b.png"
: item.userImage
}
/>
</Link>
<div className="reply_box">
<Link to="" className="link_name">
<span>{item.userName}</span>
</Link>
<p>{item.comment}</p>
<p className="date">{item.commentTime}</p>
</div>
</div>
</li>
);
});
return (
<li className="comment">
<Link to="" className="reply_thumb">
<img
src={
props.userImage === null || props.userImage === "string"
? "https://myrecord.s3.ap-northeast-2.amazonaws.com/7e1436db-68ea-45c5-b997-6de46f17280b.png"
: props.userImage
}
/>
</Link>
<div className="reply_box">
<Link to="" className="link_name">
<span>{props.userName}</span>
</Link>
<p>{props.comment}</p>
<p className="date">{props.commentTime}</p>
<div style={{ display: "flex", marginTop: "18px" }}>
<div className={props.commentList.length === 0 ? "modify_none" : "modify"} onClick={() => setstate(!state)}>
<FaPlusSquare className="plus" size="19" />
<p>{props.commentList.length}개의 답글</p>
</div>
<h5 className="recomment_btn" onClick={() => setformState(!formState)}>
답글달기
</h5>
<h5 className="recomment_btn" onClick={DeleteCommentHandler}>
삭제
</h5>
</div>
<form className={formState ? "" : "recomment_form"} onSubmit={onsubmit} style={{ marginBottom: "70px" }}>
<div className="reply_write">
<div className="form_content">
<textarea
value={recomment}
onChange={commentOnchangeHandler}
placeholder="답글을 입력해주세요."
></textarea>
</div>
<div className="form_reg">
<button type="submit" className="comment_btn">
{btnLoading ? <Loader type="Oval" color="#3d66ba" height={15} width={15} timeout={3000} /> : "등록"}
</button>
</div>
</div>
</form>
<ul className={state ? "childReply_list" : "childReply_list_none"}>{Recomment}</ul>
</div>
</li>
);
}
PostView.tsx에서 불러온 Reply 컴포넌트에서 댓글 리스트를 불러오는 함수를 실행했기에, 댓글 리스트 상태가 변경된 것을 컴포넌트가 감지하지 못해서 리스트가 최신화되지 않았다.
interface IReply {
commentId: number;
userName: string;
userImage: string;
comment: string;
commentTime: string;
commentList: Array<IArray>;
}
function Reply(props: IReply) {
return (
<li className="comment">
<Link to="" className="reply_thumb">
<img
src={
props.userImage === null || props.userImage === "string"
? "https://myrecord.s3.ap-northeast-2.amazonaws.com/7e1436db-68ea-45c5-b997-6de46f17280b.png"
: props.userImage
}
/>
</Link>
<div className="reply_box">
<Link to="" className="link_name">
<span>{props.userName}</span>
</Link>
<p>{props.comment}</p>
<span className="date">{props.commentTime}</span>
<div className="modify">
<Link to="">
<p>답글</p>
</Link>
<Link to="">
<p>수정/삭제</p>
</Link>
</div>
</div>
</li>
);
}
댓글 리스트를 불러오는 함수를 상위 컴포넌트인 PostView 컴포넌트에서 실행 후 props로 내려주는 방식으로 해결
회원정보 수정 후 이동한 경로에 대해 화면이 표시되지 않는 이슈
import { useDispatch } from "react-redux";
import { SideBarNoneHandler, SideBarOpenHandler } from "../modules/action-creator";
function ChangeInfoContainer() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(SideBarNoneHandler());
},[])
return(
<div className="changeInfo_container">
<Router>
<nav className="container_nav">
<SmallNavBar />
</nav>
<section>
<Switch>
<Route exact={true} path="/changeinfo" component={ChangeInfo}></Route>
<Route exact={true} path="/changeinfo-category" component={ChangeCategory} />
<Route exact={true} path="/changeinfo-checkPassword" component={PasswordConfirm} />
</Switch>
</section>
</Router>
)
}
중첩 라우팅에서 하위 라우터 컴포넌트에 Router로 감쌌기 때문에 상위 컴포넌트까지 경로가 전달되지 않았기 생기는 오류였다.
import { useDispatch } from "react-redux";
import { SideBarNoneHandler, SideBarOpenHandler } from "../modules/action-creator";
function ChangeInfoContainer({ match }) {
console.log(match);
const dispatch = useDispatch();
useEffect(() => {
dispatch(SideBarNoneHandler());
return(
<div className="changeInfo_container">
<>
<nav className="container_nav">
<SmallNavBar path={match.path} />
</nav>
<section>
<Switch>
<Route exact={true} path={`${match.path}`} component={ChangeInfo}></Route>
<Route path={`${match.path}/category`} component={ChangeCategory} />
<Route path={`${match.path}/checkPassword`} component={PasswordConfirm} />
</Switch>
</section>
</>
)
감싼 Router를 제거하고 경로를 새로 설정해서 해결
느낀 점
약 3개월 동안 개발 끝에 웹 페이지를 완성시켰다. 위에서 소개되지 않은 자잘한 이슈들도 많았고, 정말 간단한 것들을 못 찾아 2-3일씩 고전한 것들도 있다. 프로젝트는 종료되었지만 아직 자잘한 에러들이 나와 현재 리팩토링을 계속 진행 중이다!
그리고 타입스크립트를 처음 써보았는데 코드가 길어지고 어려워서 처음엔 내가 작성한 코드도 보기 어려웠는데 지금은 왜 타입스크립트를 사용하는지 알 것 같다. 그리고 자바스크립트보다 좀 더 재밌는 느낌...? 확실히 에러도 초기에 잡을 수 있고 내가 당연하다고 짰던 코드들도 타입스크립트 덕분에 옳지 않은 코드라는 것을 알고 바꾼 것도 있다.(대표적으로 props로 함수를 내려줄 때)
리액트를 사용한 두 번째 프로젝트였지만 아직도 모르는 것 투성이었다. 하지만 저번과는 다르게 혼자서 처음부터 끝까지 전부 마무리했기에 더 의미가 있던 프로젝트 같았다. 저번에는 구현에만 급급했다면 이번에는 클린 코드도 생각하며 코드를 짜려고 노력했다.(물론 잘 안된 거 같긴 하다..) 저번 프로젝트가 끝나고 간단한 블로그 만들기는 쉽게 만들 줄 알고 쉽게 생각했다가 엄청 두들겨 맞았다... 아직 갈 길이 멀다. 더 성장해야지.
마이레코드 프론트 GitHub
GitHub - Organization-MyRecord/client: React와 TypeScript를 이용한 취준생들의 기술스택을 전문적으로 정리
React와 TypeScript를 이용한 취준생들의 기술스택을 전문적으로 정리하기 위한 블로그, My Record! - GitHub - Organization-MyRecord/client: React와 TypeScript를 이용한 취준생들의 기술스택을 전문적으로 정리하
github.com
마이레코드 시연 영상
'회고' 카테고리의 다른 글
[블랙커피 Vanilla JS Lv.1] 챕터 1(1주차) (0) | 2022.05.15 |
---|---|
프로젝트 회고록 (0) | 2021.08.28 |