[TS 과제 챌린지] Menu Slider & Modal - 구현 (바닐라로 useState() 적용)

조민호·2023년 10월 27일
0
post-custom-banner

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


https://github.com/bradtraversy/vanillawebprojects

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


6주차 - Menu Slider & Modal

구현 코드

const toggleButton = document.getElementById('toggle') as HTMLButtonElement;
const closeButton = document.getElementById('close') as HTMLButtonElement;
const openButton = document.getElementById('open') as HTMLButtonElement;
const modal = document.getElementById('modal') as HTMLDivElement;
const navButton = document.getElementById('navbar') as HTMLDivElement;

type toggleState = {
  stateName: 'toggle';
  isToggle: boolean;
};
type modalState = {
  stateName: 'modal';
  isShow: boolean;
};
type stateType = toggleState | modalState;

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

  const state = () => initialState as T;

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

    newState.stateName === 'toggle' ? toggleRender() : modalRender();
  };

  return [state, setState];
};

const [navToggle, setNavToggle] = useState({
  isToggle: false,
  stateName: 'toggle',
} as toggleState); // isToggle: false가 아닌 isToggle: boolean로 하기 위한 타입 지정

const [showModal, setShowModal] = useState({
  isShow: false,
  stateName: 'modal',
} as modalState);

const toggleAction = (e: Event) => {
  let current = navToggle();

  // 토글이 되어 있는 상황일 때(=navbar가 열려 있는 상황일 때)
  if (current.isToggle) {

    // navbar 외부 클릭 시 (navbar를 닫아야 함)
    if (
      e.target !== toggleButton &&
      !toggleButton.contains(e.target as HTMLElement) &&
      e.target !== navButton &&
      !navButton.contains(e.target as HTMLElement)
    ) {
      setNavToggle({ isToggle: false, stateName: 'toggle' });
    }

    // 토글 버튼 클릭 시 (navbar를 닫아야 함)
    if (toggleButton.contains(e.target as HTMLElement)) {
      setNavToggle({ isToggle: false, stateName: 'toggle' });
    }

    return;
  }

  // 토글이 안 되어 있는 상황일 때(=navbar가 닫혀 있는 상황일 때)
  if (!current.isToggle) {
    if (toggleButton.contains(e.target as HTMLElement)) {
      setNavToggle({ isToggle: true, stateName: 'toggle' });
    }
    return;
  }
};

const modalAction = () => {
  let current = showModal();

  // if (current.isShow) {
  //   setShowModal({ isShow: false, stateName: 'modal' });
  //   return;
  // }
  // if (!current.isShow) {
  //   setShowModal({ isShow: true, stateName: 'modal' });
  //   return;
  // }
  
  setShowModal({ isShow: !(current.isShow), stateName: 'modal' });
};

// 리렌더링 로직들
// 상태가 true인 값으로 리렌더링이 호출 되면 navbar or modal을 엽니다
// 상태가 false인 값으로 리렌더링이 호출 되면 navbar or modal을 닫습니다
const toggleRender = () => {
  if (navToggle().isToggle) {
    document.body.classList.add('show-nav');
  }
  if (!navToggle().isToggle) {
    document.body.classList.remove('show-nav');
  }
};

const modalRender = () => {
  console.log(showModal());
  if (showModal().isShow) {
    modal.classList.add('show-modal');
  }
  if (!showModal().isShow) {
    modal.classList.remove('show-modal');
  }
};

// 이벤트 할당
openButton.addEventListener('click', modalAction);

closeButton.addEventListener('click', modalAction);

document.body.addEventListener('click', toggleAction);



이번 주 과제는 생각보다 난해했던 것 같습니다

아무래도 단순한 토글 기능이 아니라 왼쪽 navbar의 경우, css를 body에 추가해 놓았었기 때문에

body에도 클릭이벤트를 등록 해 놓아야 했고 , 이렇게 되면 화면 전체를 클릭할 때마다 이벤트가 발생하므로 클릭하는 위치에 따라 동작을 다르게 해야 했기 때문입니다

기존에 작성되어 있던 JS로직을 미리 봤었을 땐 코드는 짧지만 로직 자체가 복잡했었기 때문에 어떻게 다르게 작성을 해야 할지 고민을 많이 했었습니다

지난 주차의 과제를 구현하면서 제가 가장 중요하게 생각한 것은 단순한 DOM을 다루면서도

아래의 2가지의 추가 구현에 집중했었습니다

  • 상태값

  • 일방향적인 업데이트 로직 ( 오로지 상태 변경을 통해 화면이 리렌더링 )

사실 몇몇 과제는 굳이 위의 구현들을 추가해야 할 필요성이 없어 보이는 것도 있었습니다

그렇지만 이번주차는 위의 구현을 통해 코드 길이는 더 길어졌지만 ,

상대적으로 흐름에 따른 코드의 가독성을 보다 높일 수 있었던 같습니다


주요 기능 구현 사항은 아래와 같습니다

  • 등장
    • 토글 버튼 클릭 시 화면 왼편에 navbar 등장
  • 제거
    • navbar가 유지된 상태에서 , 토글 버튼 재 클릭 시 navbar 제거
    • navbar가 유지된 상태에서 , navbar 이외의 다른 부분 클릭시 navbar 제거
💡 위의 모든 과정 애니메이션 형태로 진행

modal창 기능

  • 등장
    • Sign Up 버튼 클릭시 화면 중앙에 모달 생성
  • 제거
    • 모달 상단 x 버튼 클릭시 모달 제거
    • form을 모두 작성하고 submit 클릭시, 모달 제거

코드 설명

이번 코드에 사용될 상태의 타입을 지정합니다

  • navbar가 토글이 됐는지 확인하는 상태값
  • modal 창이 열렸는지 확인하는 상태값

