Skip to content

πŸ™Jober κΈ°μ—… 연계 ν”„λ‘œμ νŠΈ "μžλ°”μžλ²„"

Notifications You must be signed in to change notification settings

hahahaday12/jober-frontend

Β 
Β 

Repository files navigation


Jober [μžλ²„]

πŸ“Œ μ›Ή μ„œλΉ„μŠ€ μ†Œκ°œ

μžλ²„μ˜ 리뉴얼 및 κ°œμ„ λœ μ„œλΉ„μŠ€

  • μ‚¬μš©μ„±κ³Ό νš¨μœ¨μ„ μ€‘μ‹¬μœΌλ‘œ λ””μžμΈκ³Ό 데이터 μ•ˆμ •μ„± κ°•ν™”
  • μ›Ήκ³Ό μ•± λͺ¨λ‘ μ΅œμ ν™”λœ λ””μžμΈμ„ 제곡
  • μ‚¬μš©μž μΉœν™”μ μΈ κ²½ν—˜κ³Ό 직관적 μΈν„°νŽ˜μ΄μŠ€λ₯Ό ν†΅ν•œ λΉ λ₯Έ 적응 λͺ©ν‘œ
  • κΈ°μ‘΄ 곡유 νŽ˜μ΄μ§€ κΈ°λŠ₯μ—μ„œ μ‘°κΈˆλ” μ—…λ°μ΄νŠΈλœ κ³΅μœ νŽ˜μ΄μ§€ κΈ°λŠ₯ κ΅¬ν˜„

μ΄μ œλŠ” μžλ²„μ—μ„œ 더 νŽΈλ¦¬ν•œ μ„œλΉ„μŠ€λ₯Ό λ§Œλ‚˜λ³΄μ„Έμš”!!πŸ€—

개발 κΈ°κ°„ : 2023λ…„ 09μ›” 14 ~ 2023.10.05



πŸ›  기술 μŠ€νƒ

Front-end 기술 μŠ€νƒ
Front-end 배포
배포 πŸ”— JavaJober[μžλ°”μžλ²„]
λ…Έμ…˜ πŸ‘‰ λ…Έμ…˜ λ°”λ‘œκ°€κΈ°


πŸ’‘ μ£Όμš” κΈ°λŠ₯

μ›Ή ν™”λ©΄ κΈ°λŠ₯
ν™ˆ
- μžλ²„μ—μ„œ 둜그인 ν›„ λ‚˜μ˜€λŠ” ν™ˆνŽ˜μ΄μ§€ APIμž…λ‹ˆλ‹€.
- ν™ˆμ—μ„œ κ°„λ‹¨ν•œ 개인 정보와 슀페이슀, λ¬Έμ„œ 등을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.
μΉ΄ν…Œκ³ λ¦¬
- μΉ΄ν…Œκ³ λ¦¬μ— λ§žμΆ°μ„œ 곡유 νŽ˜μ΄μ§€ ν˜•μ‹μ„ 각각 μ œκ³΅ν•©λ‹ˆλ‹€.
블둝 μΆ”κ°€
- 곡유 νŽ˜μ΄μ§€μ—μ„œ 블둝을 μΆ”κ°€,μ‚­μ œ 및 μž‘μ„±ν•˜μ—¬ μ €μž₯ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
- 블둝 μ’…λ₯˜μ—λŠ” 파일 블둝, λͺ©λ‘ 블둝, 자유 블둝, SNS 블둝이 μžˆμŠ΅λ‹ˆλ‹€.
ν…œν”Œλ¦Ώ
- 'ν…œν”Œλ¦Ώ μΆ”κ°€ν•˜κΈ°' νƒ­ 클릭 μ‹œ μ„ νƒν•œ μΉ΄ν…Œκ³ λ¦¬ 별 μΆ”μ²œ ν…œν”Œλ¦Ώμ΄ λ‚˜μ˜΅λ‹ˆλ‹€.
- 'ν…œν”Œλ¦Ώ μ„ νƒν•˜κΈ°' λͺ¨λ‹¬μ—μ„œ 검색 λ°” 클릭 μ‹œ λͺ¨λ“  ν…œν”Œλ¦Ώ 데이터가 μΉ΄ν…Œκ³ λ¦¬ λ³„λ‘œ λΆ„λ₯˜λ˜μ–΄ λ‚˜μ˜΅λ‹ˆλ‹€.
- 'ν…œν”Œλ¦Ώ μ„ νƒν•˜κΈ°' λͺ¨λ‹¬μ—μ„œ ν‚€μ›Œλ“œ 검색 μ‹œ ν‚€μ›Œλ“œμ— λ§žλŠ” ν…œν”Œλ¦Ώμ΄ λ‚˜μ˜΅λ‹ˆλ‹€.
μŠ€νƒ€μΌ μ„ΈνŒ…
- ν…œν”Œλ¦Ώμ— μ‚¬μš©λ˜λŠ” μŠ€νƒ€μΌμ„ μ μš©ν•  수 μžˆλŠ” νƒ­μž…λ‹ˆλ‹€.
- λ°°κ²½, 블둝 μŠ€νƒ€μΌ, ν…Œλ§ˆλ₯Ό μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
λ“œλž˜κ·Έμ•€λ“œλ‘­
- 블둝 λ³„λ‘œ λ“œλž˜κ·Έμ•€λ“œλ‘­ν•˜μ—¬ μˆœμ„œ 이동이 κ°€λŠ₯ν•©λ‹ˆλ‹€.
μž„μ‹œ μ €μž₯
- μž„μ‹œμ €μž₯ 내역이 μžˆμ„ λ•Œ, μ €μž₯ 내역을 μ΄μ–΄μ„œ μž‘μ„±ν•˜κ±°λ‚˜ μ‚­μ œν•  수 μžˆμŠ΅λ‹ˆλ‹€.
μ €μž₯ + κ³΅μœ νŽ˜μ΄μ§€ μ™„μ„±
- μ»€μŠ€ν…€ν•œ 블둝과 μŠ€νƒ€μΌμ„ μ €μž₯ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
- μ™„μ„±λœ κ³΅μœ νŽ˜μ΄μ§€λŠ” 'μ™ΈλΆ€ 곡개' 탭을 μ‚¬μš©ν•˜μ—¬ 전체 곡개 ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
κ³΅μœ νŽ˜μ΄μ§€ url
- κ³΅μœ νŽ˜μ΄μ§€ νŽΈμ§‘ν•˜κΈ° μ‹œ url도 μ»€μŠ€ν…€μ΄ κ°€λŠ₯ν•©λ‹ˆλ‹€.
- url둜 κ³΅μœ νŽ˜μ΄μ§€μ— 접근이 κ°€λŠ₯ν•©λ‹ˆλ‹€.

