와장창 프로젝트 경험기 #5

sham·2021년 9월 23일
0

Bobs 개발일지

목록 보기
5/6
post-thumbnail

현재 상황

마이 페이지 작업을 끝맞쳤지만 솔-찍히, 강의에서 배운 내용과 크게 다르지 않는 작업이어서 이걸 끝맞쳐도 내가 성장했다고 말할 수 있을까 하며 불안한 상황이었다. 운 좋게도 메인 페이지의 몇몇 컴포넌트 작업을 맡게 되었다. 굴곡이 많은 만큼 한층 더 성장한 기분이 든다. 기분만 든다.

구현한 것

  • UI 구현
  • 특정 컴포넌트에 마우스를 올리면 상호작용
  • 모달창에서 장소, 시간, 메뉴 정하는 기능 구현
  • 마우스 휠로 컴포넌트(state) 전환 기능 구현
  • 양방향 연결 배열(?) 형태 구현

남은 과제

  • Material UI 에서 아이콘 집어넣기
  • 서버와의 통신(서버가 돌아가야 가능)

구현

MakeBookApp

사실상 이 작업이 거의 메인이었다. 앱, 웹 환경에서 모달창을 띄워서 예약을 구현해야 했다. 모바일 환경에서의 상호작용을 위해 구현해야 할 게 많았다.

쓰기만 할 때는 몰랐지...

구현해야 할 것 중 중요한 요소.

  • 버튼을 누름으로써 생성된 모달창 디자인
  • 스크롤 해서 동적으로 시간을 설정할 수 있게 구현
  • 스크롤 해서 동적으로 메뉴를 구현할 수 있게 구현 - 선택된 메뉴는 배열에서 빠져야 한다.

처음에 든 생각은 시간을 정하는 div 태그, 메뉴를 정하는 div 태그를 만들고 상호작용하는 요소들을 무한으로 스크롤할 수 있게 만들자고 생각했다. 그래서 무한 스크롤에 대해서 찾아보았다.


그 무한 스크롤 말고!!!

무한 스크롤을 검색하면 나오는 것들은 백이면 백 서버에서 불러온 정보들을 끝없이 보여주는 것들 뿐이다. 내가 구현해야 할 컴포넌트는 정확히 다음과 같다.

  • 세 개의 스크롤 바 컴포넌트를 자식으로 가지는 고정된 컴포넌트.
  • 각 스크롤 바 컴포넌트는 현재 값, 이전 값, 다음 값 만을 보여준다.
  • 세 개 이외의 값은 숨겨져 있는 상태이며, 맨 처음과 맨 끝의 값이 연결되어 있다.

hook 에서 eventListener 등록하기

eventListener는 document 객체에 등록을 하는 메서드, 그런데 우리는 컴포넌트에 등록을 해야 하기 때문에 논외이다. 컴포넌트의 onScroll 속성에 등록시키는 것으로 해결.


onScroll 이벤트

우리가 원하는 것은 스크롤이 가능하고, 양방향으로 연결되어 있는 컴포넌트를 만드는 것이다.
우선 스크롤을 가능하게 만들어 보자.

 .select-time {
      height: 100px;
      width: 200px;
      border-top: 0.6px solid $mainColor;
      border-bottom: 0.6px solid $mainColor;
      margin-bottom: 20px;
      display: flex;
       div {
          flex: 1;
          text-align: center;
          overflow-y: scroll;
        }
      }

overflow-y: scroll; css 속성을 넣으면 세로 스크롤바가 생성된다.

스크롤은 가능해졌지만 위의 디자인과는 너무 다르다. 옆의 저 거추장스러운 것을 때버리자.

  .select-time {
      height: 100px;
      width: 200px;
      border-top: 0.6px solid $mainColor;
      border-bottom: 0.6px solid $mainColor;
      margin-bottom: 20px;
      display: flex;
      div {
        flex: 1;
        text-align: center;
        overflow-y: scroll;
        -ms-overflow-style: none; /* IE and Edge */
      }
      div::-webkit-scrollbar {
        display: none; /* Chrome, Safari, Opera*/
      }
    }

브라우저마다 다른 처리를 요구한다. 익스플로어를 위해 -ms-overflow-style: none;를, 크롬, 사파리를 위해 display: none;를 적용시켜준다.

