[TIL] 팬레터함 만들기 (1)

·2023년 11월 14일
1

TIL

목록 보기
34/85
post-thumbnail

[필수 구현 사항]

  • 팬레터 CRUD 구현 (작성, 조회, 수정, 삭제)
  • 아티스트별 게시물 조회 기능 구현 (Home - Read)
  • 원하는 아티스트에게 팬레터 등록 구현 (Home - Create)
  • 팬레터 상세 화면 구현 (Detail - Read)
  • 상세화면에서 팬레터 내용 수정 구현 (Detail - Update)
  • 상세화면에서 팬레터 삭제 구현 (Detail - Delete)

[필수 요구 사항]

  • styled-components를 이용하여 스타일링
  • 전역 스타일에 reset.css 적용 및 box-sizing: border-box 설정
  • styled-components에 props를 넘김으로 인한 조건부 스타일링 적용
  • 팬레터 등록 시 id는 uuid 이용

👉 과제는 Props Drilling → context → redux 순으로 별도의 브랜치 생성

과제는 어제 오후부터 시작했는데, 우선 useContext나, redux를 사용하지 않고 useState만으로 과제를 구현하는 props-drilling 브랜치를 오늘 완성했다. (almost..)

우선, 정확한 과제 명칭은 "그룹 아티스트 팬레터함" 이나.. (과제 예시도 에스파였다.) 딱히 떠오르는 그룹 아티스트가 없었다. 근데 눈 앞에 토이스토리 우디 인형이 있어서 토이스토리 캐릭터들로 결~정! (우디, 버즈, 포키, 보핍)

Home page UI

CharTap (WOODY, BUZZ, FORKY, BOPEEP)

  • styled-components 조건부 스타일링 적용하기
// components/CharTab.jsx
function CharTab({ char, setChar }) {
  const selectChar = (CharName) => {
    setChar(CharName);
  };
  return (
    <StDiv>
      <Button
        value="WOODY"
        onClick={() => selectChar("woody")}
        clicked={(char === "woody").toString()}
      />
      <Button
        value="BUZZ"
        onClick={() => selectChar("buzz")}
        clicked={(char === "buzz").toString()}
      />
      <Button
        value="FORKY"
        onClick={() => selectChar("forky")}
        clicked={(char === "forky").toString()}
      />
      <Button
        value="BOPEEP"
        onClick={() => selectChar("bopeep")}
        clicked={(char === "bopeep").toString()}
      />
    </StDiv>
  );
}

앱에 있는 모든 버튼들은 Button.jsx 컴포넌트로 관리할 수 있게 따로 뺐다. 조건부 스타일링 적용하는 부분도 Button.jsx에 있다.

// components/Button.jsx
unction Button({ value, onClick, clicked }) {
  return (
    <StBtn onClick={onClick} clicked={clicked}>
      {value}
    </StBtn>
  );
}

export default Button;

const StBtn = styled.button`
  height: 50px;
  width: 100px;
  border: none;
  transition: all 0.2s ease-in-out;
  background-color: ${(props) => // 조건부 스타일링
    props.clicked === "true" ? theme.yellow : theme.blue};
  color: ${(props) => (props.clicked === "true" ? "black" : "white")};
  padding: 10px;
  border-radius: 10px;
  font-size: 18px;

  &:hover {
    cursor: pointer;
    filter: brightness(1.2);
  }
`;

fakeData.json 가져오기

fakeData.json 파일을 public 폴더 아래에 두었다.
그리고 App.jsx에서 fakeData.json을 가져왔다.

// App.jsx
function App() {
  const [data, setData] = useState([]);
  useEffect(() => {
    async function fetchData() {
      const response = await fetch("/fakeData.json");
      const json = await response.json();
      setData([...json]);
    }
    fetchData();
  }, []);

  return (
    <>
      <ThemeProvider theme={theme}>
        <GlobalStyle />
        <Router data={data} setData={setData} />
      </ThemeProvider>
    </>
  );
}

export default App;

useEffect 훅을 사용하여 json data를 fetch 했다.
data를 fetch하는 건 첫 렌더링 시에민 아루어지면 되기 때문에 useEffect의 두번째 매개변수인 dependency array 를 빈 배열로 두었다.
그리고 useState 훅을 이용하여 data라는 state를 만들고, data를 fetch 해온 fakeData.json으로 설정하였다.
data라는 state를 App.jsx에 선언한 이유는 App이 최상단 컴포넌트이고, data는 그 아래의 자식 컴포넌트에서 거의 모두 사용하기 때문이다.
(props-drilling의 서막..ㅎ) 그래서 Router에도 data와 setData를 넘겨주고, Router에서는 Home 컴포넌트와 Detail 컴포넌트에 내려주게 된다. (벌써부터 context API 또는 redux 써야겠다는 생각이..😂)

