팬레터함 만들기 ① (useState)

Jtiiin:K·2023년 11월 14일
0
post-thumbnail

📝 팬레터함 만들기

✅ 프로젝트 셋업

✔ git 설정

✔ CRA 설치

✔ 폴더 및 필요 컴포넌트 작성

📦src
┣ 📂assets
┣ 📂components
┃ ┣ 📂Home
┃ ┃ ┣ 📜AddFanLetter.jsx
┃ ┃ ┣ 📜FanLetterItem.jsx
┃ ┃ ┣ 📜FanLetterList.jsx
┃ ┃ ┗ 📜Tab.jsx
┃ ┗ 📂UI
┃ ┃ ┣ 📜Button.jsx
┃ ┃ ┗ 📜Header.jsx
┣ 📂pages
┃ ┣ 📜Details.jsx
┃ ┗ 📜Home.jsx
┣ 📂shared
┃ ┗ 📜Router.js
┣ 📜App.js
┣ 📜FakeData.json
┣ 📜GlobalStyle.jsx
┗ 📜index.js

✔ styled-components, react-router-dom, uuid 설치

yarn add styled-components
yarn add react-router-dom
yarn add uuid

✔ 타이틀 변경 (index.html)

✔ jsconfig.json 변경

(src폴더 기준 절대경로 설정)

{
	"compilerOptions": {
		"baseUrl": "src"
	},
	"include": ["src"]
}

✅ "props-drilling' 브랜치 생성 및 이동

git checkout -b props-drilling


✅ Router 셋업

✔ shared/Router.js 에 설정

import { BrowserRouter, Route, Routes } from 'react-router-dom';

import Home from '../pages/Home';
import Details from '../pages/Details';
import Header from 'components/UI/Header';

const Router = () => {
  return (
    <BrowserRouter>
      <Header />
      <Routes>
        <Route
          path='/' element={<Home />}
        />
        <Route
          path='details/:id' element={<Details />}
        />
        <Route />
      </Routes>
    </BrowserRouter>
  );
};

export default Router;

✅ 전역 스타일링 적용

✔ GlobalStyle.jsx 생성

  • styled-components에서 import
  • reset.css 넣기
import { createGlobalStyle } from 'styled-components';

const GlobalStyle = createGlobalStyle`
  /* reset CSS */
  (...)
`;

export default GlobalStyle;
  • 문제
    💥 GlobalStyle을 어디에 써야할지?
    💡 답은 App.js 에 Router 위
function App() {
  return (
    <>
      <GlobalStyle />
      <Router />
    </>
  );
}

✅ 홈화면 UI 구현

  • 예시페이지 보고 Home.jsx / Detail.jsx에 UI부터 쭉 구현

✔ styled-components 에서 hover 쓰는 법

const Btn = styled.button`
  (...)
  &:hover {
    filter: brightness(1.2);
  }
`;

✔ 공용 컴포넌트 확장(?)

Button 컴포넌트를 토대로 확장해서 사용하기

const Btn = styled.button`
  background-color: #eeb20c;
  (...)
`;

const HomeBtn = styled(Btn)`
  display: block;
  margin: 0 auto;
`;

✔ 내용이 한줄까지만 나오도록 말줄임표 만들기

  • overflow:hidden : 넓이를 넘어가는 내용에 대해서는 보이지 않게 처리함
  • text-overflow:ellipsis : 글자가 넓이를 넘을 경우 생략부호를 표시함
  • white-space:nowrap : 공백문자가 있는 경우 줄바꿈하지 않고 한줄로 나오게 처리함 (\A로 줄바꿈가능)
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;

✅ Dummy Data 만들기

  • fakeData.json 파일 만들고 Dummy Data 넣기
  • Router.js에 useState로 데이터 관리하기
    (Router에 넣는 이유 : 모든 컴포넌트가 Router를 거치기 때문(=최상위))
    const [fanLetters, setFanLetters] = useState(FakeData)
  • Home.jsx, Detail.jsx에 props로 데이터 내려주기

✅ 팬레터 입력창(form)으로 등록 기능 구현

✔ input

  • 닉네임 (최대20 글자) - input의 maxlength 속성 사용
  • 내용 (최대100글자) - input의 maxlength 속성 사용
  • 누구에게 보낼지 (select-option 태그로 선택)
  • inputChangeHandler 함수와 useState로 입력폼 데이터 관리

✔ 유효성 검사

  • submitHandler
    • 브라우저 기본 동작 막기
    • 닉네임과 내용이 비었다면 alert 창 띄우고 return
    • 새로운 팬레터 내용 받아서 setFanletter로 추가하기
    • input 비우기

✅ 팬레터 클릭시 상세화면으로 이동

✔ Home.jsx

  • 각각의 팬레터를 Link로 감싸고 클릭시 이동하도록
 <Link to={`/details/${item.id}`} key={item.id}>
      <FanLetterItem item={item} />
 </Link>

