5주차 과제는 로그인 + 댓글 기능 구현하기!
로그인 & 로그아웃
기능로그인
유저 네임
으로 쓰이고 로그아웃
버튼이 나타남styled component로 구현하였고, recoil을 사용하여 현재 로그인한 유저 네임을 전역 상태로 관리하였다.
├── src
│ └── Components
│ ├── Comments.js
│ ├── Comments.module.css
│ ├── CommentsForm.js
│ ├── CommentsForm.module.css
│ ├── LoginForm.js
│ └── LoginForm.module.css
│
│ └── Function
│ └── currentTime.js
│
│ └── State
│ └── userNameState.js
│
├── App.js
├── index.js
├── Wrapper.js
└── Wrapper.module.css
로그인
유저 네임
으로 쓰이고 로그아웃
버튼이 나타남src/LoginForm.js
function LoginForm() {
const setUserName = useSetRecoilState(userNameState);
// useSetRecoilState : 상태를 업데이트하는 setter 함수.
// 현재 로그인한 userName을 전역으로 관리한다.
const [input, setInput] = useState("");
const [state, setState] = useState({
isLogined: false,
userName: ""
});
const loginText = state.isLogined ? "LOGOUT" : "LOGIN";
function onChangeInputHandler(e) {
const text = e.target.value;
setInput(text);
}
function onClickSubmitHandler(e) {
e.preventDefault();
if (!state.isLogined){
setState({
userName: input,
isLogined: true,
});
setUserName(input);
return;
}
setState({
isLogined: false,
userName: ""
});
}
const inputText = <input type="text" onChange={onChangeInputHandler}/>;
return (
<div>
<form>
{state.isLogined ? <h2>{state.userName}</h2> : inputText}
<button
type="button"
onClick={onClickSubmitHandler}>
{loginText}
</button>
</form>
<CommentsForm isLogined={state.isLogined} userName={state.userName}/>
</div>
)
}
export default LoginForm;
setState
로 로그인 여부를 판단하는 isLogined와 userName을 관리하였다.
useReducer
를 사용하여 토글 형식으로 구현하고 싶었지만, userName
을 변경하려면 state.userName
과 같이 객체 속성으로 접근하여 변경해야 했다.
localStorage를 사용하여 브라우저에 사용자 정보를 저장했다.
웹 스토리지 (localStorage, sessionStorage) 사용법을 참고했다.
src/LoginForm.js
const getUserName = JSON.parse(window.localStorage.getItem("user-name"));
// 로컬 스토리지에 저장된 "user-name"의 value 가져오기
useEffect(() => {
// 새로고침 했을 때, 현재 로그인된 user-name이 존재하는 경우에만 로그인 상태 유지
const storedUserName = JSON.parse(window.localStorage.getItem("user-name"));
if (storedUserName) {
setState({
isLogined: true,
userName: storedUserName
});
setUserName(storedUserName);
}
},[state.userName]); // 의존성 배열을 생략했을 때 무한 루프 발생
function onClickSubmitHandler(e) {
e.preventDefault();
if (!state.isLogined){
window.localStorage.setItem("user-name", JSON.stringify(input));
// input에 저장된 값을 로컬 스토리지에 저장
setState({
userName: getUserName, // input -> getUserName
isLogined: true,
});
setUserName(getUserName); // input -> getUserName
return;
}
localStorage.removeItem("user-name");
// 로그아웃 시 로컬 스토리지에 저장된 값을 지워주지 않으면 로그아웃 불가능
setState({
isLogined: false,
userName: ""
});
}
useEffect
의 두번째 파라미터를 생략했을 때 무한 루프가 발생했다.
Warning: Maximum update depth exceeded. This can happen when a component
calls setState inside useEffect, but useEffect either doesn't have a
dependency array, or one of the dependencies changes on every render.
로그아웃 버튼 클릭 시 로컬 스토리지 값을 삭제했는데, 새로고침 후에 없는 값을 찾으려고 하니 렌더링이 계속 발생하는 것 같았다.
빈 배열을 넣었을 때는 로그인 상태로 변경되긴 하지만 유저 네임이 null 값으로 들어갔다.
src/CommentsForm.js
function CommentsForm(props) {
const isLogined = props.isLogined;
const userName = props.userName;
const [comment, setComment] = useState({
userName: "",
content: ""
});
const [addComment, setAddComment] = useState([]);
function onChangeInputHandler(e) {
const text = e.target.value;
setComment({
content: text,
});
}
function onClickSubmitHandler(e) {
e.preventDefault();
const commentObject = {
...comment,
userName: userName,
date: currentTime(),
id: `${userName+currentTime()}`
};
const commentArray = [...addComment,commentObject];
setAddComment(commentArray);
}
function onClickDeleteHandler(e) {
const deleteTarget = e.target.parentNode;
const deleteTargetId = deleteTarget.id;
const deletedArray = addComment.filter(element => {
return element.id !== deleteTargetId;
})
setAddComment(deletedArray);
}
const disabledCommentsForm =
<form>
<textarea rows="5" cols="20" onChange={onChangeInputHandler} disabled/>
<button onClick={onClickSubmitHandler} disabled>Tweet</button>
</form>;
const abledCommentsForm =
<form>
<textarea rows="5" cols="20" onChange={onChangeInputHandler}/>
<button onClick={onClickSubmitHandler}>Tweet</button>
</form>
return(
<>
{isLogined ? abledCommentsForm : disabledCommentsForm}
{addComment.map((element,index) => {
return <Comment
value={element}
isLogined={isLogined}
key={element.date+JSON.stringify(index)}
onDelete={onClickDeleteHandler}
/>
})}
</>
)
}
export default CommentsForm;
setComment
: 입력하는 하나의 댓글을 관리. 입력된 댓글내용을 임시저장하는 용도
setAddComment
: 입력된 댓글 목록 전체를 관리. 댓글 추가, 삭제 기능 구현
textarea의 onChange 이벤트에 setComment
로 comment
에 유저 네임과 댓글 내용을 저장한다.
button의 onClick 이벤트에 새로운 객체를 생성하여 comment
와 댓글 생성 시간, id를 추가한 후 또 새로운 배열에 객체를 추가하여 setAddComment
에 인자로 넣어주었다.
onChange 이벤트에서 시간을 설정해버리면 내용을 작성하고 버튼을 클릭하기 전까지의 시간은 계산이 되지 않는다.
최대한 원본 객체와 배열을 수정하지 않으려고 했다.
src/Comment.js
function Comment(props) {
const current = useRecoilValue(userNameState);
// useRecoilValue : 업데이트 된 상태를 불러오는 함수.
const {userName, content, date, _} = props.value;
const onDelete = props.onDelete;
const isLogined = props.isLogined;
const isAuthor = isLogined && current === userName;
return(
<div id={userName+date}>
<div>
<span>USER 👤 - {userName}</span>
<h3>{content}</h3>
<span>{date}</span>
</div>
{isAuthor && <button
type="button"
onClick={onDelete}>
Delete</button>}
</div>
)
}
export default Comment;
recoil을 사용하여 현재 로그인하고 있는 유저 네임을 관리하였다.
prop
으로 받은 유저 네임과 useRecoilValue
로 가져온 유저 네임을 비교하여 같은 경우에만 삭제 버튼을 렌더링하였다.
$ npm install recoil
App.js
import React from 'react';
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
} from 'recoil';
function App() {
return (
<RecoilRoot>
<Wrapper />
</RecoilRoot>
);
}
export default App;
src/State/userNameState.js
import { atom } from 'recoil';
const userNameState = atom({
key: 'userNameState',
default: "",
});
export { userNameState };
setReducer
에서 setState
로 변경한 또 다른 이유는 recoil을 간편하게 사용하기 위해서이다.
처음 recoil을 사용해봤는데 간단하게 적용할 수 있었다.
Recoil 공식 문서를 참고했다.
각 댓글마다 생성시간 + 30초 후에 삭제하는 것 보다 매 초마다 30초가 지난 댓글을 삭제하는 방식으로 구현하는 게 더 쉬울 것이라고 Deok님께서 코멘트 해주셨다.
src/CommentForm.js
useEffect(() => {
const time = new Date(currentTime());
const deleteComment = setInterval(() => {
const deletedCommentsArray = addComment.filter((comment) => {
const commentedTime = new Date(comment.date);
return time.getSeconds() - commentedTime.getSeconds() < 30
});
setAddComment(deletedCommentsArray);
}, 1000);
return () => clearInterval(deleteComment);
});
처음엔 각 컴포넌트마다 30초 후에 지워지는 방식으로 하위 컴포넌트에
const $comment = document.getElementById("comment-div");
.
.
$comment.remove();
이렇게 작성하였는데, DOM 변경을 react에 전적으로 위임해야 하기 때문에 getElementById
나 querySeletor
로 접근하면 예상치 못한 오류가 발생한다고 한다.
실제로 comment-div가 고유하지 않은 id라 첫 댓글만 지워지는 상황이 발생했다.
각 컴포넌트 내에서 엘리먼트를 삭제하기 때문에 정상적으로 작동할 것이라고 생각했으나 완전히 잘못된 생각이었다.
이를 해결하기 위해 addComment
객체에 id를 userName + date 값으로 넣어주었다.
✏️ Github repo