https://webisfree.com/2019-01-08/css-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%8A%A4%ED%81%AC%EB%A1%A4%EB%B0%94-%EC%8A%A4%ED%83%80%EC%9D%BC-%EC%A7%80%EC%A0%95-%EB%B0%94%EA%BE%B8%EB%8A%94-%EB%B0%A9%EB%B2%95-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0

webkit은 css 접두어이다. 브라우저 간 호환성을 위해 사용하는 키워드인데, css 접두어에 대한 자세한 설명은 다음 링크에.


본격 구현

얼추 모양이 나왔다. 이제 디자인처럼 구현하는 것 목표로 해보자.


  const handleScroll = e => {
    // const [scrollHeight, scrollTop, clientHeight] = document.documentElement;
    // console.log(scrollHeight, scrollTop, clientHeight);
    console.log('adadas');
    console.log(e.target);
  };

  const makeScroll = (array, arrayName) => {
    console.log(array);
    return (
      <div className={arrayName} onScroll={handleScroll}>
        {array.current.map(e => (
          <div>{e}</div>
        ))}
      </div>
    );
  };

  return (
    <>
      {open && (
        <div className="modal">
          <div className="section">
            <input
              type="text"
              className="input-room"
              onChange={setSubmit}
              value={title}
              placeholder="방 제목"
            />
            <div className="select-time">
              {makeScroll(direction, 'direction')}
              {makeScroll(hour, 'hour')}
              {makeScroll(minute, 'minute')}
            </div>
            <div className="select-menu">fqwfwq</div>
            <footer>
              <button type="button" className="finish" onClick={closeFunction}>
                만들기
              </button>
            </footer>
          </div>
        </div>
      )}
    </>
  );
};

makeScroll 함수는 인자로 배열과 배열이름을 받아서 스크롤바를 자식으로 가지는 div 태그를 리턴한다. 시간과 분을 담당하는 스크롤바는 1부터 24, 0부터 59까지 만들어 놓은 배열을 map으로 해서 내용을 만든다. 다음과 같이 3개를 일렬로 두면 위치, 달, 일에 해당하는 스크롤바가 생성되는 것이다. 그런데 이런 생각이 들었다. 굳이 저 배열을 전부 다 만들 필요가 있나? 실질적으로 보여지게 되는 것은 3개 아닌가? 굳이 모든 소스를 만들어놓지 않아도 스크롤했다고 판단되면 값을 갱신하면 되지 않을까?


const MakeBook = ({ open, close }) => {
  const [title, setTitle] = useState('');
  const direction = useRef(['개포', '서초']);
  const time = useRef(new Date());
  const curHour = useRef(time.current.getHours() + 1);
  const curMinute = useRef(time.current.getMinutes());
  const [hour, setHour] = useState(curHour.current);
  const [minute, setMinute] = useState(curMinute.current);

  const setSubmit = e => {
    setTitle(e.target.value);
  };

  const closeFunction = () => {
    setTitle('');
    close();
  };

  const handleScroll = e => {
    // const [scrollHeight, scrollTop, clientHeight] = document.documentElement;
    // console.log(scrollHeight, scrollTop, clientHeight);
    const name = e.target.className;
    if (name === 'curHour') {
      setHour(hour + 1);
    } else if (name === 'curMinute') {
      setMinute(minute + 1);
    }
  };

  const makeDirectionScroll = () => {
    return (
      <div className="direction" onScroll={handleScroll}>
        {direction.current.map(e => (
          <div>{e}</div>
        ))}
      </div>
    );
  };
  const makeHourScroll = () => {
    return (
      <div className="curHour" onScroll={handleScroll}>
        <div>{hour - 1}</div>
        <div>{hour}</div>
        <div>{hour + 1}</div>
      </div>
    );
  };
  const makeMinuteScroll = () => {
    return (
      <div className="curMinute" onScroll={handleScroll}>
        <div>{minute - 1}</div>
        <div>{minute}</div>
        <div>{minute + 1}</div>
      </div>
    );
  };

  return (
    <>
      {open && (
        <div className="modal">
          <div className="section">
            <input
              type="text"
              className="input-room"
              onChange={setSubmit}
              value={title}
              placeholder="방 제목"
            />
            <div className="select-time">
              {makeDirectionScroll()}
              {makeHourScroll()}
              {makeMinuteScroll()}
            </div>
            <div className="select-menu">fqwfwq</div>
            <footer>
              <button type="button" className="finish" onClick={closeFunction}>
                만들기
              </button>
            </footer>
          </div>
        </div>
      )}
    </>
  );
};