✨ κΈ°λŠ₯ κ΅¬ν˜„μ€‘ 였λ₯˜ & ν•΄κ²°

πŸ’1. 곡톡 modalComponents μ•ˆμ— λ³€κ²½λ˜λŠ” λ§Žμ€ modalContents 관리

κΈ°λŠ₯ κ΅¬ν˜„ λͺ¨μŠ΅

ezgif com-video-to-gif (20)

κΈ°λŠ₯ κ΅¬ν˜„μ€‘ 문제점

-> ν…œν”Œλ¦Ώ 생성 ν΄λ¦­μ‹œ μΆ”μ²œ ν…œν”Œλ¦Ώβ–Ά input창에 focusμ‹œ μΉ΄ν…Œκ³ λ¦¬ ν…œν”Œλ¦Ώ β–Ά input창에 검색어 μž…λ ₯μ‹œ 검색 ν…œν”Œλ¦Ώ 총 3번의 νŽ˜μ΄μ§€ μƒνƒœ λ³€ν™”κ°€ 있게 λ©λ‹ˆλ‹€.
처음 μ½”λ“œ μž‘μ„±μ‹œ 곡톡 λͺ¨λ‹¬ λ ˆμ΄μ•„μ›ƒ μ•ˆμ— λͺ¨λ“  νŽ˜μ΄μ§€λ₯Ό 관리할 각각의 state 값을 μƒμ„±ν•˜κ³  true, false 둜 λͺ¨λ‹¬μ•ˆμ˜ 컨텐츠 μƒνƒœκ°’μ„ λ³€κ²½ν•˜κ²Œ ν•˜μ˜€μœΌλ©°, 2번째 νŽ˜μ΄μ§€κ°€ λ³΄μΌμ‹œ 1번째 νŽ˜μ΄μ§€κ°€ 보이지 μ•Šλ„λ‘ ν•˜κΈ° μœ„ν•΄ false 값을 μ£Όμ—ˆμŠ΅λ‹ˆλ‹€.
μ΄λ ‡κ²Œ ν•˜λ‚˜μ˜ μ»΄ν¬λ„ŒνŠΈκ°€ λ³€κ²½λ λ•Œλ§ˆλ‹€ 이전 μ»΄ν¬λ„ŒνŠΈκ°€ 보이지 μ•Šκ²Œ ν•˜κΈ° μœ„ν•΄ true, false(boolean νƒ€μž…)으둜 μ»΄ν¬λ„ŒνŠΈ 관리λ₯Ό ν•˜λ‹€λ³΄λ‹ˆ , 곡톡 λͺ¨λ‹¬ μ•ˆμ— 더 λ§Žμ€ μ»΄ν¬λ„ŒνŠΈκ°€ λ³€κ²½λ μ‹œ 관리 ν•˜κΈ° μ–΄λ ΅κ³  μ½”λ“œκ°€ 볡작 ν•΄μ§€λŠ” 문제점이 μƒκ²ΌμŠ΅λ‹ˆλ‹€.

초기 κΈ°λŠ₯ κ΅¬ν˜„ μ½”λ“œ

