프론트엔드로 가는 길/portfolio 제작 여정기 👩🏻‍💻

01 BookBucket - 내가 읽고 싶은 책을 추가해보자!📚[2.14~2.15]

woody-j 2023. 2. 19. 16:20

1. Input 작성으로 책 추가

1)   플러스 버튼 눌렀을 

도서를 추가하기 위해 + 버튼을 클릭해보자! 그럼 하단에 입력창이 뜬다.

(1) 하단에 input 창 띄우고 아이콘 ☑️ 버튼으로 변경

 

input 창 띄우기

 

true/false값을 저장하는 state 생성

 let [plusBtn, setPlusBtn] = useState(false);

기본 값은 false

btn을 눌렸을 때 plusBtn 상태값을 true로 바꿔주는 openInput 함수 생성
const openInput = () => {
setPlusBtn(true);
};
 
⓷ plusBtn 상태 값이 true 일때, class registration옆에 on을 붙인다.
 <div className={`registration ${plusBtn ? "on" : ""}`}>
 
⓸ css를 꾸며보자. 
// false 일때
.registration{
    display: none;
  }
  
//true 일때
.registration.on {
    display: block;
  }

처음 class registration는 안보여야 하니께 display:none 상태
-> on이 붙었을 땐 보여야 하니께 display:block 상태
 
 
아이콘 체크 ☑️ 버튼으로 변경
 
 
⓵ 조건문으로 plusBtn이 true면 check를, false면 plus 아이콘을 띄운다.
<FontAwesomeIcon
              icon={plusBtn ? faCircleCheck : faCirclePlus}
              className="circlePlus cursor"
              onClick={openInput}
            />

 


 

(2) 포커스가 인풋 창에 뜨기

💡 DOM을 직접 선택하기 보단 ref를 사용 권장

⓵ useRef로 input DOM을 선택할 변수를 저장

 const titleInputRef = useRef(null);

⓶ input에 ref 지정

 ref={titleInputRef}

⓷ useEffect로 plusBtn state가 변경 될 때마다 렌더링

 useEffect(() => {
    titleInputRef.current.focus();
  }, [plusBtn]);

 


(3)  ⛔️ 삭제 버튼  회색으로 변하고 클릭이 안됨

 

 

⓵  위와 같은 방식으로 true 일 때 class에 on 붙이기

 <FontAwesomeIcon
              icon={faSquareMinus}
              className={`squareMinus cursor ${plusBtn ? "on" : ""}`}
            />

⓶ pointer-events 이벤트 제어 속성으로 클릭 막기

  .squareMinus.on {
      color: $subG;
      pointer-events: none;
    }

💡 체크☑️ 버튼 누르면 다시 이벤트 제어 풀리고 색 원상복구 됨


 

(4) 지우개 표시 버튼 눌렀을 때, value값 초기화 

     ⓵ input value 값을 저장하는 state 생성

  let [inputValue, setInputValue] = useState();

     ⓶ input에 변화(onChange)가 생길 때마다 함수 실행

  const inputText = (e) => {
    setInputValue(e.target.value);
  };

💡 e.target(event가 발생한 DOM = input DOM), e.target.value(input 입력값)을 의미 

inputText() : setter함수[setInputValue]로 InputValue 상태값을 e.target.value(input 입력값)로 업데이트
💡 왜 onChange를 해야하는가! input에 value 속성만 지정하게 되면 그 값으로만 값을 조절할 수 있는 권한이 있다. 그럼 사용자는 값을 입력하는 권한이 없어 input 안에 값 입력이 불가능하다.

 

     ⓷ input에 onChange와 value 값 넣기

  <input
              type="text"
              placeholder="읽을 책을 입력하세요"
              value={inputValue}
// e 안쓰면 텍스트 입력 안됨
             onChange={inputText}/>

💡 그럼 input에 value 왜 적냐! 

더보기

react가 '단일 소스 진리' 원칙을 따르기 때문.

react는 컴포넌트의 상태(state)와 속성(props) 사용

-> value를 추가 안하면 input의 값이 state와 일치하지 않아 사용자 입력추적 어려움

So, react에서 input을 제어할 때, value 속성을 state와 연결해 react가 사용자 입력을 추적하고, ui 업데이트 가능케 해야함

 

     ⓸ button을 눌렀을 때(onClick) inputValue 상태 값을 ""(null)로 만드는 함수 생성

 const clearAllText = () => {
    setInputValue("");
  };

     ⓹ button에 적용

<button className="clearbtn" onClick={clearAllText}>

 

🚫  이 과정 중에 생긴 오류가 있었다!! 뮈치겠다 진짜

Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.

: 이 오류는 React 컴포넌트에서 무한 루프(infinite loop)가 발생하여 React가 렌더링을 계속 반복하고 있을 때 발생한다.

더보기

