[Javascript] Set 객체를 활용해 UI 만들기 - 약관 동의 UI, 태그 선택 UI

박세화·2024년 1월 24일
0

Javascript

목록 보기
12/12

약관 동의 UI

import { useState } from "react";
import "./styles.css";

export default function App() {
  const [selectedTerms, setSelectedTerms] = useState<Set<number>>(new Set());

  const terms = [
    { text: "약관 1", id: 1 },
    { text: "약관 2", id: 2 },
    { text: "약관 3", id: 3 },
    { text: "약관 4", id: 4 },
    { text: "약관 5", id: 5 },
  ];

  function handleOnClick(id: number) {
    const newTerms = new Set(selectedTerms);
    if (selectedTerms.has(id)) {
      newTerms.delete(id);
    } else {
      newTerms.add(id);
    }
    setSelectedTerms(newTerms);
  }

  return (
    <div className="App">
      <h1>약관 동의 UI</h1>
      <div className="container">
        {terms.map((item) => (
          <div className="termbox">
            <div
              className={
                selectedTerms.has(item.id) ? "checkbox-checked" : "checkbox"
              }
              onClick={() => handleOnClick(item.id)}
            ></div>
            <p>{item.text}</p>
          </div>
        ))}
      </div>
    </div>
  );
}
  • selectedTerms 라는 상태값은 Set 객체 형태로 되어있다.
  • 약관 왼쪽 박스를 클릭하면 handleOnClick이 실행된다. 새로운 newTerms 라는 복제 Set을 하나 만든다.
  • 기존의 selectedTerms에 파라미터로 넘어온 id 값이 있다면 삭제하고, 없다면 추가한다. 그리고 newTerms 로 상태값을 업데이트한다.
  • selectedTerms 를 JSX 에 map 메소드를 이용해 나열한다.

태그 선택 UI

import { useState } from "react";
import TagComponent from "./TagComponent";
import "./styles.css";

type tagDataType = {
  text: string;
  id: number;
};

export default function App() {
  const tagData = [
    { text: "Tag1", id: 1 },
    { text: "Tag2", id: 2 },
    { text: "Tag3", id: 3 },
    { text: "Tag4", id: 4 },
    { text: "Tag5", id: 5 },
    { text: "Tag6", id: 6 },
    { text: "Tag7", id: 7 },
    { text: "Tag8", id: 8 },
  ];

  const [selectedTags, setSelectedTags] = useState<Set<number>>(new Set());
  const filteredTags = tagData.filter((item) => selectedTags.has(item.id));
  //useMemo 사용하여 개선 가능

  function handleOnClick(id: number) {
    const items = new Set(selectedTags);
    if (selectedTags.has(id)) {
      items.delete(id);
    } else {
      items.add(id);
    }
    setSelectedTags(items);
  }

  return (
    <div className="App">
      <h1>Set UI</h1>
      <h2>Selected Tags</h2>
      <div className="tagList">
        {filteredTags.map((item) => (
          <div className={selectedTags.has(item.id) ? "tag-selected" : "tag"}>
            {item.text}
          </div>
        ))}
      </div>

      <h2>All Tags</h2>
      <div className="tagList">
        {tagData.map((item) => (
          <div
            className={selectedTags.has(item.id) ? "tag-selected" : "tag"}
            onClick={() => handleOnClick(item.id)}
          >
            {item.text}
          </div>
        ))}
      </div>
    </div>
  );
}
  • 전체적인 로직은 위의 약관 동의 UI 와 동일하다.
  • 다만 상단에 선택된 태그들만 나열하는 부분이 추가되었다.
    filteredTags 라는 배열을 새로이 만들어서 map 메소드를 이용해 나열하고 있다.

🤔 만약 위 컴포넌트 안에 관리해야 하는 상태가 하나 혹은 그 이상 늘어난다면 어떨까?