export const ModalOpen = () => {
  const { Search } = Input;
  // λͺ¨λ‹¬ μ˜€ν”ˆμ„ κ΄€λ¦¬ν•˜κΈ° μœ„ν•œ μƒνƒœκ΄€λ¦¬
  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
  // 처음 μΆ”μ²œ ν…œν”Œλ¦Ώμ„ 보여주기 μœ„ν•œ μƒνƒœκ΄€λ¦¬
  const [showBestTemplate, setShowBestTemplate] = useState<boolean>(true);
  // 인풋창에 ν¬μ»€μŠ€μ‹œ 보여주기 μœ„ν•œ μƒνƒœκ΄€λ¦¬
  const [categoryTemplate, setCategoryTemplate] = useState<boolean>(false);
  // 인풋창에 μž…λ ₯μ‹œ λ³€κ²½λ˜λŠ” μƒνƒœκ΄€λ¦¬
  const [inputText, setInputText] = useState('');
  // 검색 λ²„νŠΌ ν΄λ¦­μ‹œ μ‹€ν–‰λ˜λŠ” ν•¨μˆ˜
  const onSearch = (value: string) => {
    console.log(value);
    alert('');
  };
  // λͺ¨λ‹¬μ°½μ„ λ³΄μ—¬μ£ΌλŠ” ν•¨μˆ˜
  const showModal = () => {
    setIsModalOpen(true);
    setShowBestTemplate(true);
    setCategoryTemplate(false);
  };

  const handleSearchFocus = () => {
    setShowBestTemplate(false); // Search μž…λ ₯에 ν¬μ»€μŠ€κ°€ 클릭되면 BestTemplate μˆ¨κΉ€
    setCategoryTemplate(true);
    if (inputText.length > 0) {
      setCategoryTemplate(false);
    } else {
      return;
    }
  };

  const handleOk = () => {
    setIsModalOpen(false);
    setShowBestTemplate(false);
    //setInputText('');
  };

  const handleCancel = () => {
    alert('μ·¨μ†Œ');
    setIsModalOpen(false);
    setShowBestTemplate(true);
    setCategoryTemplate(false);
    //setInputText('');
  };

  const handleChangeText = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInputText(e.target.value);
    if (e.target.value.length > 0) {
      setCategoryTemplate(false);
      setShowBestTemplate(false);
      console.log(e.target.value);
    } else {
      setCategoryTemplate(true);
    }
  };

  return (
    <>
      <Button className="buttonOpen" type="primary" onClick={showModal}>
        ν…œν”Œλ¦Ώ 생성
      </Button>
      <Modals
        title="Basic Modal"
        open={isModalOpen}
        onOk={handleOk}
        onCancel={handleCancel}
        maskClosable={false}
      >
        <ModalHeader>
          <p>ν…œν”Œλ¦Ώ μ„ νƒν•˜κΈ°</p>
        </ModalHeader>
        <SettingTemplet>
          <p className="settingtText">ν…œν”Œλ¦Ώ μ„€μ •ν•˜κΈ°</p>
          <SelectBox>
            <InputBox>
              <Select
                className="selectbox"
                defaultValue="λ¬Έμ„œμ œλͺ©"
                allowClear
                options={[{ value: 'λ¬Έμ„œ', label: 'λ¬Έμ„œμ œλͺ©' }]}
              />
              <Search
                className="searchBox"
                type="text"
                placeholder="input search text"
                onSearch={onSearch}
                onFocus={handleSearchFocus}
                value={inputText}
                onChange={handleChangeText}
              />
            </InputBox>
            // λ³€κ²½μ „
            {showBestTemplate && <BestTemplate />}
            {categoryTemplate && <CategoryTemplet />}
            {inputText && <SelecteSearchTemplate inputText={inputText} />}
          </SelectBox>
        </SettingTemplet>
      </Modals>
    </>
  );
};

ν•΄κ²° λ°©μ•ˆ

-> 킀값에 λ§žλŠ” μ»΄ν¬λ„ŒνŠΈ 객체λ₯Ό μƒμ„±ν•˜μ—¬ ν•΄λ‹Ή 객체λ₯Ό μƒνƒœκ΄€λ¦¬ ν•˜λ„λ‘ κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€. ν•˜λ‚˜μ˜ setState λ₯Ό ν†΅ν•˜μ—¬ 각각의 μ»΄ν¬λ„ŒνŠΈλ₯Ό λ³€κ²½μ‹œμΌœ 주도둝 ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

κ°œμ„  ν›„ μ½”λ“œ

export const ModalOpen = () => {
  const { Search } = Input;

  // modal contents λ₯Ό κ΄€λ¦¬ν•˜λŠ” state, type 생성
  const [procedure, setProcedure] = useState<'recommand' | 'category' | 'search'>('recommand');

  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
  const [inputText, setInputText] = useState('');

  // 킀값에 λ§žλŠ” μ»΄ν¬λ„ŒνŠΈ 객체 생성
  const PROCEDURE_MAPPER = {
    recommand: <BestTemplate />,
    category: <CategoryTemplate />,
    search: <SelecteSearchTemplate inputText={inputText} />,
  };
  // 검색 λ²„νŠΌ ν΄λ¦­μ‹œ μ‹€ν–‰λ˜λŠ” ν•¨μˆ˜
  const onSearch = (value: string) => {
    console.log(value);
    alert('');
  };
  // λͺ¨λ‹¬μ°½μ„ λ³΄μ—¬μ£ΌλŠ” ν•¨μˆ˜
  const showModal = () => {
    setIsModalOpen(true);
  };
  const handleSearchFocus = () => {
    setProcedure('category');
  };
  const handleOk = () => {
    setIsModalOpen(false);
    setInputText('');
    setProcedure('recommand');
  };
  const handleCancel = () => {
    setIsModalOpen(false);
    setInputText('');
    setProcedure('recommand');
  };
  const handleChangeText = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInputText(e.target.value);
    if (e.target.value.length > 0) {
      setProcedure('search');
    } else {
      setProcedure('category');
    }
  };
  return (
    <>
      <Button className="buttonOpen" type="primary" onClick={showModal}>
        ν…œν”Œλ¦Ώ 생성
      </Button>
      <Modals
        centered
        title={
          <ModalHeader
            title="ν…œν”Œλ¦Ώ μ„ νƒν•˜κΈ°"
            handleOk={handleOk}
            handleCloseModal={handleCancel}
          />
        }
        footer={null}
        open={isModalOpen}
        onOk={handleOk}
        onCancel={handleCancel}
        closeIcon={null}
      >
        <SettingTemplet>
          <p className="settingtText">ν…œν”Œλ¦Ώ μ„€μ •ν•˜κΈ°</p>
          <SelectBox>
            <InputBox>
              <Select
                className="selectbox"
                defaultValue="λ¬Έμ„œμ œλͺ©"
                allowClear
                options={[{ value: 'λ¬Έμ„œ', label: 'λ¬Έμ„œμ œλͺ©' }]}
              />
              <Search
                className="searchBox"
                type="text"
                placeholder="input search text"
                onSearch={onSearch}
                onFocus={handleSearchFocus}
                value={inputText}
                onChange={handleChangeText}
              />
            </InputBox>
            // λ³€κ²½ν›„
            {PROCEDURE_MAPPER[procedure]}
          </SelectBox>
        </SettingTemplet>
      </Modals>
    </>
  );
};