코드를 대거 수정했다. 사용자에게 보여줄 시간과 분을 담당할 state와 지금 현재의 시간과 분을 담당할 변수 역할의 Ref를 생성했다. 총 4개의 변수를 가지고 무한 스크롤을 흉내내 보자.

시간, 분을 담당하는 컴포넌트는 이전과 달리 3개의 값(이전 값, 현재 값, 다음 값)만을 보여준다. 여기서 디폴트로 보여줄 값은 현재의 시간(curHour, curMinute)이다. Date 객체를 만들어서 getHours() getMinutes() 메소드로 구해준 값이 Ref에 저장되어 있다. state로 초기값을 지정해줄 때 Ref에 저장된 값을 할당시키고, state - 1, state, state + 1로 보여줄 값을 설정한다.

만약 스크롤이 감지되면 setState를 통해 스크롤이 작동된 해당 컴포넌트의 state를 변경시킬 것이다. state가 변경되었기에 컴포넌트는 즉시 리렌더링되고, 3개의 값도 갱신이 이루어진다. 이제 남은 것은 위로 스크롤했냐, 아래로 스크롤했냐의 여부에 따라 값을 올리느냐 내리느냐의 처리다.


스크롤이 아니라 휠이라구요

용어를 착각해서 뭔가 잘못 구현을 했다. onScroll로 이벤트를 주었는데 나는 스크롤이 아니라 마우스 휠을 굴렸을 때 상호작용이 되게 하고 싶은 것이었다. 찾아보니 onWheel이라는 이벤트가 존재했다.


  const handleWheel = e => {
    const value = e.deltaY / 60;
    if (value > 1) {
      console.log(value);
      console.log('아래로');
    } else if (value < -1) {
      console.log(value);
      console.log('위로');
    }
  };

deltaY라는 속성이 스크롤에 대응하는 속성이라고 한다. console.log로 이벤트를 출력해보니 아래로 스크롤 할 때는 양수값이, 위로 스크롤 할 때는 음수값이 나오고 절대값은 한 번 스크롤할 때의 강도에 비례하였다.

일정 강도 이상으로 스크롤 했을 때 스크롤이 되게 하고 싶다면 다음과 같이 특정 값 이상으로 됬을 때 인식하게 하면 된다.

진짜로 스크롤이 되야 하나?

기존의 모든 소스를 스크롤할 수 있게 보이지 않게 깔아놓는 것을 3개만 보이게 깔아놓고 스크롤하자고 생각을 했다. 그런데 휠 이벤트를 적용한 결과, 휠을 하더라도 실제로 스크롤이 되지 않는 경우가 빈번했고, 스크롤이 되지 않는 고정된 화면이더라도 휠 이벤트는 작동하는 것을 확인했다. 그렇다면 고정된 화면에서 스크롤을 할 때 값을 교체하는 방법이 더 정확하고 깔끔하지 않을까?

import React, { useState, useRef } from 'react';
import './MakeBook.scss';