이 2개를 union type으로 사용해서 최종 상태값의 타입을 지정했고,

추후에 타입 기반으로 리렌더링을 진행 해야 하므로 union type을 구분하기 위한 Discriminated Union으로써 , stateName 키값을 사용합니다

type toggleState = {
  stateName: 'toggle';
  isToggle: boolean;
};
type modalState = {
  stateName: 'modal';
  isShow: boolean;
};
type stateType = toggleState | modalState;

상태관리 함수입니다

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

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

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

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

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

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

  const state = () => initialState as T;

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

		// Discriminated Union을 이용해서 상태 종류에 따라 서로 다른 리렌더링 로직 수행
    newState.stateName === 'toggle' ? toggleRender() : modalRender();
  };

  return [state, setState];
};
  • 초기 상태를 받아서 , [상태를 리턴하는 함수 , 상태를 업데이트 하는 함수 ] 를 리턴합니다 여기서 반환하는 상태와 상태 업데이트 함수는 클로저를 이용하여 외부에서도 상태를 참조 및 업데이트가 가능합니다
  • ts의 제네릭 제약조건을 활용하여 기존에 사용하던 상태 타입인 statesType만 상태로 받을 수 있으며 업데이트 역시 이 타입으로만 가능하게 하였습니다
  • 상태가 업데이트 되면 자동으로 리렌더링 함수( render() ) 가 발동합니다 이때 , Discriminated Union을 이용해서 상태의 타입 (토글인지 모달인지)에 따라 서로 다른 리렌더링 로직을 수행 하도록 했습니다

상태 선언

const [navToggle, setNavToggle] = useState({
  isToggle: false,
  stateName: 'toggle',
} as toggleState);

const [showModal, setShowModal] = useState({
  isShow: false,
  stateName: 'modal',
} as modalState);

여기서 중요한 것은 처음에 상태를 선언할 때

아래와 같이 초깃값을 주게 된다면

isToggle , isShow 의 타입이 boolean이 아니라 false 리터럴 값으로 고정이 되어 버립니다

{
  isToggle: false,
  stateName: 'toggle',
} 

그러므로 반드시 type assertion을 사용해야 합다

{
  isToggle: false, // boolean 타입
  stateName: 'toggle',
} as toggleState

토글 기능 함수

const toggleAction = (e: Event) : void => {
  const current: toggleState = navToggle();

  // 토글이 되어 있는 상황일 때(=navbar가 열려 있는 상황일 때)
  if (current.isToggle) {
    // navbar 외부 클릭 시 (navbar를 닫아야 함)
    if (
      e.target !== toggleButton &&
      !toggleButton.contains(e.target as HTMLElement) &&
      e.target !== navButton &&
      !navButton.contains(e.target as HTMLElement)
    ) {
      setNavToggle({ isToggle: false, stateName: 'toggle' });
    }

    // 토글 버튼 클릭 시 (navbar를 닫아야 함)
    if (toggleButton.contains(e.target as HTMLElement)) {
      setNavToggle({ isToggle: false, stateName: 'toggle' });
    }

    return;
  }

  // 토글이 안 되어 있는 상황일 때(=navbar가 닫혀 있는 상황일 때)
  if (!current.isToggle) {
    if (toggleButton.contains(e.target as HTMLElement)) {
      setNavToggle({ isToggle: true, stateName: 'toggle' });
    }
    return;
  }
};
  • 현재 토글 상태를 가져온 다음
    • 토글이 되어 있는 상황일 때(=navbar가 열려 있는 상황일 때) 이 때 ,만약 navbar의 외부를 클릭했거나 토글 버튼을 클릭했다면 navbar를 닫습니다
    • 토글이 안 되어 있는 상황일 때(=navbar가 닫혀 있는 상황일 때) 이 때는 navbar를 바로 열어줍니다

모달 기능 함수

const modalAction = () : void => {
  const current: modalState = showModal();
  
  setShowModal({ isShow: !(current.isShow), stateName: 'modal' });
};
  • 토글 버튼의 경우, navbar의 css를 body에 추가해 놓았었기 때문에 클릭할 때마다 이벤트가 발생하므로 클릭하는 위치에 따라 동작을 다르게 해야 했기 때문에 로직이 길었지만
  • 모달은 단순히 특정 버튼에 따라 열고 닫고를 반복하면 되므로 매우 간단합니다 기존 상태값의 반대로만 적용해주면 됩니다

리렌더링 함수

// 토글 리렌더링
const toggleRender = () : void=> {
  const current: toggleState =navToggle()
  if (current.isToggle) {
    document.body.classList.add('show-nav');
  }
  if (!current.isToggle) {
    document.body.classList.remove('show-nav');
  }
};

// 모달 리렌더링
const modalRender = () : void => {
  const current: modalState = showModal()
  if (current.isShow) {
    modal.classList.add('show-modal');
  }
  if (!current.isShow) {
    modal.classList.remove('show-modal');
  }
};

상태 업데이트가 발생하게 되면 , 해당 상태에 따른 리렌더링을 진행하는 로직입니다

  • 상태가 true인 값으로 리렌더링이 호출 되면 navbar or modal창을 엽니다
  • 상태가 false인 값으로 리렌더링이 호출 되면 navbar or modal칭을 닫습니다

이벤트 할당

openButton.addEventListener('click', modalAction);

closeButton.addEventListener('click', modalAction);

document.body.addEventListener('click', toggleAction);
  • 모달창은 openButton , closeButton 두 곳에만 달아줬으며
  • navbar의 토글 기능은 말했듯이 body 태그 전체에 달아줍니다
post-custom-banner

0개의 댓글