πŸ’2. κ²€μƒ‰νŽ˜μ΄μ§€ κΈ°λŠ₯ κ΅¬ν˜„ 방법과 였λ₯˜

πŸŽˆλ°©λ²• 1
πŸ“μ²˜μŒ νŽ˜μ΄μ§€ mount μ‹œ μ„œλ²„μ—μ„œ λͺ¨λ“  데이터λ₯Ό κ°€μ Έμ˜€λŠ” apiλ₯Ό ν˜ΈμΆœν›„ ν”„λ‘ νŠΈμ—μ„œ filter μ²˜λ¦¬ν›„ κ²°κ³Όκ°’ λ…ΈμΆœ
-> ν•΄λ‹Ή λ°©λ²•μœΌλ‘œ κΈ°λŠ₯ κ΅¬ν˜„μ‹œ μƒκΈ°λŠ” 문제점 = 데이터 변경이 많이 μžˆμ„ 경우 ν”„λ‘ νŠΈμ—μ„œ filter 처리λ₯Ό ν•˜λ©΄ μ΅œμ‹ μœΌλ‘œ λ°˜μ˜λ˜λŠ” 데이터λ₯Ό κ°€μ Έμ˜€μ§€ λͺ»ν•˜λŠ” λ¬Έμ œμ μ„ μƒκ°ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
λ˜ν•œ 데이터가 λ§Žμ„μˆ˜λ‘ λͺ¨λ“  데이터λ₯Ό λ°›μ•„μ˜€λŠ”κ²ƒμ€ μ„±λŠ₯ μ μœΌλ‘œλ„ 쒋지 μ•Šμ„κ²ƒ κ°™λ‹€κ³  νŒλ‹¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

πŸŽˆλ°©λ²• 2
πŸ“μ„œλ²„μ—μ„œ μž…λ ₯값에 λŒ€ν•΄ ν•„ν„°λ§λœ 데이터에 λŒ€ν•œ api λ₯Ό μ‚¬μš©ν•˜μ—¬ κ²°κ³Όκ°’ λ…ΈμΆœ
-> 첫번째 λ°©μ‹μ—μ„œμ˜ λ¬Έμ œμ μ„ μƒκ°ν•˜μ—¬ λ‘λ²ˆμ§Έ λ°©μ‹μœΌλ‘œ 검색 νŽ˜μ΄μ§€λ₯Ό κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€. 따라 μ„œλ²„μ—μ„œ μž…λ ₯값에 λŒ€ν•΄ ν•„ν„°λ§λœ apiλ₯Ό 생성후 ν•΄λ‹Ή apiλ₯Ό μ΄μš©ν•˜μ—¬ 검색 νŽ˜μ΄μ§€λ₯Ό κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

이전 μ½”λ“œ

export const SelecteSearchTemplate: React.FC<Props> = ({ keyword }) => {
  const [product, setProductInfo] = useState<ProductItem[]>([]);
  const [filteredResults, setFilteredResults] = useState<ProductItem[]>([]);

  useEffect(() => {
    const getData = async () => {
      try {
        const response = await fetch(
          `${import.meta.env.VITE_SERVER_BASE_URL}/${keyword}`,
        );
        if (response.ok) {
          const data = await response.json();
          setProductInfo([...product, ...data]);
        } else {
          console.error('Response not OK:', response);
        }
      } catch (error) {
        console.error('Error while fetching data:', error);
      }
    };
    getData();
  }, [keyword, product]);

  useEffect(() => {
    const filteredResults = product.filter((item) =>
      item.title.toLowerCase().includes(keyword),
    );
    setFilteredResults(filteredResults);
  }, [keyword, product]);

  return (
    <>
      <SeleteContainer>
        <h3>검색결과</h3>
        <ResultBox>
          {filteredResults.map((item) => (
            <ResultTemBox key={item.id}>{item.title}</ResultTemBox>
          ))}
        </ResultBox>
        <BestTemplate />
      </SeleteContainer>
    </>
  );
};

이후 μ½”λ“œ

export const SelecteSearchTemplate: React.FC<Props> = ({ inputText }) => {
  const [debouncedInputValue, setDebouncedInputValue] = useState('');
  const [products, setProducts] = useState<ProductItem[]>([]);

  useEffect(() => {
    // μž…λ ₯값이 변경될 λ•Œλ§ˆλ‹€ debounce된 값을 μ—…λ°μ΄νŠΈ.
    const debounceTimer = setTimeout(() => {
      setDebouncedInputValue(inputText);
    }, 300); // 300 λ°€λ¦¬μ΄ˆ(0.3초) λ””λ°”μš΄μŠ€ μ‹œκ°„

    return () => {
      // 이전 타이머λ₯Ό 클리어.
      clearTimeout(debounceTimer);
    };
  }, [inputText]);

  useEffect(() => {
    if (debouncedInputValue) {
      const getData = async () => {
        try {
          const response = await axios.get(`${import.meta.env.VITE_SERVER_BASE_URL}`, {
            params: {
              search: debouncedInputValue,
            },
          });
          const data = response.data.data.list;
          setProducts([...data]);
        } catch (error) {
          console.error('API 호좜 μ—λŸ¬:', error);
        }
      };
      getData();
    } else {
      setProducts([]);
    }
  }, [debouncedInputValue]);

  return (
    <>
      <SeleteContainer>
        <h3>{templateText.inputResult}</h3>
        <ResultBox>
          {products.map((item) => (
            <ResultTemBox key={item.templateId}>
              {item.templateTitle} <br />
              {item.templateDescription}
            </ResultTemBox>
          ))}
        </ResultBox>
        <BestTemplate PERSONAL={''} />
      </SeleteContainer>
    </>
  );
};

