React에서 자료구조의 활용

김현조·2023년 2월 25일
5

FrontEnd

목록 보기
2/9
post-thumbnail

리액트로 프로젝트를 진행하다 보면 Computer Science 시간에 배운 여러 자료구조를 잊어버릴때가 있다. 이에 리액트 사용 시 자료구조에 대한 깊은 학습이 필수적으로 수반되어야 하는지에 대한 논쟁은 개발자들 사이에서도 의견이 분분하다. React(javascript, frontend) 개발자가 자료구조를 공부하지 않아도 된다는 의견에 대한 근거는 대부분 다음과 같다.

  • 프론트엔드에서 사용하는 데이터는 대부분 고급 자료구조를 요하지 않는다.
  • 심지어 JavaScript에서도 native한 자료구조를 제공하지 않는 경우가 많다.(heap - 직접 구현해야 함, queue - 배열로 구현할 경우 시간복잡도 문제로 연결리스트 구현해서 사용해야 함)
  • React의 state는 불변성을 유지해야 해서 자료구조를 제대로 활용하기 어렵다.

하지만 반대 의견도 분명한 논리가 있다.

적절한 자료구조를 선택하는 것만으로도 코드의 질(가독성, 성능…)을 높일 수 있다.

이는 다른 단점들을 보완할만큼 강력한 장점이라 할 수 있다. 그렇다면 여러 자료구조를 React에서 어떻게 활용할 수 있는지 살펴보도록 하자.

Set

JavaScript에서 Set 객체는 유일한 값을 저장하는 콜렉션이다. 한마디로 “집합”이며 순서가 없고 중복을 허용하지 않는다. 삽입, 삭제, 값 확인에 모두 O(1)의 시간복잡도를 갖는다.

체크박스 리스트의 상태를 다루는 경우, 체크된 상태의 아이템의 id만 Set에 가지고 있는 방식으로 관리할 수 있다.

import { useState } from "react";

function Set() {
  const [checkedItems, setCheckedItems] = useState(new Set());

  const handleChange = (event) => {
    const item = event.target.name;
    const isChecked = event.target.checked;
    setCheckedItems((prev) => {
      if (isChecked) {
        return prev.add(item);
      } else {
        prev.delete(item);
        return new Set(prev);
      }
    });
  };

  return (
    <div>
      <label>
        <input
          type="checkbox"
          name="item1"
          checked={checkedItems.has("item1")}
          onChange={handleChange}
        />
        Item 1
      </label>
      <br />
      <label>
        <input
          type="checkbox"
          name="item2"
          checked={checkedItems.has("item2")}
          onChange={handleChange}
        />
        Item 2
      </label>
      <br />
      <label>
        <input
          type="checkbox"
          name="item3"
          checked={checkedItems.has("item3")}
          onChange={handleChange}
        />
        Item 3
      </label>
    </div>
  );
}

Map

JavaScript에서 Map 객체는 키가 있는 데이터를 저장한다는 점에서 객체와 유사하나, 키에 다양한 자료형을 허용한다는 점, 순서를 기억한다는 점에서 다르다.

채팅에서 이름과 메세지를 id값으로 찾아 함께 보여줘야할 경우와 같이 특정 키값으로 값을 찾아 보여주는 경우 활용하기 좋다.

import { useState } from "react";

const messages = [
  {
    id: "message-1",
    text: "Hey!",
    userId: "user-1",
  },
  {
    id: "message-2",
    text: "Hi!",
    userId: "user-2",
  },
];

const users = [
  {
    id: "user-1",
    name: "Paul",
  },
  {
    id: "user-2",
    name: "Lisa",
  },
];

function Map() {
  const namesById = users.reduce(
    (prev, user) => ({ ...prev, [user.id]: user.name }),
    {}
  );

  return messages.map(({ id, text, userId }) => (
    <div key={id}>
      <div>{text}</div>
      <div>{namesById[userId]}</div>
    </div>
  ));
}