일반적으로 이 오류는 다음과 같은 상황에서 발생합니다.

  • state나 props가 변경될 때 렌더링이 계속 발생하는 경우
  • 렌더링이 발생하는 함수 내에서 상태를 변경하거나 props를 변경하는 경우
  • 조건문, 반복문 등의 제어문 안에서 렌더링이 발생하는 경우

일반적인 해결 방법은 다음과 같습니다.

  • 상태(state)나 속성(props)이 변경될 때 불필요한 렌더링을 방지하기 위해 shouldComponentUpdate() 함수를 사용하거나 PureComponent를 사용합니다.
  • 상태(state)를 변경할 때 setState() 함수 대신에 setState() 함수의 콜백 함수를 사용합니다.
  • 조건문이나 반복문 내에서 렌더링이 발생하는 경우, 조건문이나 반복문을 밖으로 이동시키고, 조건문이나 반복문의 결과에 따라 렌더링을 처리합니다.

암튼 이유는 ⓷⓹에서 onClick={clearAllText()},onChange={inputText()} 이렇게 작성했기 때문-> 이건 콜백함수도 뭣도 아님

-> onClick={clearAllText} Or onClick={()=>{clearAllText()} 이렇게 작성해야함


(5)  표시 버튼 눌렀을 때 창 닫힘

⓵ plusBtn 상태 값을 false로 변경

  const closeInput = () => {
    setPlusBtn(false);
  };
<button className="inputCloseBtn" onClick={closeInput}>

    

2) 체크☑️ 버튼 눌렀을

 

(1) input에 입력을 모음/자음만 하면 alert 창 띄우기

[전체 코드]

  const addBook = () => {
    if (pattern.test(inputValue)) {
      alert("똑바로 작성해주세요");
      setInputValue("");
      return titleInputRef.current.focus();
    }
  };

⓵ 자음/모음을 검사하는 정규식을 저장

  let pattern = /([^가-힣a-z\x20])/i;

⓶ test 메서드로 inputValue가 pattern 정규식에 만족하는지 판별한다.

 if (pattern.test(inputValue)) {
    }

false일 때 alert창을 띄우고 inputValue 상태 값을 비운다.

 if (pattern.test(inputValue)) {
      alert("똑바로 작성해주세요");
      setInputValue("");
    }

⓸ 상태 값을 비우고 그 input 칸에 focusing 한다.

 if (pattern.test(inputValue)) {
      alert("똑바로 작성해주세요");
      setInputValue("");
      return titleInputRef.current.focus();
    }

 

(2) input에 아무것도 입력을 안했을 때 alert 창 띄우기

 

[전체코드]

  const addBook = () => {
   // if (pattern.test(inputValue)) {
      //alert("똑바로 작성해주세요");
     // setInputValue("");
     // return titleInputRef.current.focus();
  //  }
    if (plusBtn) {
      if (!inputValue) {
        alert("도서를 입력해주세요");
        return titleInputRef.current.focus();
      }
    }
  };

 

⓵ plusBtn이 true일 때

if (plusBtn) {
    }

 

⓶ inputValue 값이 false 이면

if (plusBtn) {
      if (!inputValue) {
      }
    }

💡 plusBtn이 true일 때 조건을 하나 더 만든 이유 : 

조건을 추가하지않으면 버튼을 누른 후 input 창이 이미 inputValue 값이 false이기 때문에 도서를 입력하라는 alert창이 먼저 출력된다.

 

⓷ alert창을 띄우고 focusing 해라

if (plusBtn) {
      if (!inputValue) {
        alert("도서를 입력해주세요");
        return titleInputRef.current.focus();
      }
    }

(3) 입력한 value 포스트잇으로 보여주기

[전체코드]

  const addBook = () => {
  //  if (pattern.test(inputValue)) {
   //   alert("똑바로 작성해주세요");
   //   setInputValue("");
    //  return titleInputRef.current.focus();
    //}
    //if (plusBtn) {
     // if (!inputValue) {
     //   alert("도서를 입력해주세요");
     //   return titleInputRef.current.focus();
     // }
      let copy = [...listBook];
      copy.unshift({ id: bookId, title: inputValue });
      setListBook(copy);
      increaseId();
      setInputValue("");
    }
  };

⓵ 책 배열 깊은 복사

 let copy = [...listBook];

⓶ 새로 추가되는 object 앞에 추가하기

copy.unshift({ id: bookId, title: inputValue });

⓷ setListBook copy 대입

  setListBook(copy);

💡 ⓸ id 값 부여

 

- id값 state에 저장

  const [bookId, setBookId] = useState(listBook.length);

💡 ⓹ id 값 증가

  const increaseId = () => {
    if (plusBtn) {
      let copy = bookId;
      copy++;
      setBookId(copy);
    }
  };

- plusBtn이 true일 때 : ☑️ btn을 눌렀을 때