✔ Detail.jsx

  • useParams 사용하여 id 가져와서 팬레터 상세화면 보여주고 수정과 삭제할 때도 사용

✅ 팬레터 삭제 기능 구현

  • confirm으로 확인 후 삭제(filter 사용)
  • 삭제 된 후에 홈으로 이동 (useNavigate 사용)

✅ 팬레터 수정 기능 구현

  • 수정상태인지 확인하기 위해 useState 추가
    const [editInputShown, setEditInputShown] = useState(false);
  • 수정된 text를 받기 위해 useState 추가
    const [editInput, setEditInput] = useState('');
const editHandler = () => {
  // 전체 팬레터에서 현재 팬레터를 선택
    const selectedFanLetter = fanLetters.find((item) => item.id === id);
  // 수정상태 토글 (수정상태 or 아닌상태)
    setEditInputShown((editInputShown) => !editInputShown);
  // 수정상태가 아니라면 원래 content를 보여줌
    if (!editInputShown) { setEditInput(selectedFanLetter.content);
    }

    // 수정 사항이 없으면 alert를 띄우고 함수 종료
  // (수정상태이고 editInput의 값이 기존의 content 내용과 같다면)
    if (
      editInputShown &&
      editInput.trim() === selectedFanLetter.content.trim()
    ) {
      alert('수정할 내용이 없습니다.');
      return;
    }

  // 수정 상태라면 최신 상태를 받아와서 content만 update
    if (editInputShown) {
      setFanLetters((prevFanLetters) =>
        prevFanLetters.map((item) =>
          item.id === id ? { ...item, content: editInput } : item
        )
      );
    }
  };
  • UI 부분도 조건부 렌더링 (수정상태라면 textarea 보여주기)
 {editInputShown ? (
     <textarea value={editInput} onChange={editInputHandler}></textarea>
      ) : (
      <p>{item.content}</p>
 )}

✅ 멤버별 Tab 전환

✔ Home.jsx

  • 클릭된 멤버와 선택된 멤버를 알기 위한 useState
  const [memberClick, setMemberClick] = useState({
    전체: false,
    카리나: false,
    윈터: false,
    닝닝: false,
    지젤: false,
  });

  const [selectedMember, setSelectedMember] = useState('전체');
  • 버튼을 눌렀을 때 선택된 멤버를 setState로 바꿔주고 해당 정보를 Tab 컴포넌트와 FanLetterList 컴포넌트에 props로 넘겨줌
  const clickHandler = (e) => {
    setSelectedMember(e.target.innerHTML);
    setMemberClick({
      ...memberClick,
      [selectedMember]: true,
    });
  };

✔ Tab.jsx

  • 넘겨받은 정보를 토대로 active 된 멤버를 Button 컴포넌트에 넘겨줌 (true/false)
const Tab = ({ clickHandler, selectedMember }) => {
  return (
    <TabGroup>
      <Button onClick={clickHandler} $active={selectedMember === '전체'}>
        전체
      </Button>
      <Button onClick={clickHandler} $active={selectedMember === '카리나'}>
        카리나
      </Button>
      <Button onClick={clickHandler} $active={selectedMember === '윈터'}>
        윈터
      </Button>
      <Button onClick={clickHandler} $active={selectedMember === '닝닝'}>
        닝닝
      </Button>
      <Button onClick={clickHandler} $active={selectedMember === '지젤'}>
        지젤
      </Button>
    </TabGroup>
  );
};

✔ Button.jsx

  • active 상태를 넘겨 받아 조건부로 background 속성 부여
const Button = ({ children, onClick, $active }) => {
  return (
    <Btn onClick={onClick} $active={$active}>
      {children}
    </Btn>
  );
};
const Btn = styled.button`
  (...)
  ${(props) =>
    props.$active &&
    `
      background-color: #015aff;
      color: #fff;
    `}
`;

✔ FanLetterList.jsx

  • filteredLetters && 를 쓰는 이유?
    filteredLetters가 null 이거나 undefined 인 경우에도 map을 실행하려고 해서 에러가 남
const FanLetterList = ({ fanLetters, selectedMember }) => {
  // 선택된 멤버가 '전체'가 아니라면 
  // 선택된 멤버와 item의 wirtedTo(누구에게 쓸건가요?)가 일치하는 팬레터만 filter
  // '전체' 라면 모두 true 처리해서 모든 팬레터가 나오도록
  const filteredLetters = fanLetters.filter((item) =>
    selectedMember !== '전체' ? item.writedTo === selectedMember : true
  );
  return (
    <ScFanLetterItems>
      /* 팬레터가 있거나 길이가 0보다 크다면 */
      {filteredLetters && filteredLetters.length > 0
        ? filteredLetters.map((item) => (
            <Link to={`/details/${item.id}`} key={item.id}>
              <FanLetterItem item={item} />
            </Link>
          ))
        : '팬레터가 없습니다'}
    </ScFanLetterItems>
  );
};
profile
호기심 많은 귀차니즘의 공부 일기

0개의 댓글