export default function App() {
  //생략
  
  const [selectedTags, setSelectedTags] = useState<Set<number>>(new Set());
  const [trigger, setTrigger] = useState<boolean>(false);  //새로운 상태값
  const filteredTags = tagData.filter((item) => selectedTags.has(item.id)); 

  function handleButtonClick() {
    setTrigger((prev) => !prev);
  }

  function handleOnClick(id: number) {
    //생략
  }

  return (
    <div className="App">
      <h1>Set UI</h1>
      <button onClick={handleButtonClick}>클릭</button>  //추가된 라인
      <h2>Selected Tags</h2>
      <div className="tagList">
        {filteredTags.map((item) => (
          <div className={selectedTags.has(item.id) ? "tag-selected" : "tag"}>
            {item.text}
          </div>
        ))}
      </div>

      <h2>All Tags</h2>
      <div className="tagList">
        {tagData.map((item) => (
          <div
            className={selectedTags.has(item.id) ? "tag-selected" : "tag"}
            onClick={() => handleOnClick(item.id)}
          >
            {item.text}
          </div>
        ))}
      </div>
    </div>
  );
}
  • setTrigger 가 발생할 때마다 컴포넌트 전체가 다시 렌더링 되면서, const filteredTags = tagData.filter((item) => selectedTags.has(item.id)) 라인이 다시 실행되며 필터 메소드가 tagData 를 처음부터 끝까지 한 번 순회할 것이다.

  • 하지만 태그 선택을 새로이 하지 않았음에도 이 라인이 실행되는 것은 불필요하다.
    ➡️ 이럴 경우, useMemo 로 코드를 개선할 수 있다

📝 useMemo 를 활용해서 코드 개선하기

useMemo 는 컴포넌트가 처음 렌더링이 될 때 계산된 값을 메모리에 저장하여(Memoization), 이후 컴포넌트가 다시 렌더링 될 때 저장된 값을 불러온다. 따라서 useMemo 를 잘 활용한다면 불필요한 재계산을 줄여 낭비를 방지할 수 있다.

🚨 주의할 점
useMemo 가 굳이 필요하지 않을 때에도 데이터를 memoize 하게 되면 오히려 성능이 더 떨어질 수 있으므로, 필요에 따른 적절한 사용이 요구된다.

import { useMemo, useState } from "react";
import TagComponent from "./TagComponent";
import "./styles.css";

type tagDataType = {
  text: string;
  id: number;
};

export default function App() {
  const tagData = [
    { text: "Tag1", id: 1 },
    { text: "Tag2", id: 2 },
    { text: "Tag3", id: 3 },
    { text: "Tag4", id: 4 },
    { text: "Tag5", id: 5 },
    { text: "Tag6", id: 6 },
    { text: "Tag7", id: 7 },
    { text: "Tag8", id: 8 },
  ];

  const [selectedTags, setSelectedTags] = useState<Set<number>>(new Set());
  const [trigger, setTrigger] = useState<boolean>(false);

  const filteredTags = useMemo(
    () => tagData.filter((item) => selectedTags.has(item.id)),
    [selectedTags]
  );  //selectedTags 가 변경될 때에만 filter 메소드가 실행되도록 memoization

  function handleButtonClick() {
    setTrigger((prev) => !prev);
  }

  function handleOnClick(id: number) {
    const items = new Set(selectedTags);
    if (selectedTags.has(id)) {
      items.delete(id);
    } else {
      items.add(id);
    }
    setSelectedTags(items);
  }

  return (
    <div className="App">
      <h1>Set UI</h1>
      <button onClick={handleButtonClick}>클릭</button>
      <h2>Selected Tags</h2>
      <div className="tagList">
        {filteredTags.map((item) => (
          <div className={selectedTags.has(item.id) ? "tag-selected" : "tag"}>
            {item.text}
          </div>
        ))}
      </div>

      <h2>All Tags</h2>
      <div className="tagList">
        {tagData.map((item) => (
          <div
            className={selectedTags.has(item.id) ? "tag-selected" : "tag"}
            onClick={() => handleOnClick(item.id)}
          >
            {item.text}
          </div>
        ))}
      </div>
    </div>
  );
}

0개의 댓글