[TS 과제 챌린지] DOM Array Methods - 구현 (바닐라로 useState() 적용)

조민호·2023년 10월 27일
0

TS스터디 팀원들과 함께 매주 주차별 과제를 진행합니다


https://github.com/bradtraversy/vanillawebprojects

  • 위의 링크에는 바닐라JS로 간단히 구현된 단일 페이지의 예시들이 있습니다
  • HTML/CSS는 기존에 작성된 예시를 그대로 사용하지만, JS로직은 전부 삭제하고 스스로의 로직대로 완전히 새로 작성합니다
    - 이때 , 모든 기능 구현을 JS대신 TS로 직접 변환해서 작성합니다


5주차 - DOM Array Method

구현 코드

const main = document.getElementById('main') as HTMLDivElement;
const addUserBtn = document.getElementById('add-user') as HTMLButtonElement;
const doubleBtn = document.getElementById('double') as HTMLButtonElement;
const showMillionairesBtn = document.getElementById(
  'show-millionaires',
) as HTMLButtonElement;
const sortBtn = document.getElementById('sort') as HTMLButtonElement;
const calculateWealthBtn = document.getElementById(
  'calculate-wealth',
) as HTMLButtonElement;

let currentStateKey = 0; // useState가 실행 된 횟수

type setStateFunc<T> = (state: Promise<T>) => void;
const states: any = []; // state를 보관할 배열

type userType = {
  name: string;
  money: number;
};
type statesType = userType[];

const useState = <T>(initState: T | Promise<T>): [T, setStateFunc<T>] => {
  // initState로 초기값 설정
  const key: number = currentStateKey;
  if (states.length === key) {
    states.push(initState);
  }

  // state 할당
  const state = states[key] as T;

  const setState = (newState: Promise<T>) => {
    // 값이 똑같은 경우
    if (newState === state) return;
    if (JSON.stringify(newState) === JSON.stringify(state)) return;

    // 기존 값과 다른 경우에만 값을 변경하고 render()를 실행한다.
    states[key] = newState;
    console.log('render 호출');
    render();
  };
  currentStateKey += 1;
  return [state, setState];
};

async function getRandomUser(num: number) {
  const arr = [];
  for (let i = 0; i < num; i++) {
    const result = (await (await fetch('https://randomuser.me/api')).json())
      .results[0];

    const newUser = {
      name: `${result.name.first} ${result.name.last}`,
      money: Math.floor(Math.random() * 1000000),
    };

    arr.push(newUser);
  }

  return arr;
}

const Component = async () => {
  const [userInfo, setUserInfo] = useState(getRandomUser(3));

  console.log(userInfo);

  return userInfo;
};

const render = async () => {
  const data = await Component();
  main.innerHTML = '<h2><strong>Person</strong> Wealth</h2>';
  console.log(data);
  data.forEach((item) => {
    const element = document.createElement('div');
    element.classList.add('person');
    element.innerHTML = `<strong>${item.name}</strong> ${item.money}`;
    main.appendChild(element);
  });

  currentStateKey = 0;
};
render();

이번 과제의 주요 기술 구현 목표는 아래와 같습니다

  • 상태가 중요하게 사용된 과제인 만큼 , JS의 클로저 개념을 활용하여 기존 상태를 유지하며 이 상태가 업데이트 될 시 , 업데이트 된 상태를 바탕으로 해당 HTML 부분만 자동 리렌더링을 하도록 구현
    >> 리액트의 useState
  • TS의 제네릭 제약조건을 활용하여 무분별한 상태값이 사용되는 것을 막고 , 기존에 사용하던 상태의 타입으로만 사용 및 업데이트를 가능하도록 구현

상태값으로 사용할 타입 입니다

type userInfoObj = {
  name: string;
  money: number;
};
type statesType = userInfoObj[];

const states: statesType = [];
  • 여기서 사용할 상태는 배열 객체 형태입니다 ( [ { },{ },{ } ] ) 각 객체는 유저의 정보를 담고 있고 이것들을 배열 형태로 여러개 사용하는 것입니다
  • 그러므로 각 객체의 타입을 지정해주고 이걸 배열로 사용하는 최종 상태로 지정합니다