Stack

LIFO(Last in First out)의 특성을 가지는 추상 자료구조로 JavaScript에서 push, pop 메서드 등 활용하여 배열로 구현할 수 있다. 삽입, 삭제에 O(1), 탐색에 O(n) 소요되므로 탐색이 필요한 경우보다는 이전 작업 내용 저장하기 등의 작업에 적절하다. 함수 호출 스택, JavaScript의 history 객체 등이 stack으로 구현되어 있다.

이전 동작을 취소하는 등의 기능에 활용할 수 있지만 이외의 경우 배열을 자주 활용한다. 아래는 단순 stack 구현 코드이다.

import { useState } from "react";

function Stack() {
  const [stack, setStack] = useState([]);
  const [errorMessage, setErrorMessage] = useState("");

  const handlePush = () => {
    const valueToAdd = document.getElementById("stack-input").value;
    if (valueToAdd.trim() !== "") {
      setStack([...stack, valueToAdd]);
      setErrorMessage("");
    } else {
      setErrorMessage("Please enter a value.");
    }
  };

  const handlePop = () => {
    if (stack.length > 0) {
      const newStack = [...stack];
      newStack.pop();
      setStack(newStack);
      setErrorMessage("");
    } else {
      setErrorMessage("Stack is empty.");
    }
  };

  const handleClear = () => {
    setStack([]);
    setErrorMessage("");
  };

  return (
    <div>
      <h1>Stack Example</h1>
      <input type='text' id='stack-input' />
      <button onClick={handlePush}>Push to Stack</button>
      <button onClick={handlePop}>Pop from Stack</button>
      <button onClick={handleClear}>Clear Stack</button>
      {errorMessage ? <div className='error'>{errorMessage}</div> : <></>}
      <div>
        <ul>
          {stack.map((value, index) => (
            <li key={index}>{value}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}

Queue

FIFO(First in First out)의 특성을 가지는 추상 자료구조로 JavaScript에서 배열로도 구현 가능하나 시간복잡도를 고려할 때 연결리스트를 구현하여 사용하는 것이 적절하다. 해당 경우 삽입, 삭제에 O(1), 탐색에 O(n) 소요된다.

알림과 같이 시간순으로 추가되는 UI에 활용할 수 있다.

import { faker } from "@faker-js/faker";
import { useState } from "react";

function Queue() {
  const [notifications, setNotifications] = useState([]);

  const addNotification = () => {
    setNotifications((previous) => {
      return previous.concat(`${faker.name.firstName()} joined!`);
    });
    setTimeout(() => {
      setNotifications((previous) => previous.slice(1));
    }, 1000);
  };

  return (
    <>
      <button onClick={() => addNotification()}>
        Invite User To Community
      </button>

      <aside>
        {notifications.map((message, index) => (
          <p key={index}>{message}</p>
        ))}
      </aside>
    </>
  );
}

Tree

그래프의 일종으로, 하나의 루트 노드에서 시작해 다른 노드들로 연결된 사이클이 없는 비선형 계층적 자료구조이다. 여러 종류가 있는데 예를 들어 이진트리는 최대 자식 수가 2개인 트리, 이진탐색트리는 부모 기준 왼쪽 자식은 작고, 오른쪽 자식은 큰 트리, 포화이진트리는 자식요소가 0 or 2개인 경우, 완전이진트리는 자식 요소가 모두 2개인 경우이다. 이진트리의 삽입, 삭제, 탐색 시간복잡도는 트리의 높이에 비례하나, 일반적으로 O(logn)이라 계산할 수 있다.

2,3단 메뉴 등에 활용할 수 있다. 다만 재귀적으로 구현할 경우 오버플로우에 유의해야 한다.

import { useState } from "react";

function TreeNode({ node }) {
  const [collapsed, setCollapsed] = useState(true);

  const toggleCollapsed = () => {
    setCollapsed(!collapsed);
  };

  return (
    <div>
      <div onClick={toggleCollapsed}>{node.label}</div>
      {!collapsed && node.children && node.children.map((child) => (
        <TreeNode key={child.label} node={child} />
      ))}
    </div>
  );
}

function Tree() {
  const treeData = {
    label: "Root",
    children: [
      {
        label: "Node 1",
        children: [
          { label: "Leaf 1.1" },
          { label: "Leaf 1.2" },
        ],
      },
      {
        label: "Node 2",
        children: [
          { label: "Leaf 2.1" },
          { label: "Leaf 2.2" },
        ],
      },
    ],
  };

  return (
      <TreeNode node={treeData} />
  );
}

export default Tree;

Trie

문자열을 효율적으로 탐색하기 위한 트리 형태의 자료구조로 “retrieval tree”에서 나온 단어이다. “Apple”이라는 단어를 검색하는 경우, “A”를 먼저 찾고, 그 후 “p”, “p”, “l”, “e”를 순서대로 찾게 된다. 해당 방식을 통해 문자열을 하나하나 비교해서 탐색하기 보다 효율적으로 문자열을 검색할 수 있다.

프론트엔드에서는 자동완성 검색창 등의 구현에 활용할 수 있다.

import { useState } from "react";

class TrieNode {
  constructor() {
    this.children = {};
    this.isEndOfWord = false;
  }
}

class Trie {
  constructor() {
    this.root = new TrieNode();
  }

  insert(word) {
    let currentNode = this.root;
    for (let i = 0; i < word.length; i++) {
      const char = word[i];
      if (!currentNode.children[char]) {
        currentNode.children[char] = new TrieNode();
      }
      currentNode = currentNode.children[char];
    }
    currentNode.isEndOfWord = true;
  }

  search(word) {
    let currentNode = this.root;
    for (let i = 0; i < word.length; i++) {
      const char = word[i];
      if (!currentNode.children[char]) {
        return false;
      }
      currentNode = currentNode.children[char];
    }
    return currentNode.isEndOfWord;
  }

  startsWith(prefix) {
    let currentNode = this.root;
    for (let i = 0; i < prefix.length; i++) {
      const char = prefix[i];
      if (!currentNode.children[char]) {
        return false;
      }
      currentNode = currentNode.children[char];
    }
    return true;
  }

  getAllWordsWithPrefix(prefix) {
    const result = [];
    let currentNode = this.root;
    for (let i = 0; i < prefix.length; i++) {
      const char = prefix[i];
      if (!currentNode.children[char]) {
        return result;
      }
      currentNode = currentNode.children[char];
    }
    this._getAllWordsFromNode(currentNode, prefix, result);
    return result;
  }

  _getAllWordsFromNode(node, prefix, result) {
    if (node.isEndOfWord) {
      result.push(prefix);
    }
    for (const childChar in node.children) {
      this._getAllWordsFromNode(
        node.children[childChar],
        prefix + childChar,
        result
      );
    }
  }
}

function AutocompleteSearchBar(props) {
  const [inputValue, setInputValue] = useState("");
  const [suggestions, setSuggestions] = useState([]);

  const trie = new Trie();
  props.words.forEach((word) => trie.insert(word));

  const handleInputChange = (event) => {
    const value = event.target.value;
    setInputValue(value);
    if (value === "") {
      setSuggestions([]);
    } else {
      const suggestions = trie.getAllWordsWithPrefix(value.toLowerCase());
      setSuggestions(suggestions);
    }
  };

  const handleSuggestionClick = (suggestion) => {
    setInputValue(suggestion);
    setSuggestions([]);
  };

  return (
    <div>
      <input type='text' value={inputValue} onChange={handleInputChange} />
      {suggestions.length > 0 && (
        <ul>
          {suggestions.map((suggestion) => (
            <li
              key={suggestion}
              onClick={() => handleSuggestionClick(suggestion)}
            >
              {suggestion}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default AutocompleteSearchBar;

0개의 댓글