const MakeBook = ({ open, close }) => {
  const [title, setTitle] = useState('');
  const direction = useRef(['개포', '서초']);
  const time = useRef(new Date());
  const curHour = useRef(time.current.getHours() + 1);
  const curMinute = useRef(time.current.getMinutes());
  const [hour, setHour] = useState(curHour.current);
  const [minute, setMinute] = useState(curMinute.current);

  const setSubmit = e => {
    setTitle(e.target.value);
  };

  const closeFunction = () => {
    setTitle('');
    close();
  };

  const handleDirectionWheel = e => {
    const value = e.deltaY;
    if (value > 0) {
      console.log(value);
      console.log('아래로');
    } else if (value < 0) {
      console.log(value);
      console.log('위로');
    }
  };

  const handleHourWheel = e => {
    const value = e.deltaY;
    if (value < 0) {
      setHour(hour === 0 ? 23 : hour - 1);
    } else if (value > 0) {
      setHour(hour === 23 ? 0 : hour + 1);
    }
  };
  const handleMinuteWheel = e => {
    const value = e.deltaY;
    if (value < 0) {
      if (minute === 0) {
        setMinute(59);
        setHour(hour === 0 ? 23 : hour - 1);
      } else {
        setMinute(minute - 1);
      }
    } else if (value > 0) {
      if (minute === 59) {
        setMinute(0);
        setHour(hour === 23 ? 0 : hour + 1);
      } else {
        setMinute(minute + 1);
      }
    }
  };

  const makeDirectionWheel = () => {
    return (
      <div className="direction" onWheel={handleDirectionWheel}>
        {direction.current.map(e => (
          <div>{e}</div>
        ))}
      </div>
    );
  };
  const makeHourWheel = () => {
    const prev = hour === 0 ? 23 : hour - 1;
    const next = hour === 23 ? 0 : hour + 1;
    console.log(prev, hour, next);
    return (
      <div className="curHour" onWheel={handleHourWheel}>
        <div>{prev}</div>
        <div>{hour}</div>
        <div>{next}</div>
      </div>
    );
  };
  const makeMinuteWheel = () => {
    const prev = minute === 0 ? 59 : minute - 1;
    const next = minute === 59 ? 0 : minute + 1;
    return (
      <div className="curMinute" onWheel={handleMinuteWheel}>
        <div>{prev}</div>
        <div>{minute}</div>
        <div>{next}</div>
      </div>
    );
  };

  return (
    <>
      {open && (
        <div className="modal">
          <div className="section">
            <input
              type="text"
              className="input-room"
              onChange={setSubmit}
              value={title}
              placeholder="방 제목"
            />
            <div className="select-time">
              {makeDirectionWheel()}
              {makeHourWheel()}
              {makeMinuteWheel()}
            </div>
            <div className="select-menu">fqwfwq</div>
            <footer>
              <button type="button" className="finish" onClick={closeFunction}>
                만들기
              </button>
            </footer>
          </div>
        </div>
      )}
    </>
  );
};

export default MakeBook;

마우스휠을 할 때마다 화면이 다시 렌더링 되야 하므로 state로 값을 관리할 것이다. hour는 시, minute는 분을 관리한다. onWheel 이벤트 발동 시 해당하는 set 함수가 실행된다. deltaY값으로 위로 휠했는지 아래로 휠했는지를 구분해서 위로 했으면 -1을, 아래로 했으면 +1을 해준다. state가 변경되었으므로 렌더링이 다시 되면서 make{}Wheel 함수도 다시 실행된다. prev와 next의 값도 변경된 state에 맞게 조정된다.

이런 UI를 보면 최소값, 최대값에 도달했을 때 더 휠을 하면 자동으로 최소값은 최대값, 최대값은 최소값으로 넘어가게 구현이 되어있는데, 의외로 간단히 구현할 수 있었다. hour의 경우, 23에서 1을 더할 때, 0(24)에서 1을 뺄 때 예외처리를 해주어야 한다. (24시 대신 00시를 사용할 것이다.) 삼항연산자로 23이라면 0, 0이라면 23으로 설정하게 예외처리를 해준다. 이 때 make{}Wheel 함수의 prev, next도 동일한 처리를 해주어야 한다. minute도 크게 다르지 않지만, 0분을 지나갈 때 시간도 지나가게 처리를 해주고 싶어서 코드를 약간 수정해주었다.

투명하게 만들거나 애니메이션 작업은 CSS 작업으로 해결될 것 같은 느낌적인 느낌.


장소 선택 구현

장소 선택하는 것을 그냥 넘어갈 뻔 헀다. 사실 이걸 구현이라고 해야 할지도 애매하고 꼼수로 해결해서 좀 찜찜하긴 하지만 일단은 기록한다.

  const [direction, setDirection] = useState('개포');

  const handleDirectionWheel = e => {
    const value = e.deltaY;
    if (value > 0 && direction === '개포') {
      setDirection('서초');
    } else if (value < 0 && direction === '서초') {
      setDirection('개포');
    }
  };


  const makeDirectionWheel = () => {
    return (
      <div className="direction" onWheel={handleDirectionWheel}>
        {direction === '개포' && <div className="dummy">{}</div>}
        <div>개포</div>
        <div>서초</div>
        {direction === '서초' && <div className="dummy">{}</div>}
      </div>
    );
  };

return (
     <div className="select-place">
              {makeDirectionWheel()}
              {makeHourWheel()}
              {makeMinuteWheel()}
              <div className="split">:</div>
            </div>
  );

state로 장소에 대한 정보를 기록한다. 기본값은 개포로 한다. 신경써줘야 할 점은 디자인적인 부분, 장소 div가 시간 div과 수평이 되게 배치되어야 한다. 그래서 간격을 맞춰주기 위해 dummy용 div 태그를 집어넣었다. 개포일 때는 위쪽 div가, 서초일 때는 아래쪽 div가 생겨서 간격을 맞춰주게 된다.