이 로직의 핵심인 , 클로저를 이용한 상태관리 함수입니다

리액트의 useState()를 구현해보는 과정에서 사용한 로직입니다

상태를 변수가 아닌 함수로 사용하고 있으며, 그 외에도 리액트와

완벽히 똑같이 작동하지는 않지만 아래의 기능들을 구현했습니다

  • 클로저를 통해 외부에서 특정 함수를 통해서만 상태값에 접근이 가능하므로 외부에서 상태가 의도치 않게 변경되는 것을 방지하는 캡슐화 기능
  • 각 상태들들 독립적으로 모듈화 해서 사용 가능
  • 외부에서 상태 값을 참조하려면 오로지
  • 오직 상태가 변할 때만 , 화면 다시 리렌더링 하는 구조

    자세한 설명은 바닐라로 useState 구현해보기

const useState = <T extends statesType>(status: T): [() => T, (state: T) => void] => {
  let initialState = status;

  const state = () => initialState as T;

  const setState = (newState: T) => {
    initialState = newState;
    render();
  };

  return [state, setState];
};

// 상태를 리턴하는 함수 , 상태를 업데이트 하는 함수  
const [getUserInfo, setUserInfo] = useState(states);
  • 초기 상태를 받아서 , [상태를 리턴하는 함수 , 상태를 업데이트 하는 함수 ] 를 리턴합니다 여기서 반환하는 상태와 상태 업데이트 함수는 클로저를 이용하여 외부에서도 상태를 참조 및 업데이트가 가능합니다
  • ts의 제네릭 제약조건을 활용하여 기존에 사용하던 상태 타입인 statesType만 상태로 받을 수 있으며 업데이트 역시 이 타입으로만 가능하게 하였습니다
  • 상태가 업데이트 되면 자동으로 리렌더링 함수( render() ) 가 발동합니다


리렌더링 함수 입니다

const render = (): void => {
  main.innerHTML = `<h2><strong>Person</strong> Wealth</h2>`;

  for (let i of getUserInfo()) {
    let state = i;

    let htmlString : string = '';
    const newElement = document.createElement('div') as HTMLDivElement;
    newElement.classList.add('person');
    htmlString += `<strong>${state.name}</strong> ${state.money}`;
    newElement.innerHTML = htmlString;
    main.appendChild(newElement);
  }
};
  • 기존 상태를 바탕으로 DOM 부분을 다시 그립니다
💡 virtual DOM처럼 변화가 생긴 컴포넌트만 자동으로 리렌더링 되는 구조가 아닌, 변화가 예측되는 DOM부분을 직접 업데이트 하는 것입니다

유저 목록을 가져오는 함수입니다

const getUser = async (num: number): Promise<userInfoObj[]> => {
  let dataArr = [];

  for (let i = 0; i < num; i++) {
    const result = (await (await fetch('https://randomuser.me/api')).json())
      .results[0];

    const newUser = {
      name: `${result.name.first} ${result.name.last}`,
      money: Math.floor(Math.random() * 1000000),
    };

    dataArr.push(newUser);
  }

  return dataArr;
};
  • async/await을 사용하여 api 비동기 처리를 했으며
  • 여러 유저를 한번에 받아서 배열 객체 형태로 리턴합니다 💡 async 가 붙은 함수는 무조건 프로미스를 반환하므로 리턴 타입을 Promise로 감싸줍니다


최초 실행시 , 초기 세팅을 진행하는 함수

const init = async () : Promise<void> => {
  const userData = await getUser(3);
  setUserInfo(userData);
};
init();
  • 최초에 3명의 유저만 먼저 받아와서 화면서 보여주도록 합니다절
  • getUser(3)를 사용한뒤 이 값을 바탕으로 setUserInfo()로 상태 업데이트를 해주고 업데이트 된 상태를 바탕으로 리렌더링을 진행하는 것입니다 💡 이 함수 역시 비동기 처리가 진행되는 getUser()함수를 사용하므로 async/await을 사용해줘야 하며 , 프로미스를 반환하므로 리턴 타입을 Promise로 감싸줍니다


profile
웰시코기발바닥

0개의 댓글