-> μ„œλ²„μ—μ„œ user μž…λ ₯값에 λŒ€ν•΄ filter 처리λ₯Ό ν•˜κ³ , debounce λ₯Ό μ‚¬μš©ν•˜μ—¬ κΈ°λŠ₯ κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

πŸ‘€debounce λž€?

-> 일정 μ‹œκ°„ λ™μ•ˆ μ—°μ†μ μœΌλ‘œ λ°œμƒν–ˆλ˜ μ΄λ²€νŠΈλ“€ 쀑 λ§ˆμ§€λ§‰λ§Œ μ‹€ν–‰μ‹œμΌœ κ³Όλ‹€ν•œ ν˜ΈμΆœμ΄λ‚˜ λ Œλ”λ₯Ό 막아 μ΅œμ ν™”ν•˜λŠ” 기술 μž…λ‹ˆλ‹€.

따라 μ‚¬μš©μžκ°€ 검색창에 타이핑 ν• λ•Œλ§ˆλ‹€ Apiκ°€ ν˜ΈμΆœλ˜λŠ”κ²ƒμ΄ μ•„λ‹Œ , debounce λ₯Ό μ‚¬μš©ν•˜μ—¬ λ§ˆμ§€λ§‰μ— 타이핑 μž…λ ₯ν• λ•Œ Apiκ°€ ν˜ΈμΆœλ˜λ„λ‘ κΈ°λŠ₯ κ΅¬ν˜„μ„ ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

πŸ’3. antd button components μ‚¬μš©μ‹œ ν•œκ°œμ˜ λ²„νŠΌλ§Œ μ„ νƒλ˜λŠ”κ²Œ μ•„λ‹Œ, μ—¬λŸ¬ λ²„νŠΌ 선택됨 ( λ‹€μŒ 체크 λ²„νŠΌ ν΄λ¦­μ‹œ 이전 클릭된 체크 λ²„νŠΌμ€ 없어져야함)

였λ₯˜ 이미지

image

이전 μ½”λ“œ

-> radio λ²„νŠΌ ν΄λ¦­μ‹œ handleRadioChange ν•¨μˆ˜ μ‹€ν–‰.

 <Radio
     onChange={() => handleRadioChange(item)}
 />

μˆ˜μ • μ½”λ“œ

-> radio λ²„νŠΌμ˜ 속성값 checkedλ₯Ό μ΄μš©ν•˜μ—¬ μ„ νƒν•œν…œν”Œλ¦Ώμ˜ 아이디와 , λ…ΈμΆœλœ ν…œν”Œλ¦Ώμ˜ 아이디가 κ°™μ•„ true이 λ˜μ–΄μ•Ό 체크가 λ˜λ„λ‘ 쑰건식을 μΆ”κ°€ ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

 <Radio
     onChange={() => handleRadioChange(item)}
     checked={
      selectedTemplate &&
       selectedTemplate.templateId === item.templateId
     }
 />

μˆ˜μ •ν›„

-> ν•œκ°œμ˜ λ²„νŠΌλ§Œ μ„ νƒλ©λ‹ˆλ‹€.

ezgif com-video-to-gif (19)

πŸ’4. 미리보기 νŽ˜μ΄μ§€ κ΅¬ν˜„ 및 문제점

미리보기 νŽ˜μ΄μ§€ κ΅¬ν˜„μ„ μœ„ν•΄ μƒνƒœκ΄€λ¦¬ 라이브러리 zustand λ₯Ό μ‚¬μš©ν•΄ Radio button ν΄λ¦­μ‹œ ν•΄λ‹Ή 데이터가 store에 μ €μž₯ν•˜λ„λ‘ κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

-> 각 νŽ˜μ΄μ§€ λ§ˆλ‹€ νŽ¨ν”Œλ¦Ώ μ˜†μ— radioλ²„νŠΌμ„ μ„ νƒν• μˆ˜ 있게 되고, μ„ νƒμ‹œ ν•΄λ‹Ή id,title, description 이 전역관리 μƒνƒœ store μ €μž₯됨.

μž‘μ„± μ½”λ“œ

πŸ“‚store.ts -> store 와 type 생성

type TemplateState = {
  selectedTemplate: {
    category: string;
    id: string;
    title: string;
    description: string;
  };
  setSelectedTemplate: (template: {
    category: string;
    id: string;
    title: string;
    description: string;
  }) => void;
};

export const useTemplateStore = create<TemplateState>((set) => ({
  selectedTemplate: {
    category: '',
    id: '',
    title: '',
    description: '',
  },
  setSelectedTemplate: (template) =>
    set({ selectedTemplate: template }),
}));

πŸ“‚RecommendInner.ts -> λ§Œλ“€μ–΄μ§„ store에 μ„ νƒν•œ ν…œν”Œλ¦Ώ 데이터 μ €μž₯

const { setSelectedTemplate } = useTemplateStore();