메뉴 선택 구현

메뉴 선택도 뼈대적인 부분은 시간 선택처럼 휠로 구현할 수 있을 것 같다. 문제는 메뉴를 선택했을 때 넣고 빼는 작업.

  const handleMenuWheel = e => {
    const value = e.deltaY;
    if (value < 0) {
      setMenuIndex(menuIndex === 0 ? 11 : menuIndex - 1);
    } else if (value > 0) {
      setMenuIndex(menuIndex === 11 ? 0 : menuIndex + 1);
    }
  };

  const makeMenu = () => {
    const curMenu = menu.current;
    console.log(curMenu);
    const prev = menuIndex === 0 ? 11 : menuIndex - 1;
    const next = menuIndex === 11 ? 0 : menuIndex + 1;
    return (
      <div className="curMenu" onWheel={handleMenuWheel}>
        <div>{curMenu[prev]}</div>
        <div>{curMenu[menuIndex]}</div>
        <div>{curMenu[next]}</div>
      </div>
    );
  };
return (
              <div className="select-menu">{makeMenu()}</div>
);
  

엄밀히 보면 좌우로 슬라이드이니 다르게 작업해야 할 것 같은데... 일단은 임시로라도 구현해야 하니 같은 방식을 택했다. 메뉴 배열은 Ref에 할당해서 index를 state로 써서 관리한다. prev, next나 최대값 최소값도 시간 선택과 같은 방식으로 처리해줬다.


메뉴 선택하기

가운데 메뉴를 클릭하면 해당 메뉴가 제거되고 밑의 선택된 메뉴창에 나타나고 배열에서 제거해야 한다.
선택된 메뉴를 다시 누르면 취소되야 한다.

먼저 제거되는 것부터 구현하기로 했다.

import React, { useState, useRef } from 'react';
import './MakeBook.scss';