- bookId 값을 1 증가

increaseId();

⓺ inputValue 상태 값 비우기

setInputValue("");

 


(4) 엔터했을 때 도서 추가

⓵ keycode가 13(enter)일 때 addBook함수 실행
const addEnter = () => {
if (window.event.keyCode === 13) {
addBook();
}
};
 
⓶ input에 onKeyPress 
<input
type="text"
placeholder="읽을 책을 입력하세요"
value={inputValue}
onChange={(e) => inputText(e)}
ref={titleInputRef}
onKeyPress={addEnter}
/>
 
 

(5) 도서 추가 후 다시 인풋에 포커싱

⓵ listBook 내용이 렌더링 시
useEffect(() => {
titleInputRef.current.focus();
}, [plusBtn, listBook]);

 


[전체 코드]

app.js

import { useState, useRef, useEffect } from "react";
import "./css/reset.css";
import "./css/common.scss";
import book from "./data/Book";
// fontawesome
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
  faCirclePlus,
  faCircleCheck,
  faSquareMinus,
  faMagnifyingGlass,
  faXmark,
  faEraser,
} from "@fortawesome/free-solid-svg-icons";

function App() {
  // 변수
  let pattern = /([^가-힣a-z\x20])/i;
  // ---usestate
  // plus button
  let [plusBtn, setPlusBtn] = useState(false);
  // input value
  let [inputValue, setInputValue] = useState("");
  // book data
  let [listBook, setListBook] = useState(book);
  //book id
  const [bookId, setBookId] = useState(listBook.length);
  //------------useref
  const titleInputRef = useRef(null);
  // ---------------------useEffect
  // input focus
  useEffect(() => {
    titleInputRef.current.focus();
  }, [plusBtn, listBook]);

  // --------함수 생성
  //도서 추가
  const addBook = () => {
    if (pattern.test(inputValue)) {
      alert("똑바로 작성해주세요");
      setInputValue("");
      return titleInputRef.current.focus();
    }
    if (plusBtn) {
      if (!inputValue) {
        alert("도서를 입력해주세요");
        return titleInputRef.current.focus();
      }
      let copy = [...listBook];
      copy.unshift({ id: bookId, title: inputValue });
      setListBook(copy);
      increaseId();
      setInputValue("");
    }
  };
  // 엔터로 도서 추가
  const addEnter = () => {
    if (window.event.keyCode === 13) {
      addBook();
    }
  };
  // input 하단에 띄우기
  const openInput = () => {
    setPlusBtn(true);
    // testWord();
    addBook();
  };

  //input value change
  const inputText = (e) => {
    setInputValue(e.target.value);
  };
  //input value null
  const clearAllText = () => {
    setInputValue("");
  };
  // listBook.id lncrease
  const increaseId = () => {
    if (plusBtn) {
      let copy = bookId;
      copy++;
      setBookId(copy);
    }
  };
  // input close
  const closeInput = () => {
    setPlusBtn(false);
  };
  // 자음, 모음 검사
  const testWord = () => {
    if (pattern.test(inputValue)) {
      alert("똑바로 작성해주세요");
      setInputValue("");
      return titleInputRef.current.focus();
    }
  };
  // 빈칸 검사
  const blankWord = () => {
    if (inputValue === "") {
      alert("도서를 입력해주세요");
      return titleInputRef.current.focus();
    }
  };
  return (
    <div className="App">
      <div className="bg center">
        <div className="container">
          <h1 className="title">2023 독서 계획</h1>
          <p className="msg">2023년에 읽을 도서</p>
          <div className="icon">
            {" "}
            <FontAwesomeIcon
              icon={plusBtn ? faCircleCheck : faCirclePlus}
              className="circlePlus cursor"
              onClick={openInput}
            />
            <FontAwesomeIcon
              icon={faSquareMinus}
              className={`squareMinus cursor ${plusBtn ? "on" : ""}`}
            />
          </div>
          <div className={`registration ${plusBtn ? "on" : ""}`}>
            <input
              type="text"
              placeholder="읽을 책을 입력하세요"
              value={inputValue}
              onChange={(e) => inputText(e)}
              ref={titleInputRef}
              onKeyPress={addEnter}
            />
            <button className="clearBtn" onClick={clearAllText}>
              {" "}
              <FontAwesomeIcon icon={faEraser} className="faEraser cursor" />
            </button>
            <button className="inputCloseBtn" onClick={closeInput}>
              {" "}
              <FontAwesomeIcon icon={faXmark} className="faXmark cursor" />
            </button>
          </div>
          <div className="category">
            <div className="menu">
              <ul>
                <li>전체보기</li>
                <li>즐겨찾기</li>
              </ul>
            </div>
            <div className="search">
              {" "}
              <FontAwesomeIcon
                icon={faMagnifyingGlass}
                className="faMagnifyingGlass cursor"
              />
            </div>
          </div>
          <div className="row flex-row wrap">
            {listBook.map((book, i) => {
              return (
                <div className="bucket img2 center ">
                  <h4>{book.title}</h4>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
}

export default App;

 

➕ (22.02.20) app.js 쓴 것들 component화 하기 ------------------------

(1) main으로 따로 컴포넌트화 시킴

[Main.js]

import React from "react";
import { useState, useRef, useEffect } from "react";
import book from "../../data/Book";
import "./main.scss";

// fontawesome
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
  faCirclePlus,
  faCircleCheck,
  faSquareMinus,
  faMagnifyingGlass,
  faXmark,
  faEraser,
} from "@fortawesome/free-solid-svg-icons";

const Main = () => {
  // 변수
  let pattern = /([^가-힣a-z\x20])/i;
  // ---usestate
  // plus button
  let [plusBtn, setPlusBtn] = useState(false);
  // input value
  let [inputValue, setInputValue] = useState("");
  // book data
  let [listBook, setListBook] = useState(book);
  //book id
  const [bookId, setBookId] = useState(listBook.length);
  //------------useref
  const titleInputRef = useRef(null);
  // ---------------------useEffect
  // input focus
  useEffect(() => {
    titleInputRef.current.focus();
  }, [plusBtn, listBook]);

  // --------함수 생성
  //도서 추가
  const addBook = () => {
    if (pattern.test(inputValue)) {
      alert("똑바로 작성해주세요");
      setInputValue("");
      return titleInputRef.current.focus();
    }
    if (plusBtn) {
      if (!inputValue) {
        alert("도서를 입력해주세요");
        return titleInputRef.current.focus();
      }
      let copy = [...listBook];
      copy.unshift({ id: bookId, title: inputValue });
      setListBook(copy);
      increaseId();
      setInputValue("");
    }
  };
  // 엔터로 도서 추가
  const addEnter = () => {
    if (window.event.keyCode === 13) {
      addBook();
    }
  };
  // input 하단에 띄우기
  const openInput = () => {
    setPlusBtn(true);
    // testWord();
    addBook();
  };

  //input value change
  const inputText = (e) => {
    setInputValue(e.target.value);
  };
  //input value null
  const clearAllText = () => {
    setInputValue("");
  };
  // listBook.id lncrease
  const increaseId = () => {
    if (plusBtn) {
      let copy = bookId;
      copy++;
      setBookId(copy);
    }
  };
  // input close
  const closeInput = () => {
    setPlusBtn(false);
  };
  return (
    <div className="main">
      {" "}
      <div className="bg center">
        {" "}
        <div className="container">
          {" "}
          <p className="msg">2023년에 읽을 도서</p>
          <div className="icon">
            {" "}
            <FontAwesomeIcon
              icon={plusBtn ? faCircleCheck : faCirclePlus}
              className="circlePlus cursor"
              onClick={openInput}
            />
            <FontAwesomeIcon
              icon={faSquareMinus}
              className={`squareMinus cursor ${plusBtn ? "on" : ""}`}
            />
          </div>
          <div className={`registration ${plusBtn ? "on" : ""}`}>
            <input
              type="text"
              placeholder="읽을 책을 입력하세요"
              value={inputValue}
              onChange={(e) => inputText(e)}
              ref={titleInputRef}
              onKeyPress={addEnter}
            />
            <button className="clearBtn" onClick={clearAllText}>
              {" "}
              <FontAwesomeIcon icon={faEraser} className="faEraser cursor" />
            </button>
            <button className="inputCloseBtn" onClick={closeInput}>
              {" "}
              <FontAwesomeIcon icon={faXmark} className="faXmark cursor" />
            </button>
          </div>
          <div className="category">
            <div className="menu">
              <ul>
                <li>전체보기</li>
                <li>즐겨찾기</li>
              </ul>
            </div>
            <div className="search">
              {" "}
              <FontAwesomeIcon
                icon={faMagnifyingGlass}
                className="faMagnifyingGlass cursor"
              />
            </div>
          </div>
          <div className="row flex-row wrap">
            {listBook.map((book, i) => {
              return (
                <div className="bucket img2 center ">
                  <h4>{book.title}</h4>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
};

export default Main;

(2) 메인 타이틀 클릭 시 이동할 수 있도록 수정

 <span
              className="cursor"
              onClick={() => {
                navigate("/");
              }}
            >
              2023 독서 계획
            </span>
 <Routes>
        {" "}
        <Route path="/" element={<Main />}>
          {" "}
        </Route>
        <Route path="/bookSearch" element={<BookSearch />}>
          {" "}
        </Route>
        <Route path="*" element={<div>없는 페이지에요</div>} />
      </Routes>{" "}

(3) scss도 따로 폴더 정리