const handleRadioChange = (item: TemplateData, status: boolean) => {
    const param = {
      category: PERSONAL,
      id: item.id,
      title: item.title,
      description: item.description,
    };
    console.log(item);
    console.log(status);
    setSelectedTemplate(param);
  };

return(
 <Radio
    onChange={(e) => handleRadioChange(item, e.target.checked)}
 />
)

-> store에 λ§Œλ“€μ–΄μ§„ setSelectedTemplate λ₯Ό μ΄μš©ν•΄μ„œ 데이터 μ €μž₯

πŸ”₯ μœ„μ˜ λ°©μ‹μœΌλ‘œ κΈ°λŠ₯ κ΅¬ν˜„ν–ˆμ„λ•Œ 생긴 문제점 및 ν•΄κ²° 방식

문제점 : ν…œν”Œλ¦Ώμ— μžˆλŠ” radio button ν΄λ¦­μ‹œ ν•΄λ‹Ή 데이터λ₯Ό store에 μ €μž₯ν•˜κ³  store을 κ΅¬λ…ν•˜κ³  μžˆλŠ” wallcomponent에 ν•΄λ‹Ή 데이터가 λ°”λ‘œ λ‚˜νƒ€λ‚΄λŠ” 문제점이 μƒκ²ΌμŠ΅λ‹ˆλ‹€.
radio button ν΄λ¦­μ‹œ λ°”λ‘œ λ“±λ‘λœ ν…œν”Œλ¦Ώμ΄ λ³΄μ΄λŠ”κ²Œ μ•„λ‹Œ, radio button 클릭후 "μ™„λ£Œ" λ²„νŠΌμ„ λˆŒλŸ¬μ•Ό λͺ¨λ‹¬μ°½μ΄ λ‹«νž˜κ³Ό λ™μ‹œμ— wallcomponent에 λ“±λ‘λœ ν…œν”Œλ¦Ώμ΄ 보여야 ν•©λ‹ˆλ‹€.

ν•΄κ²° 방법 -> true, false μƒνƒœκ°’μ— λŒ€ν•œ 쑰건식을 μΆ”κ°€ν•΄μ„œ radio button ν΄λ¦­μ‹œμ—λŠ” μƒνƒœκ°€ false 이고, "확인" λ²„νŠΌ ν΄λ¦­μ‹œμ—λŠ” true. true μΌλ•Œλ§Œ ν…œν”Œλ¦Ώ 등둝이 λ˜λŠ” 둜직으둜 κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

μˆ˜μ • μ½”λ“œ πŸ“‚store.ts

export const useTemplateStore = create<TemplateState>((set) => ({
  selectedTemplate: {
    category: '',
    templateId: '',
    templateTitle: '',
    templateDescription: '',
  },
  setSelectedTemplate: (template) => set({ selectedTemplate: template }),
 // μƒˆλ‘œμš΄ μƒνƒœκ°’ μΆ”κ°€ 
  newStatus: false,
  setNewStatus: (newStatus) => set({ newStatus }),
}));

πŸ“‚RecommedInner.tsx

const handleRadioChange = (item: TemplateData) => {
    const param = {
      category: PERSONAL,
      templateId: item.templateId,
      templateTitle: item.templateTitle,
      templateDescription: item.templateDescription, 
    };
    setSelectedTemplate(param);
    // radio λ²„νŠΌ ν΄λ¦­μ‹œ Status false
    setNewStatus(false);
  };