const MakeBook = ({ open, close }) => {
  const [title, setTitle] = useState('');
  const time = useRef(new Date());
  const curHour = useRef(time.current.getHours() + 1);
  const curMinute = useRef(time.current.getMinutes());
  const [direction, setDirection] = useState('개포');
  const [hour, setHour] = useState(curHour.current);
  const [minute, setMinute] = useState(curMinute.current);
  const menu = useRef([
    '아무거나',
    '한식',
    '중식',
    '일식',
    '양식',
    '커피',
    '편의점',
    '빵',
    '분식',
    '배달음식',
    '술',
    '도시락',
  ]);
  const [menuIndex, setMenuIndex] = useState(1);
  const [selectedMenu, setSelectedMenu] = useState([]);

  const setSubmit = e => {
    setTitle(e.target.value);
  };

  const closeFunction = () => {
    setTitle('');
    close();
  };

  const handleDirectionWheel = e => {
    const value = e.deltaY;
    if (value > 0 && direction === '개포') {
      setDirection('서초');
    } else if (value < 0 && direction === '서초') {
      setDirection('개포');
    }
  };

  const handleHourWheel = e => {
    const value = e.deltaY;
    if (value < 0) {
      setHour(hour === 0 ? 23 : hour - 1);
    } else if (value > 0) {
      setHour(hour === 23 ? 0 : hour + 1);
    }
  };

  const handleMinuteWheel = e => {
    const value = e.deltaY;
    if (value < 0) {
      if (minute === 0) {
        setMinute(59);
        setHour(hour === 0 ? 23 : hour - 1);
      } else {
        setMinute(minute - 1);
      }
    } else if (value > 0) {
      if (minute === 59) {
        setMinute(0);
        setHour(hour === 23 ? 0 : hour + 1);
      } else {
        setMinute(minute + 1);
      }
    }
  };

  const handleMenuWheel = e => {
    const max = menu.current.length - 1;
    const min = 0;
    const value = e.deltaY;
    if (value < 0) {
      setMenuIndex(menuIndex === min ? max : menuIndex - 1);
    } else if (value > 0) {
      setMenuIndex(menuIndex === max ? min : menuIndex + 1);
    }

    console.log(`index : ${menuIndex}`);
    console.log(menu);
  };

  const makeDirectionWheel = () => {
    return (
      <div className="direction" onWheel={handleDirectionWheel}>
        {direction === '개포' && <div className="dummy">{}</div>}
        <div>개포</div>
        <div>서초</div>
        {direction === '서초' && <div className="dummy">{}</div>}
      </div>
    );
  };

  const makeHourWheel = () => {
    const prev = hour === 0 ? 23 : hour - 1;
    const next = hour === 23 ? 0 : hour + 1;
    return (
      <div className="curHour" onWheel={handleHourWheel}>
        <div>{prev}</div>
        <div>{hour}</div>
        <div>{next}</div>
      </div>
    );
  };
  const makeMinuteWheel = () => {
    const prev = minute === 0 ? 59 : minute - 1;
    const next = minute === 59 ? 0 : minute + 1;
    return (
      <div className="curMinute" onWheel={handleMinuteWheel}>
        <div>{prev}</div>
        <div>{minute}</div>
        <div>{next}</div>
      </div>
    );
  };

  const selectMenu = e => {
    if (selectedMenu.length < 5) {
      const select = e.target.innerText;
      const last = menu.current[menu.current.length - 1];
      setSelectedMenu(prev => [...prev, select]);
      menu.current = menu.current.filter(c => c !== select);
      if (last === select) {
        console.log('마지막꺼!');
        setMenuIndex(0);
      }
    }
  };

  const makeMenu = () => {
    const curMenu = menu.current;
    const max = menu.current.length - 1;
    const min = 0;
    const prev = menuIndex === min ? max : menuIndex - 1;
    const next = menuIndex === max ? min : menuIndex + 1;
    return (
      <div className="curMenu" onWheel={handleMenuWheel}>
        <div>{curMenu[prev]}</div>
        <div role="button" onClick={selectMenu} onKeyDown="" tabIndex={0}>
          {curMenu[menuIndex]}
        </div>
        <div>{curMenu[next]}</div>
      </div>
    );
  };

  return (
    <>
      {open && (
        <div className="modal">
          <div className="section">
            <input
              type="text"
              className="input-room"
              onChange={setSubmit}
              value={title}
              placeholder="방 제목"
            />
            <div className="select-place">
              {makeDirectionWheel()}
              {makeHourWheel()}
              {makeMinuteWheel()}
              <div className="split">:</div>
            </div>
            <div className="select-menu">{makeMenu()}</div>
            <div className="selected-menu">
              <div className="curSelectMenu">
                {selectedMenu.map(e => {
                  return <div>{e}</div>;
                })}
              </div>
            </div>
            <footer>
              <button type="button" className="finish" onClick={closeFunction}>
                만들기
              </button>
            </footer>
          </div>
        </div>
      )}
    </>
  );
};

export default MakeBook;

하다보니 작업이 자꾸 불어나서 중간에 끊을 타이밍을 놓쳤다...ㅠ 선택된 메뉴들은 state인 selectedMenu에 배열로 관리해줄 것이다. makeMenu 함수로 생성되는 div 태그 중 가운데 태그를 클릭 가능하게 만들어주고 onClick 이벤트로 selectMenu 를 걸어준다. selectMenu가 실행되면 기존 selectedMenu 배열에 e 인자에서 innerText를 추출한 메뉴를 추가한다. 그와 동시에 전체 메뉴를 관리하는 Ref인 menu 배열에서 해당 메뉴를 제거해준다.

이 때 기존의 wheel 이벤트에도 변화가 생기게 된다. 시간인 hour, minute과는 달리 이제 menu 배열은 동적으로 추가되고 삭제되기를 반복할 것이기 때문이다. 배열의 마지막 인덱스에 도달하면 자동으로 0번째 인덱스로 보내버리는 로직이었는데, 마지막 인덱스가 동적으로 변화할 수 있게 되었으므로 배열의 최대길이 max 라는 변수를 생성해주었다. max의 값은 menu.length - 1이다.

애초에 하드코딩으로 인덱스의 최대범위를 때려넣은 것이 오산이었다. 머리가 나쁘면 몸이 고생한다.

menu에서 selectedMenu로 추출할 때 신경써주어야 할 것이 하나 더 있는데, 만약 menu 배열의 마지막 요소를 추출하게 된다면 menuIndex도 즉시 0으로 할당해주어야 한다. 마지막 요소를 추출했다는 말은 즉슨 menuIndex 역시 배열의 마지막 인덱스를 가리키고 있다는 뜻이고, state를 변경했으니 즉시 리덴데링되는 것에 대응해야 하므로.


