인스타그램 클론을 진행하면서 html css js로 작업했던 결과물을 react로 전환하면서 겪언던 부분에 대해 적어보겠습니다.
로그인 부분은 id와pw에 대한 유효성 검사를 넣었습니다.
유효성 검사에 따라 로그인 버튼의 활성화 여부를 결정하고 활성화 시 enter key와 login button을누르면 main페이지로 라우팅 되게 작업했습니다.
form태그로 id와 pw에 대한 input과 button을 네스팅해서 submit에 대한 함수 1개 정의로 두개의 이벤트에 대해 동일한 효과를 주었습니다.
const REGEXP = {
emailRegExp:
/[a-zA-Z0-9.-_+!]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]{2,}(?:.[a-zA-Z0-9]{2,3})?/,
passwordRegExp: /[a-zA-Z0-9]{5,100}/,
};
validate = (value, regExp) => {
const reg = new RegExp(regExp);
return reg.test(value);
};
id에 대한 형식을 email로 정하고 이에 대한 정규표현식을 만들었습니다.
pw는 최소 5자리에 대해 정규 표현식을 만들어 최소 비밀번호 길이를 제한했습니다.
처음에는 id와 pw에 대한 유효성 검사 함수를 각각 만들었는데 공통 로직을 발견하고 하나의 함수로 리펙토링하는 과정에서 validate함수
안에서 id와 pw에대해 분기 처리를 해줘야돼서 어떻게 구성을 할까 고민하다가 최종적으로 함수의 인자로 각 경우에 대한 표현식을 넘겨주기 위해 id와pw에 대한 REGEXP객체
로 만들어서 중복 코드를 해결했습니다.
// ❕❗리펙토링 전
validateId = email => {
const emailRegExp =
/[a-zA-Z0-9.-_+!]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]{2,}(?:.[a-zA-Z0-9]{2,3})?/;
let isValidEmail = false;
const idRegExp = new RegExp(emailRegExp);
if (idRegExp.test(email)) {
isValidEmail = true;
}
return isValidEmail;
};
validatePassword = password => {
const passwordRegExp = /[a-zA-Z0-9]{5,100}/;
let isValidPassword = false;
const pwRegExp = new RegExp(passwordRegExp);
if (pwRegExp.test(password)) {
isValidPassword = true;
}
return isValidPassword;
};
// ❕❗리펙토링 후
const REGEXP = {
emailRegExp:
/[a-zA-Z0-9.-_+!]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]{2,}(?:.[a-zA-Z0-9]{2,3})?/,
passwordRegExp: /[a-zA-Z0-9]{5,100}/,
};
validate = (value, regExp) => {
const reg = new RegExp(regExp);
return reg.test(value);
};
메인에는 아쉬움이 좀 많이 남습니다.
main에서 fetch로 api데이터를 받아와서 부모에서 하나의 state로 feed와 comment를 관리해줘야하는 구조라 랜더링되는 영역이 너무 커서 React를 사용하는 의미가 없는 느낌이였습니다. 이부분에 대해 멘토님과 많은 얘기를 했고 받아오는 데이터의 구조에 따른 발생하는 현상이라 어쩔 수 없다라고 하셨습니다.
class FeedList extends Component {
constructor(props) {
super(props);
this.state = {
feedList: [],
};
}
componentDidMount() {
fetch('http://localhost:3000/data/feedData.json', {
method: 'GET',
})
.then(res => res.json())
.then(data => {
this.setState({
feedList: data,
});
});
}
받아오는 데이터를 가공해서 feed에 관한 state 1개와 comment에 관한 state 1개로 각각 독립적으로 관리해서 랜더링 되는 영역을 줄이고자했으나 멘토님이 fetch data를 저장하는 state인 feedList에 feed와 comment에 변화에 따른 결과를 다시 반영해줘야하기 때문에 결국 기존의 구조를 벗어나 하나의 state인 feedList
에서 관리하라고 하셔서 그러면 랜더링 되는 영역이 feedList로 커지는데 이렇게 할 필요가 있을까라고 질문하고 답변을 받으며 많은 얘기가 오고갔습니다.
이때문에 구조를 3번이나 다시 갈아엎어서 시간을 많이 뺏겼습니다. 🥶😱
이로인해 추가 구현할 수 있는 기능을 구현 못한 것이 좀 아쉽긴하지만, 이부분에 대해 멘토님과의 토론(?)을 통해서 구조에 대한 생각을 정립하는 과정을 가진게 뜻 깊은 기회였습니다.💯💯💯
// feedDate.json
[
{
"id": 1,
"userName": "wecode",
"city": "KAYTRANADA",
"userImgSrc": "...",
"feedImgSrc": "...",
"feedLike": true,
"feedLikeCounts": "216,608",
"content": "Welcome to world best coding bootcamp!",
"comments": [
{
"id": 1,
"userName": "wecode",
"content": "Welcome to world best coding bootcamp!",
"Likers": ["hyun", "aaa"]
},
{
"id": 2,
"userName": "joonsikyang",
"content": "Hi there.",
"Likers": []
},
{
"id": 3,
"userName": "jayPark",
"content": "Hey.",
"Likers": ["hyun"]
}
]
},
...
]
댓글 부분은 feedList의 n depth에 반영되는 부분이라 다른 기능들 보다 구현하는데 애 좀 먹었던 부분입니다.
spread문법과 Array.map/concat/filter 함수등을 같이 써서 새로운 state를 만들어줘서 기존 state인 feedList에 할당해주는 작업으로 얕은 검사를 하는 React의 특성에 맞게 로직을 짜봤습니다.
map()
으로 feedList의 각 feed에 대해 댓글이 추가된 feed의 정보인 feedId랑 비교하고 spread
를 사용하여 기존 배열을 복사하고 새로운 댓글을 추가했습니다.
onSubmitCommentForm = (e, feedId, newComment) => {
e.preventDefault();
if (!newComment) return;
const { feedList } = this.state;
const updatedFeedList = feedList.map(feed =>
feed.id === feedId
? {
...feed,
comments: [
...feed.comments,
{
id: feed.comments.length + 1,
userName: 'hyunchan',
content: newComment.trim(),
Likers: [],
},
],
}
: feed
);
this.setState({
feedList: updatedFeedList,
});
};
map()
으로 feedList의 각 feed에 대해 feedId랑 비교한 후
spread
를 사용하여 기존 feed를 복사하고 filter()
를 사용하여 삭제할 댓글을 제외한 나머지만 새로운 배열로 리턴해줬습니다.
onRemoveComment = (e, feedId) => {
const { feedList } = this.state;
const { id } = e.target;
const nextState = feedList.map(feed =>
feed.id === feedId
? {
...feed,
comments: feed.comments.filter(
comment => comment.id !== Number(id)
),
}
: feed
);
console.log('new', nextState);
this.setState({
feedList: nextState,
});
};
좋아요 구현은 위에 댓글 추가 삭제에 비해 더 시간을 들였습니다.
기존 fetchData는 각 댓글에 대한 1명의 사용자에 한해서 구조가짜여졌습니다.
그래서 좋아요에 해당되는 부분이 isLike: true||false
였습니다.
보여주는 대상이 로그인한 사용자에 국한되어 이 구조가 틀렸다곤 생각하진 않았습니다.
하지만 1개의 댓글에 여러명이 좋아요를 할 수 있다 생각하여 저는 배열로 Likers: []
이렇게 만들었습니다.
결국 fetch데이터는 4 depth가 되었고 코드를 짜면서 한 라인마다 일일이 console로 확인해가며 만들었습니다.
애먹은 부분이 삼항 연산자의 true일때의 반환값으로 값이 반영되어 복사된 comment가 되야하는데 이부분을 캐치하지 못하고 comment.Likers
로 줘서 comment
의 값을 덮어쓰게 된 현상이 일어나 문제를 해결하는데 시간이 많이 할애됐습니다.🥶😱
괜히 구조를 바꿔서 이렇게 시간을 많이 잡아먹나 생각했지만 api에서 받아오는 데이터의 형식이 이럴수도 저럴수도 있다 생각하고 삽질하며 구현에 성공했습니다.
onToggleLike = (e, feedId) => {
const { feedList } = this.state;
const { commentid } = e.target.dataset;
const updatedToggleHeart = feedList.map(feed =>
feed.id === feedId
? {
...feed,
comments: feed.comments.map(comment =>
comment.id === Number(commentid)
? comment.Likers.includes('hyunchan')
? {
...comment,
Likers: comment.Likers.filter(
liker => liker !== 'hyunchan'
),
}
: { ...comment, Likers: comment.Likers.concat('hyunchan') }
: comment
),
}
: feed
);
this.setState({
feedList: updatedToggleHeart,
});
};
wordList
는 임의의 사용자로 검색 대상을 만들었습니다.
filtering기능은 regExp를 사용해서 구현했습니다. 배열 method인 includes로 구현할수도 있었지만 많이 활용되는 regExp를 공부하면서 로그인에 적용했듯이 여기서도 적용했습니다.
const wordList = [
'wecode_bootcamp',
'wecoffee__',
'we',
'jiwoo',
'hyun',
'chan',
];
const searchInputWord = (e) => {
let word = e.target.value;
let regExp = new RegExp('([a-zA-Z0-9]*)?' + word + '([a-zA-Z0-9]+)?');
let newWords = wordList.filter((word) => regExp.test(word) === true);
console.log(newWords);
searchResultsContainer.innerHTML = '';
newWords.forEach((word) => {
searchResultsContainer.insertAdjacentHTML(
'beforeend',
`<li class="results">
<div class="result">
<img src="./image/user-icon.jpg" alt="user님의 프로필 사진" />
<div class="searched-user">
<a class="searched-username" href="">
${word}
</a>
</div>
</div>
</li>`,
);
});
};
프로필 메뉴는 제가 구현하지 못한 기능입니다. 프로필 메뉴 외부 영역 클릭 시 메뉴가 닫히는 기능을 구현해야하는데 당시 마감시간이 촉박해서 추가 기능인 이부분을 포기했습니다.
후에 이 기능을 구현한 사람들에게 어떻게 구현했는지 물어봤더니 body에 이벤트를 주고 클릭된 영역이 프로필 메뉴면 보이고/안보이게 기능을 구현했다라고 들었습니다.
이 부분은 모달창을 구현할 때에도 요긴하게 쓰일거 같습니다.
처음으로 클론다운 클론 프로젝트를 한거 같습니다.
지금까지 클론은 영상을 보면서 따라하는 수준이였지만 이런거라도 해본 적이 있어서 그나마 이정도의 결과가 나온게 아닌가 생각이 듭니다.
구현을 하면서 미흡한 부분이 많고 이러한 부분에 고민하고 다른 사람들의 생각을 공유하면서 보는 관점을 기를수 있는 기회를 가졌다고 생각합니다.
특히 멘토님과의 토론 같지않은 토론은 재밌었습니다. 앞으로도 많은 질문과 재밌거리(???)를 들고 찾아가겠습니다😍😍😍