content 한 줄만 표시하기

// components/Comment.jsx
function Comment({ comment }) {
  return (
    <Link to={`/detail/${comment.id}`}>
      <CommentBox>
        <StImg src={comment.avatar} />
        <div>
          <StP>{comment.name}</StP>
          <p>{comment.createdAt}</p>
          <p>
            {comment.content.length > 40
              ? `${comment.content.slice(0, 40)}...`
              : comment.content}
          </p>
        </div>
      </CommentBox>
    </Link>
  );
}

export default Comment;

40글자에서 잘리도록 삼항연산자를 사용해서 구현했다. 나머지는 말줄임표로 표현한다. (Detail page로 이동하면 전문 확인 가능)
각 코멘트를 Link 태그로 감싸서 클릭 시 /detail/:id로 이동할 수 있게 했다.

Detail page UI

// pages/Detail.jsx
const { id } = useParams();
const comment = data.find((item) => item.id === id);

우선 react-router-dom에서 제공하는 useParams 훅을 사용하여 url에 있는 id값을 가져오고, data에서 해당하는 id의 comment를 find 메서드를 사용하여 가져왔다.

Delete Comment

삭제버튼을 클릭했을 때 바로 삭제되는 것이 아니라, 사용자에게 확인을 받고 삭제처리르 하기 위해 window.confirm을 이용했다.

// pages/Detail.jsx
const deleteComment = () => {
    const result = window.confirm("정말 삭제하시겠습니까?");
    if (result) {
      setData(data.filter((item) => item.id !== id));
      navigate("/");
    } else return;
  };

그러면 "정말 삭제하시겠습니까?" 라는 alert 창이 뜨고, 확인을 눌렀을 때 삭제가 이루어진다. 삭제 후 홈 화면으로 이동하기 위해 useNavigate 훅을 사용했다. const navigate = useNavigate(); 상단에 이와 같은 코드를 쓰고, 삭제 버튼 클릭 시 navigate("/"); 코드를 통해 홈 화면으로 이동하게 된다.

Update Comment

삭제보다 약간 애를 먹었었던 수정 기능..
우선 수정버튼을 클릭했을 때 수정 버튼과 삭제 버튼이 사라지고, 수정 완료 버튼만 보여야 한다.

// pages/Detail.jsx
...
<StTextarea
  type="text"
  value={textarea}
  disabled={isInputDisabled}
  onChange={(e) => setTextarea(e.target.value)}
  />
...
<Btns>
  {isInputDisabled ? (
      <>
      <Button value="수정" onClick={() => setIsInputDisabled(false)} />
      <Button value="삭제" onClick={deleteComment} />
      </>
  ) : (
    <Button value="수정완료" onClick={updateComment} />
  )}
</Btns>

그걸 위와 같이 조건부 렌더링으로 구현하였다.
isInputDisabled 라는 state에는 textarea의 disabled 속성과 묶여있다. 초기값에는 true가 들어있어 disabled={true} 가 되어 textarea에 어떠한 입력도 못하는 상태였다가, 수정 버튼을 클릭하면 setIsInputDisabled(false)를 통해 disabled={false} 로 바꿔주어 textarea에 입력을 할 수 있는 상태로 바뀌게 된다.

수정완료 버튼을 클릭하면 updateComment 함수가 실행된다.

// pages/Detail.jsx
const updateComment = () => {
    if (textarea === comment.content) alert("수정사항이 없습니다.");
    else {
      const result = window.confirm("이대로 수정하시겠습니까?");
      if (result) {
        data.find((item) => item.id === id).content = textarea; // mutable 한 코드임을 깨닫고 훗날 변경하게 됩니다..
        navigate("/");
      } else return;
    }
  };

아무런 수정사항 없이 수정완료 버튼을 클릭하면 수정되지 않고, 사용자에게 "수정사항이 없습니다" 라는 알림을 주기 위해 textarea라는 state값과 (textarea의 value와 묶여있음) data에서 find메서드로 가져온 comment의 content가 일치하는지 확인하고, 일치하면 alert 창을 띄운다.

그렇지 않으면 (textarea가 기존 data의 content와 다르다면 수정 사항이 있다는 뜻이므로) "이대로 수정하시겠습니까?" 라는 confirm 창을 띄우고, data에서 일치하는 comment를 찾아 content를 textarea의 값으로 바꾼다. 그리고 navigate를 통해 홈 화면으로 이동한다.

[느낀점]
내일은 context API 활용하는 부분을 도전해보아야 겠다.
styled component 를 활용하여 프로젝트를 한 게 처음인데 생각보다 편리한 것 같다. 아직 안 익숙해서 그렇지.. 하나의 파일 안에 컴포넌트와 css 코드가 같이 있다보니 관리하기도 용이할 것 같다.

profile
느리더라도 조금씩, 꾸준히

0개의 댓글