πŸ“‚TemplateModal.tsx

  const { selectedTemplate, newStatus } = useTemplateStore();
  const [templateHistory, setTemplateHistory] = useState<Array<TemplateItem>>(
    [],
  );
  useEffect(() => {
    // μƒνƒœκ°’ 쑰건식을 ν†΅ν•˜μ—¬ μ €μž₯된 ν…œν”Œλ¦Ώμ„ λ³΄μ—¬μ€Œ.
    if (newStatus) {
      setTemplateHistory((prevHistory) => [...prevHistory, selectedTemplate]);
    }
  }, [newStatus, selectedTemplate]);

  return(
    <BlockContainer blockName="templateBlock">
      <div
        className={`
        ${isEdit && 'px-[8px] pb-[8px] pt-[30px]'} 
        gap-4 grid sm:grid-cols-2 grid-cols-1
        `}
        >
        {templateBlockSubData?.map((template) => (
          <SingleTemplate
            key={template.templateBlockUUID}
            templateTitle={template.templateTitle}
            templateDescription={template.templateDescription}
          />
        ))}
       {isEdit && (
          <>
          <BlockContainer blockName="template">
           <div className="sm:h-[210px] h-[115px] flex flex-col items-center justify-center gap-[8px] dm-16" ref={templateAddButtonRef}>
            <ModalOpen />
           </div>
          </BlockContainer>
       {templateHistory.map((template, index) => (
          <BlockContainer key={index} blockName="template">
            <div className="sm:h-[210px] h-[115px] p-block">
              <div className="flex items-center justify-between mb-[12px]">
                <h4 className="db-18 sm:db-20">{template.templateTitle}</h4>
              </div>
              <div className="flex sm:gap-[8px] gap-[6px]">
                <p className="dm-16 text-gray88">
                  {template.templateDescription}
                </p>
              </div>
            </div>
          </BlockContainer>
       ))}
      )

μˆ˜μ •ν›„ 생긴 2μ°¨ 문제점 πŸ”₯문제점 : μ•„λž˜ 이미지 처럼 μΆ”κ°€ ν• λ•Œλ§ˆλ‹€ μ•ˆμ— ν…œν”Œλ¦Ώμ΄ μΆ”κ°€λ λ•Œλ§ˆλ‹€ ν…œν”Œλ¦Ώ μƒμ„±μ˜ λΈ”λŸ­μ΄ μžμ—°μŠ€λŸ½κ²Œ λ°€λ €λ‚˜μ•Ό ν•˜λŠ”λ°, μœ„μ˜ λ°©λ²•λŒ€λ‘œ κ΅¬ν˜„ν•˜λ©΄ ν…œν”Œλ¦Ώμ΄ μΆ”κ°€λ˜λ„ ν…œν”Œλ¦Ώ μƒμ„±μ˜ λΈ”λŸ­μ˜ μœ„μΉ˜λŠ” κ·ΈλŒ€λ‘œ μžˆλŠ”
λΆ€μžμ—°μŠ€λŸ¬μš΄ λͺ¨μŠ΅μ΄ λ³΄μž…λ‹ˆλ‹€.

κΈ°λŠ₯ κ΅¬ν˜„λͺ¨μŠ΅

image

였λ₯˜ 해결방법 -> ν˜„μž¬ store에 μ €μž₯된 λ°μ΄ν„°λ‘œ wall λ°μ΄ν„°λ‘œ μ „μ—­μ μœΌλ‘œ μ“°κ³  μžˆλ‹€.

store.tsx

export const useWallStore = create<WallStoreType>((set) => ({
  isEdit: false,
  setIsEdit: (bool) => set(() => ({ isEdit: bool })),
  isPreview: false,
  setIsPreview: (bool) => set(() => ({ isPreview: bool })),

  getWall: async () => {
    const response = await fetch('http://localhost:3000/wall');
    if (response.ok) {
      set({ wall: await response.json() });
    }
  },

  wall: {} as WallType,
  setWall: (states: object) =>
    set((state) => ({ wall: { ...state.wall, ...states } })),
}));

μœ„μ˜ ν…œν”Œλ¦Ώ 블둝에 λŒ€ν•œ μ»΄ν¬λ„ŒνŠΈ μ½”λ“œλŠ” μ•„λž˜μ™€ κ°™λ‹€.

SingleTemplate.tsx


πŸ’5.

✨ ν”„λ‘œμ νŠΈ λ₯Ό ν•˜λ©΄μ„œ 크게 λ°°μ› λ˜ "λ¦¬μ•‘νŠΈ λΆˆλ³€μ„±" 에 κ΄€ν•˜μ—¬.

λ¦¬μ•‘νŠΈμ—μ„œλŠ” state의 λΆˆλ³€μ„±μ„ μ§€μΌœμ•Ό ν•©λ‹ˆλ‹€.

import { useState } from 'react';

export default function App() {
  const [cat, setCat] = useState({
    name: 'howoo',
    age: 6,
  });

  const handleChangeCatName = () => {
    cat.name = 'mango';
    setCat(cat);
  };
  console.log(cat); //{ name: 'mango', age: 6 }

  return (
    <div style={{ textAlign: 'center' }}>
      <div>고양이 이름 : {cat.name}</div>
      <button onClick={handleChangeCatName}>이름변경</button>
    </div>
  );
}

λ²„νŠΌμ„ λˆ„λ₯΄λ©΄ console.log(cat)을 톡해 μ‹€μž¬ cat.name은 변경이 λœκ²ƒμ„ 확인할 수 μžˆμ§€λ§Œ cat의 참쑰값은 κ·ΈλŒ€λ‘œμ΄κΈ° λ•Œλ¬Έμ— μž¬λžœλ”λ§μ΄ λ°œμƒν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

image

λΆˆλ³€μ„±μ„ μ§€μΌœμ•Όν•œλ‹€λŠ” μ˜λ―ΈλŠ” 얕은 비ꡐλ₯Ό ν•˜λŠ” λ¦¬μ•‘νŠΈμ˜ νŠΉμ„±μƒ μ°Έμ‘°ν˜• λ°μ΄ν„°μ˜ 원본은 λ³€ν•˜μ§€ μ•Šκ²Œ μœ μ§€ν•΄μ•Όν•˜κ³  μž¬λžœλ”λ§μ„ μœ„ν•΄ μƒˆλ‘œμš΄ 참쑰값을 setν•΄μ•Ό 함을 의미 ν•©λ‹ˆλ‹€.

λ³Έ ν”„λ‘œμ νŠΈμ—μ„œλŠ” wall(κ³΅μš©νŽ˜μ΄μ§€μ—μ„œ λ³΄μ—¬μ§€λŠ” λͺ¨λ“  정보) 객체가 μžˆμŠ΅λ‹ˆλ‹€.

const wall = {
  category: 'personal',
  memberId: 1,
  spaceId: 1,
  shareURL: 'howooking',
  wallInfoBlock: {
    wallInfoBlockId: 9,
    wallInfoTitle: '이호우',
    wallInfoDescription: 'μ•ˆλ…•ν•˜μ„Έμš”. 고양이 개발자 μ΄ν˜Έμš°μž…λ‹ˆλ‹€.',
    wallInfoImgURL: 'https://avatars.githubusercontent.com/u/87072568?v=4',
    backgroundImgURL:
      'https://images.unsplash.com/photo-1696251143046-2d32fb985b59?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2670&q=80',
  },
  blocks: [
    {
      blockUUID: '1108fff1-0106-4340-b505-280e15626ecc',
      blockType: 'listBlock',
      subData: [
        {
          listBlockId: 33,
          listLabel: 'ν•™λ ₯/κ²½λ ₯',
          listTitle: 'ν•™λ ₯',
          listDescription: 'μ„œμšΈλŒ€ν•™κ΅',
          isLink: false,
        },
      ],
    },
    ... μƒλž΅

βœ… 문제점의 μ‹œμ΄ˆ

-> 곡유 νŽ˜μ΄μ§€μ—μ„œ λ°œμƒν•˜λŠ” λͺ¨λ“  onChange μ΄λ²€νŠΈλŠ” wall λ‚΄λΆ€ 값듀을 μ‹€μ‹œκ°„μ„ λ³€κ²½μ‹œμΌœμ•Ό ν•©λ‹ˆλ‹€.

예λ₯Ό λ“€μ–΄ wall.wallInfoBlock.wallInfoTitle값을 μƒˆλ‘œμš΄ κ°’μœΌλ‘œ λ³€κ²½ν•˜κΈ° μœ„ν•΄μ„œλŠ” λ‹€μŒκ³Ό 같이 ν•΄μ•Ό ν•©λ‹ˆλ‹€.

setWall({
  ...wall,
  wallInfoBlock: { ...wall.wallInfoBlock, wallInfoTitle: 'μƒˆλ‘œμš΄ κ°’' },
});

μœ„μ™€ 같이 wall 객체의 κΉŠμ΄κ°€ 얕은 κ²½μš°λŠ” 어렡지 μ•Šκ²Œ λΆˆλ³€μ„±μ„ 지킬 수 μžˆμœΌλ‚˜ κΉŠμ΄κ°€ κΉŠμ–΄μ§μ— 따라 λΆˆλ³€μ„±μ„ μ§€ν‚€λŠ” 것은 λΆˆκ°€λŠ₯에 κ°€κΉŒμ›Œ μ§‘λ‹ˆλ‹€.

βœ… 문제 ν•΄κ²° 방법

-> 이 문제λ₯Ό ν•΄κ²°ν•΄μ£ΌλŠ” λΌμ΄λΈŒλŸ¬λ¦¬κ°€ 'IMMER' μž…λ‹ˆλ‹€.
λ¬Έμ œμ μ— λŒ€ν•œ ν•΄κ²° 방법을 μ°Ύκ³  ν•΄λ‹Ή 라이브러리λ₯Ό μ°Ύμ•„ μ μš©ν•˜κΈ°κΉŒμ§€ λ§Žμ€ μ‹œκ°„μ΄ κ±Έλ ΈμŠ΅λ‹ˆλ‹€.
μ΄μ „μ—λŠ” react 의 μž₯점만 κ²½ν—˜ν–ˆλ˜ λΆ€λΆ„κ³ΌλŠ” λ‹€λ₯΄κ²Œ, ν•΄λ‹Ή 문제λ₯Ό κ²ͺμœΌλ©΄μ„œ react 의 단점도 ν™•μ—°ν•˜κ²Œ λŠλ‚„μˆ˜ 있게 된 κ²½ν—˜μ΄μ˜€μŠ΅λ‹ˆλ‹€.
μ‚¬μš©ν•˜λŠ” 기술 μŠ€νƒμ— λŒ€ν•΄ μž₯,단점을 λͺ¨λ‘ 깨닫은 후에 ν•΄κ²° λ°©μ•ˆμ„ 찾던 도쀑 react의 단점을 μ΅œμ†Œν™” ν• μˆ˜ 있고, 더 λ‚˜μ€ μ½”λ“œ κ°œμ„ μ„ μœ„ν•œ 라이브러리 `IMMER'을 μ„ νƒν•˜κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

IMMERλ₯Ό μ‚¬μš©ν•˜λ©΄ 기쑴의 객체의 κ°’λ₯Ό λ‹€λ£¨λŠ” 문법을 μ‚¬μš©ν•˜μ—¬ stateλ₯Ό μ—…λ°μ΄νŠΈ μ‹œμΌœμ€„ 수 μžˆμŠ΅λ‹ˆλ‹€.

βœ… IMMER 적용 방법

import { produce } from 'immer';

setWall(
  produce(wall, (draft) => {
    draft.wallInfoBlock.wallInfoTitle = 'μƒˆλ‘œμš΄ κ°’';
  }),
);

πŸ“‚ ν”„λ‘œμ νŠΈ ꡬ성도

아킀텍쳐(Architecture)
개체-관계 λͺ¨λΈ (ERD)

πŸ“‚ API λͺ…μ„Έμ„œ πŸ”—

API λͺ…μ„Έμ„œ

πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ 개발 νŒ€ μ†Œκ°œ

πŸ’œ Front-end
μ΄μ •μš° ν”„λ‘œν•„ κΉ€ν•˜μ€ ν”„λ‘œν•„ λ°©λ―Έμ„  ν”„λ‘œν•„
μ΄μ •μš°(νŒ€μž₯)
(Front-end)
κΉ€ν•˜μ€
(Front-end)
λ°©λ―Έμ„ 
(Front-end)
πŸ’œ Back-end
이미연 ν”„λ‘œν•„ μ„ μ˜ˆμ€ ν”„λ‘œν•„ μ–‘μˆ˜ν˜„ ν”„λ‘œν•„ κΉ€ν¬ν˜„ ν”„λ‘œν•„ μœ€ν˜„μ§„ ν”„λ‘œν•„
이미연(νŒ€μž₯)
(Back-end)
μ„ μ˜ˆμ€
(Back-end)
μ–‘μˆ˜ν˜„
(Back-end)
κΉ€ν¬ν˜„
(Back-end)
μœ€ν˜„μ§„
(Back-end)

About

πŸ™Jober κΈ°μ—… 연계 ν”„λ‘œμ νŠΈ "μžλ°”μžλ²„"

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 97.9%
  • Other 2.1%