선택한 메뉴 제거하기

이제 진짜 얼마 안 남았다... 선택된 메뉴들을 클릭하면 제거되게 구현해야 한다. 메뉴를 선택할 때처럼 선택된 div 태그를 클릭할 수 있게 하고 onClick 이벤트로 보내서 처리해버리자.

const selectMenu = e => {
    if (selectedMenu.length < 5) {
      const select = e.target.innerText;
      const last = menu.current[menu.current.length - 1];
      setSelectedMenu(prev => [...prev, select]);
      menu.current = menu.current.filter(c => c !== select);
      if (last === select) {
        console.log('마지막꺼!');
        setMenuIndex(0);
      }
    }
  };

  const deleteSelectMenu = e => {
    const select = e.target.innerText;
    console.log(select);
    setSelectedMenu(prev => prev.filter(c => c !== select));
    menu.current = [...menu.current, select];
  };

  <div className="curSelectMenu">
                {selectedMenu.map(e => {
                  return (
                    <div
                      role="button"
                      onClick={deleteSelectMenu}
                      onKeyDown=""
                      tabIndex={0}
                    >
                      {e}
                    </div>
                  );
                })}
              </div>

순서대로 돌아가지 않는다는 아주 사소한 옥의 티를 제외하고는 다 잘 돌아간다!!! 만세!!!
selectMenu 함수와 반대로만 작동되게 해주면 된다!!!


자잘한 디자인

정말 거의 다왔다. 이제 남은 것은 디자인 뿐이다.

부득이하게 아이콘을 가져다 쓸 Material UI에서 문제가 생겨 그 부분은 빼놓고 진행하였다. css 애니메이션을 넣는 게 원래 의도와 더 맞지 않나하는 생각도 했지만 일단 컨펌을 받고 난 후 진행해도 늦지 않을 것이라 판단했다.


최초 디자인처럼 선택되지 않은 부분의 내용(text)에 투명도를 적용시켰다. linear-gradient를 사용하면 그라데이션을 줄 수 있다고 하는데, 여유가 되면 적용시켜 볼 생각이다.

길고도 길었다...


이슈

깃 여러 브랜치 관리하기

메인 페이지의 작업을 맡게 되어 기존의 feature/mypage 브랜치가 아닌 feature/main 브랜치에서 작업을 하게 되었다. 해당 브랜치를 깃 클론 하는 방법도 있겠지만, 이번에는 기존 레포에서 다른 해당 브랜치로 이동해서 작업을 해보기로 했다.

브랜치를 하나 만들고 git pull origin {원하는 브랜치} 로 불러오는 방법이 있지만, 이후 브랜치를 이동하려고 하니 merge를 해주라며 문제가 발생. git reset으로 해결이 되지만 다른 방법으로도 가능하다고 한다.

우선 git remote update를 통해 원격 저장소의 브랜치를 업데이트 해서 같은 상태로 만들어 준다. git branch -r로 원격의 브랜치를, git branch -a로 로컬, 원격의 브랜치를 확인할 수 있다.

git checkout -t {가져오려는 원격의 브랜치 이름}을 하면 해당 원격의 브랜치를 가져오는 게 가능해진다.

Module not found: Can't resolve '@mui/material/utils' in ...

Module not found: Can't resolve '@mui/material/utils' in '/Users/sham/Desktop/42seoul_cadet/workspace/react/Bobs/node_modules/@mui/icons-material/utils'
material ui, icon을 사용하다 생긴 이슈. npm, yarn 모두 써가며 패키지를 설치했음에도 불구하고 인식을 하지 못한다. 다른 아이콘도 마찬가지. 그런데 마이페이지를 할 때는 문제없이 사용했다?

TypeError: setTitle is not a function

const MakeBook = props => {
  const { open, close } = props;
  const { title, setTitle } = useState('');

  const setSubmit = e => {
    console.log(e.target.value);
    // setTitle(e.currentTarget.value);
    setTitle('');
    console.log(title);
  };
  return (
    <>
      {open && (
        <div className="modal">
          <div className="section">
            <input
              type="text"
              className="input-room"
              onChange={setSubmit}
              value={title}
            />

            <footer>
              <button type="button" className="finish" onClick={close}>
                만들기
              </button>
            </footer>
          </div>
        </div>
      )}
    </>
  );
};

export default MakeBook;

