[React] study 5주차 - 로그인, 댓글 기능 구현하기 (localStorage, Recoil useSetRecoilState, useRecoilValue)

newsilver·2022년 2월 22일
3

react-study

목록 보기
5/9
post-thumbnail

5주차 과제는 로그인 + 댓글 기능 구현하기!

요구사항

Basic

  • 로그인 & 로그아웃 기능
    • 텍스트 인풋과 로그인 버튼이 존재
    • 임의의 텍스트 입력후 로그인 버튼 클릭시 ⇒ 로그인
    • 로그인 상태인 경우 입력한 텍스트가 유저 네임으로 쓰이고 로그아웃 버튼이 나타남
    • 로그아웃 클릭시 다시 처음 상태로 전환
  • 댓글 목록 & 댓글 입력 기능
    • 로그인 상태에서만 입력 가능
    • 댓글은 유저이름, 댓글 내용, 작성시간이 나타난다.
    • 댓글에서 삭제 버튼은 로그인한 유저이름이 같은사람만 보인다. 삭제 기능은 로그인한 유저이름이 같은사람만 가능하다.

Hard

  • 새로 고침해도 로그인을 유지 하기
  • 만들어진 댓글은 30초가 지나면 사라지게 하기
  • 이쁘게 꾸며보기
    • box-shadow
    • css animation

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

로그인 & 로그아웃 기능

Basic

  • 텍스트 인풋과 로그인 버튼이 존재
  • 임의의 텍스트 입력후 로그인 버튼 클릭시 ⇒ 로그인
  • 로그인 상태인 경우 입력한 텍스트가 유저 네임으로 쓰이고 로그아웃 버튼이 나타남
  • 로그아웃 클릭시 다시 처음 상태로 전환

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과 같이 객체 속성으로 접근하여 변경해야 했다.

Hard - 새로 고침해도 로그인 유지 하기

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 값으로 들어갔다.

댓글 목록 & 댓글 입력 기능

Basic

  • 로그인 상태에서만 입력 가능
  • 댓글은 유저이름, 댓글 내용, 작성시간이 나타난다.
  • 댓글에서 삭제 버튼은 로그인한 유저이름이 같은사람만 보인다. 삭제 기능은 로그인한 유저이름이 같은사람만 가능하다.

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 이벤트에 setCommentcomment에 유저 네임과 댓글 내용을 저장한다.
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 공식 문서를 참고했다.

Hard - 만들어진 댓글은 30초가 지나면 사라지게 하기

각 댓글마다 생성시간 + 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에 전적으로 위임해야 하기 때문에 getElementByIdquerySeletor로 접근하면 예상치 못한 오류가 발생한다고 한다.
실제로 comment-div가 고유하지 않은 id라 첫 댓글만 지워지는 상황이 발생했다.
각 컴포넌트 내에서 엘리먼트를 삭제하기 때문에 정상적으로 작동할 것이라고 생각했으나 완전히 잘못된 생각이었다.
이를 해결하기 위해 addComment 객체에 id를 userName + date 값으로 넣어주었다.


✏️ Github repo

profile
이게 왜 🐷

0개의 댓글