우리가 만든 페이지부터 자랑해보자!
로그인/회원가입 페이지
메인 페이지
피드 상세 페이지
캐릭터/카테고리 페이지
검색 모달창
제품상세 페이지
카카오 프렌즈샵 클론을 하게 되다. 그리고 PM을 맡게 되다.
프론트엔드에는 준우님, 성훈님, 나은님 그리고 나 이렇게 네 명과 백엔드에는 왕록님, 정원님 까지 총 6명이 한 팀이 되었다.
일단 나는 모든게 두렵고 자신이 없었다. 어떻게 백엔드와 소통이 되는지 그 구조 자체도 온전히 이해가 이루어지지 않은 상태였고 과제를 하나씩 진행해나갈 때마다 나는 다른 이들보다 느리다는 것에 많은 압박감을 느끼고 있던 때였다.
과연 내가 이들 안에서 수치적으로 '얼만큼' 내 몫을 해낼 수 있을까 하는 걱정이 앞섰다. 내가 선택했던 사이트가 지정이 되어서 얼떨결에 PM을 맡게 되다보니 (사실 큰 의미가 없는)직책으로 인한 책임감이 부담스럽게 느껴졌다.
잘 모르는 상태에서 하려다보니, 최대한 알려준 대로 따라가 보기로 했다. 그래서 트렐로를 적극적으로 활용했다. 뭐든 다 적고 매일 아침마다 스탠드업 미팅을 하며 트렐로에 기록을 했다. 아무래도 내가 프론트엔드이다 보니, 백엔드 쪽으로 신경을 못써드려서 개인적으로 죄송한 마음이다. 그저 진행상황을 얘기해주는 것 말고는 할 수 있는게 없었다.
첫 주에는 각자 구현할 페이지를 하나씩 맡아서 진행하기로 했다.
나름대로 언제까지 레이아웃을 완성하고 -> 그 후에는 기능 구현을 한 뒤, -> 백엔드와 맞춰보자! 란 식으로 큰 틀을 잡고 진행을 하였는데
(예를 들어, 댓글이 최신순으로 달려야 해서, 나는 단순하게 댓글 리스트 맵 함수에 reverse를 사용하여 댓글이 역순으로 달리도록 기능을 구현했는데, 백엔드에서 최신순으로 데이터를 보내줄 수 있어서 굳이 내가 역순으로 정렬할 필요가 없었고, 댓글을 달고 삭제하기 위해서는 로그인을 해서 인증/인가를 거친 상태여야 가능하다보니 내가 삭제 기능을 구현한다고 해결되는 것이 아니라, 백엔드에게 fetch 메소드 delete를 요청해야 가능한 것이었다.)
사실 이런 부분들을 PM이 주도적으로 이끌었어야하는데 백엔드에 대한 지식도 부족하고 프로젝트 자체가 처음이다보니 정말 너무너무나 서툴었고 그래서 우리 팀원들에네 너무너무 미안한 마음이다.. 😭
분명 쉬지않고 코딩만 한 것 같은데 결과물은 왜 어설픈걸까?
공감 됐던 짤이라 첨부해본다.
내가 댓글 상세 페이지를 구현하면서 모르는 것이 너무 많아, 깃허브에서 우리 동기들의 위스타그램 코드를 다 뜯어봤다. 생각보다도 대부분의 동기들이 필수 기능뿐만 아니라 추가 기능들을 다 구현해놨었고 배운 세션을 참고하여 코드의 완성도를 높인 동기들이 너무나 많았다.
다시금 나의 안일함과 부족함을 절실히 깨닫는 시간이었다.
더 열심히, 그리고 더 투자하여 공부를 해야겠다는 다짐을 가지게 되었다.
사실 내가 맡은 댓글 상세페이지는 이전에 진행했던 위스타그램 과제와 비슷한 부분이 굉장히 많았다. 그럼에도 위스타그램에서 간신히 필수기능만 구현했던 나에게는 어려운 과제였다.
기억해두고 싶은 코드를 잠깐 기록해보겠다.
//SubNav.js
class SubNav extends React.Component {
constructor() {
super();
this.state = {
toggleOn: false,
};
}
toggleOn = () => {
this.setState({
toggleOn: true,
});
};
toggleOff = () => {
this.setState({
toggleOn: false,
});
};
/*네브 바에서 돋보기 아이콘을 누를 시,
search bar 모달창을 띄우기 위해 작성한 함수.
나은님의 도움을 받아 좀 더 명확하게 이해할 수 있게 되었다.*/
render() {
return (
<div className="wrapMain">
{this.state.toggleOn ? (
<Search toggleOff={this.toggleOff} />
//search component 안에 props를 지정해주었음.
) : (
<div className="mainNav">
<div className="navBarFirst">
<Link to="/">
<FaChevronLeft size="22" />
<FaHome className="homeIcon" size="22" />
</Link>
<h1 className="title"> {this.props.title} </h1>
/*이 부분은 각 페이지마다 sub nav의 구조는 같으나,
제목이 달라서 제목 부분만 props로 보내주고 있는 것을 알 수 있다.*/
<div>
<button className="navIcon" onClick={this.toggleOn}>
<FaSearch size="22" />
</button>
<button className="navIcon">
<FiGlobe size="22" />
</button>
</div>
</div>
</div>
)}
</div>
);
}
}
//MainNav.js
<ul className="navBarSecond">
{menuList.map(menu => (
<Link to="/finish">
<li className="width">{menu}</li>
</Link>
/*이 부분은 메인 네브 바의 카테고리가 반복됨으로
맵 함수를 이용하여 간략하게 줄인 것이다.*/
))}
</ul>
</div>
)}
</div>
);
}
}
export default MainPageNav;
const menuList = ['오늘', '신규', '인기', '마이'];
//MainNav.scss
.width {
display: flex;
justify-content: center;
width: 160px;
font-size: 16px;
line-height: 40px;
&:hover {
border-bottom: black solid 4px;
}
}
//메인 네브바에 준 호버 효과
//Routes.js
<Route exact path="/feed/:id" component={Feed} />
//Feed.js
componentDidMount() {
fetch(`http://192.168.200.131:8000/feed/${this.props.match.params.id}`)
/*match.params를 이용하여 페이지마다
다른 데이터를 각각 받아올 수 있도록 지정.*/
.then(res => res.json())
.then(res => this.setState({ content: res.result }));
}
pressEnter = async e => {
await fetch(
`http://192.168.200.131:8000/feed/reply?feed_id=${this.props.match.params.id}`,
/*페이지 마다 각각 다른 댓글 데이터를 보내야하므로
여기 또한 match.params 를 이용했다.*/
{
method: 'POST',
body: JSON.stringify({
id: this.state.id,
content: this.state.value,
}),
}
)
.then(res => res.json())
.then(res => res.status);
e.preventDefault();
if (this.state.value === '') {
alert('내용을 입력해주세요');
return;
}
this.setState({
commentList: this.state.commentList.concat([
{
userId: this.state.id,
content: this.state.value,
},
]),
});
};
//Feed.js
render() {
const settings = {
dots: true,
infinite: false,
speed: 500,
slidesToShow: 1,
slidesToScroll: 1,
};
const title = '게시물';
const {
content,
isLoginModalView,
heartColor,
isShareModalView,
} = this.state;
const changeHandleBtnColor = this.state.value.length >= 1;
return (
<>
<SubNav title={title} />
//props 지정.
<div className="feedPage">
<div className="feedBox">
<div className="feedBoxHeader">
<div className="feedBoxHeaderImg">
<img
className="headerImg"
src={content?.profile_picture}
/*백엔드로부터 받은 데이터의 객체 내의 속성값에
접근하기 위해 옵셔널 체이닝을 이용하였다.*/
alt="이미지"
/>
</div>
<div className="nameAndTime">
<div className="characterName">{content?.username}</div>
<div className="time">{content?.datetime}</div>
</div>
</div>
<StyledSlider className="feedBoxImg" {...settings}>
{content.image_url?.map((list, index) => (
<img key={index} className="mainImg" src={list} alt="이미지" />
))}
</StyledSlider>
//이미지 슬라이드 기능은 라이브러리를 이용
<div className="feedBoxIcon">
{isLoginModalView ? '' : ''}
{heartColor ? (
<div className="heartIcon">
<button onClick={this.colorChangeBtn}>
<FaRegHeart size="24" />
</button>
</div>
) : (
<div className="heartIconColorChange">
<button onClick={this.colorChangeBtn}>
<FaHeart color="red" size="24" />
</button>
</div>
)}
<div className="chatIcon">
<button onClick={this.goToFeedDetail}>
<BsChat size="24" />
</button>
</div>
<div className="replyIcon">
{isShareModalView && (
<ShareModal shareHandleModal={this.shareHandleModal} />
)}
<button onClick={this.shareHandleModal}>
<BsReply size="32" />
</button>
</div>
</div>
<div className="feedLikeCount">
좋아요
<span className="feedLikeCountUpDown">{content?.like_count}</span>
개
</div>
<p className="feedContentTitle">{content?.title}</p>
<p className="feedContent">{content?.content}</p>
</div>
<Comment
value={this.state.value}
inputComment={this.inputComment}
changeHandleBtnColor={changeHandleBtnColor}
pressEnter={this.pressEnter}
/>
{content.reply?.map(comment => (
//댓글의 정보들을 맵 함수를 이용해 돌리고,
//props를 통해 자식 component에게 보내줌
<CommentBox
key={comment.id}
name={comment.reply_username}
text={comment.reply_content}
likeCount={comment.like_count}
createdAt={comment.datetime}
handleCommentDelete={this.handleCommentDelete}
/>
))}
</div>
<Footer />
</>
);
}
}
export default Feed;
const StyledSlider = styled(Slider)`
ul.slick-dots {
margin-bottom: -20px;
}
.slick-prev {
poacity: 0.6;
margin-left: 45px;
z-index: 9;
}
.slick-next {
margin-right: 60px;
poactiy: 0.6;
}
.slick-prev:before {
color: black;
font-size: 30px;
}
.slick-next:before {
color: black;
font-size: 30px;
}
.slick-disabled {
display: none !important;
}
`;
MERGE가 뭐지? 머지가 왜 머지?
둘째 주에는 정말 충돌의 전쟁이었다. 기능 구현도 기능 구현인데 충돌을 해결하느라 많은 시간을 할애했다.
그리고, merge로 인한 충돌 뿐만 아니라.. 각자 만들고 있던 페이지가 하나의 단독적인 페이지가 아니고 연결되는 페이지였어서 merge를 하고보니 수정할 내용들이 계속 생기고 최상단이었던 component 위로 또 부모가 생기는 경우가 발생해서 부모였던 component는 자식이 되고 구조가 엉켜버려서 그거 해결하는데도 꽤 애먹은 기억이 난다.
독단적으로 진행하는 기능 구현은 의미가 없다.. 무조건 소통해야 한다.
여러 브랜치를 생성하여 이동하고 수정하고 푸시하여야 하다보니, 잦은 커밋이 발생하게 되었다ㅠ 거기다 충돌 해결하고 머지하면 또 충돌이 나서 해결하고를 반복.. 허허허 '최종 진짜 진짜 최종'의 연속이었다.
그래서 이번 프로젝트를 통해 얻은게 뭔데?
왕록님과 정원님의 완성된 모델링!
진짜 우리팀의 백엔드는 최강자였다. 데이터는 열심히 주시는데 왜 받아먹질 못하니.. ㅠㅠ 제대로 활용하지 못해서 죄송한 마음 뿐.. 부탁할 때마다 흔쾌히 들어주시고 항상 파이팅 넘치게 긍정적으로 도와주셔서 정말 감사했다. 정말 좋은 분들!!! 항상 오께이~~ 오케이~~ 하시면서 다 가능하고 다 된다고 서포트 해주셔서 너무 고마웠어요!!!!!
직접 캐릭터를 그려서 제공해준 나은님.. 최고.. 너무 귀여워오.. 계속된 실패에 좌절해있을 때마다 서로 할 수 있다고 응원해주던 우리 프론트.. 진짜 2주 내내 즐겁게 프로젝트에 임할 수 있었다. 그리고 자기 일 처럼 옆에서 도와주셔서 좋은 자극도 넘 많이 받았다. 우린 언제나 할 수 있어빌리티~~
기능 구현을 하면서 나의 부족함을 절실히 깨달았고, 동시에 쓰면서도 헷갈리던 것들을 명확하게 배울 수 있는 시간이었다. 내가 데이터를 받고 동시에 요청하면서 화면에 랜더될 때 그 짜릿함! 함수 한 줄에 달라지는 화면을 보고 얻는 성취감! 그리고, 위에는 프론트엔드 목표인데 이 전에 나는 해내지 못했던 것들이었는데 이번 프로젝트 통해서 목표를 이루었으니 스스로를 칭찬해본다.. 쓰담쓰담.
지은님 마지막 사진 ㅋㅋㅋㅋ(시선강탈!)
수고 많으셨어요 지은님 😋👏🏻