useState로 hooks을 관리하려고 하자 발생. 구글링 결과 리액트와 리액트 돔의 버전이 다르면 생길 수 있다고 했으나 동일한 버전임을 확인.

기본적인 실수였다. useState로 분할 할당 할 때는 {}이 아니라 []다. 나는 똥멍청이다

div 태그에 onClick 적용하기

eslint 때문인지는 모르겠지만, div태그에 onClick 이벤트를 적용하니 에러가 터져나왔다.

What's wrong with you???

Visible, non-interactive elements with click handlers must have at least one keyboard listener

https://stackoverflow.com/questions/48575674/how-to-add-a-keyboard-listener-to-my-onclick-handler

onClick만 지정해서 생긴 에러, onKeyDown도 지정해주니 해결됐다.

Static HTML elements with event handlers require a role

https://stackoverflow.com/questions/54274473/how-to-fix-static-html-elements-with-event-handlers-require-a-role

역할을 부여해주지 않았다고 뜨는 경고인 것 같다. role="button"으로 해결됐다.

Elements with the 'button' interactive role must be focusable

https://stackoverflow.com/questions/56441825/how-to-fix-button-interactive-role-must-be-focusable

에러를 고치니 새로운 에러가 생겼다. 번역기를 돌려보니 버튼 역할이 자체적으로 초점을 맞출 수 없는 요소(div, span, p)에 추가되었을 경우, 탭인덱스 속성을 사용하여 버튼의 초점을 맞출 수 있어야 한다고 한다.

<div role="button" 이렇게 짧은 코드가 <div role="button" onClick={selectMenu} tabIndex={0}> 이렇게나 길어졌다.


중요 개념

git stash

https://gmlwjd9405.github.io/2018/05/18/git-stash.html

브랜치 작업 중 다른 브랜치로 이동하려고 할 때 커밋은 하고 싶지 않을 때 임시로 저장할 수 있다.

git stash로 스택에 저장한 후 git stash apply로 불러올 수 있다.

flex-basis

flex의 item에 지정할 수 있는 css 속성. 기본 크기가 flex-basis 만큼 고정된다. 일정한 간격을 주고 싶을 때 유용.

justify-content, align-items

flex 하위의 item을 가운데로 정렬하고 싶을 때, 메인축이 수평이나 수직이냐에 따라 각각 다르게 적용된다. justify-content는 메인축과 동일한 방향을, align-items는 메인축과 수직인 방향을 가리키게 된다.

useRef vs variable, useState 차이점

https://velog.io/@pks787/useRef-vs-variable-useState-%EC%B0%A8%EC%9D%B4%EC%A0%90

투명도 그라데이션

http://tcpschool.com/css/css3_module_linearGradients
http://tcpschool.com/css/css3_module_linearGradients

linear-gradient 속성으로 html element에 그라데이션을 적용해줄 수 있다.
linear-gradient(to top, white, white 10%, transparent)
첫 번째 인자는 그라데이션의 시작 지점(위에서부터)을 받는다. 두 번째 인자부터는 가변 인자로 색깔과 영역(%)을 지정함으로써 사용자 임의대로 그라데이션 범위를 설정해줄 수 있다. 위 코드는 맨 위에서부터 10% 까지는 흰색 배경이, 10% ~ 100% 인 나머지는 투명한 배경이 설정된다.

글씨에 그라데이션을 적용하려면 다음 링크로.


느낀 점

마이페이지를 작업할 때와 비교하면 난이도가 확 올랐다. 어찌어찌 구현하는데 성공은 했지만, 깔끔한 코드라고는 빈말로라도 못 말할 정도로 난잡한 코드가 되버렸다.
하나의 컴포넌트에 다 때려넣다보니 너무 복잡해진 것도 거슬린다. 당장 코드를 짠 나도 헷갈리는데 감히 다른 사람에게 넘길 엄두가 안 난다. 유지보수의 중요성, 컴포넌트화에 대해 다시금 깨달았다. 애초에 컴포넌트로 쪼개지 않는다면 리액트의 이점을 사용하지 못하는 것 아닌가 하는 생각이 들었다. 뿌듯하면서도 아쉽다.

모바일 상에서 보일 화면을 웹에서 작업하니 이게 제대로 된 건지 안 된건지 알 수가 없다. 배포하기 전에 테스트를 해 봐야 하긴 할 텐데... 부디 내가 적당히 실수했기를 비는 수 밖에ㅠ

profile
씨앗 개발자

0개의 댓글