[React] - D-day 계산기

ain·2022년 7월 18일
2

미니프로젝트

목록 보기
1/2
post-thumbnail

D-day 만들기

last update: 2022.07.19
todo list와 비슷하게 작동하는 D-day를 만들어 봤다. todo list 는 아직 직접 만들어 보진 못해서 모든 기능을 완벽하게 집어넣지는 못했다. 이번에 만들면서 아쉬웠던 점은 추후에 더 배워나가면서 보완해나갈 예정이다.

최종 화면


구조


UI

  1. 오늘 날짜
  2. 디데이 설명과 디데이 날짜 입력란
  3. 디데이 등록 버튼
  4. 등록된 디데이 목록

사용 언어 및 라이브러리

  • 언어: React
  • CSS 라이브러리: styled-components

Hook

  • useState
  • useEffect

기능


  • 화면 제일 상단에는 오늘 날짜가 보여진다.

  • 그 다음으로 두개의 입력란이 있는데 첫번째에는 어떤 D-day를 기다리고 있는지 적어주고, 두번째에는 D-day의 날짜를 적어준다.

  • 두개의 입력란에 값을 모두 적어주면 등록버튼이 활성화가 되어 아래에 등록 할 수 있다. (입력값이 없으면 눌러도 등록이 안됨.)

  • 등록버튼을 누르면 입력란이 비워진다.

  • D-day를 등록하면 D-day 설명과 D-day 날짜, 그리고 D-day까지 몇일 남았는지가 보인다. (D-day는 여러개가 등록이 된다.)

  • 여러개를 등록을 하면 제일 최근 등록한 D-day가 제일 위에 등록이 된다.

  • 만약 오늘 이전의 날짜를 설정하면 D-가 아닌 D+가 된다. (날짜수가 아닌 D-day이기 때문에 2022년 1월 1일이 D-day가 되어 1월 2일부터 D+1이 된다.)

  • D-day가 여러개라면 화면 최상단의 오늘 날짜와 입력란은 움직이지 않고 D-day들만 스크롤이 된다.

  • 등록된 D-day에 마우스를 올리면 지우기 텍스트가 보이고, 클릭을 하면 클릭한 D-day가 없어진다.


코드


  • 컴포넌트는 크게 3가지로 나누었다.
    1. 오늘 날짜를 표시해주는 컴포넌트 Today.js
    2. D-day를 입력해줄 수 있는 컴포넌트 Dday.js
    3. 그리고 D-day들이 등록될 컴포넌트 SetDday.js

[App.js]

App.js에는 크게 Today와 SetDday를 넣어주었다.

export default function App() {
  return (
    <>
      <Today />
      <SetDday />
    </>
  );
}

[Today.js]

const today = new Date();
const todayMonth = today.getMonth();
const todayYear = today.getFullYear();
const todayDate = today.getDate();
const todayDay = today.getDay();
const DAYS = ['일', '월', '화', '수', '목', '금', '토'];
export default function Today() {
  return (
    <TodayStyle>
      <TodayYear>{todayYear}</TodayYear>
      <TodayMonthnDate>
        {todayMonth + 1}{todayDate}{DAYS[todayDay]}요일
      </TodayMonthnDate>
    </TodayStyle>
  );
}
  • 오늘의 날짜를 전부 가져와서 JSX로 렌더링 해주었다.
    getMonth와 getDay는 인덱스가 0부터 시작한다. 때문에 Month에는 1을 더해주었고, DAYS 배열도 '월'부터가 아닌 '일'부터 배열로 만들어 주었다.
  • 적용해줄 style이 그렇게 많지가 않아서 따로 스타일 컴포넌트를 만들지 않았고 js파일에 바로 만들어서 넣어주었다. (나머지 컴포넌트도 동일)

[SetDday.js]

함수

const [isSubmit, setIsSubmit] = useState(false);
  const [list, setList] = useState([]);
  const [userInputs, setUserInputs] = useState({
    dDayName: '',
    date: '',
  });
  const handleChange = (e) => {
    setUserInputs({ ...userInputs, [e.target.name]: e.target.value });
  };
  const handleSubmit = (e) => {
    e.preventDefault();
    if (userInputs.dDayName === '' || userInputs.date === '') {
      return;
    } else {
      setIsSubmit(true);
      setList([...list, userInputs]);
    }
    for (let i = 0; i < 2; i++) {
      e.target[i].value = '';
    }
    setUserInputs({ dDayName: '', date: '' });
  };
  const deleteItem = (e) => {
    const key = e.target.id;
    list.splice(key, 1);
    setList([...list]);
  };

state

  • state는 각각 userInputs, isSubmit, list를 만들어주었다.
  • userInputs에는 D-day 설명 부분과 D-day 날짜 부분을 빈 문자열로 초기화 해준다.
  • input의 value에 onChange 이벤트가 발생하면 handleChange함수가 실행된다.
	const [isSubmit, setIsSubmit] = useState(false);
	const [list, setList] = useState([]);
	const [userInputs, setUserInputs] = useState({
    dDayName: '',
    date: '',
  });

handleChange

  • onChange됐을 때 실행되는 handleChange 함수에서는 userIputs의 값을 바꿔주는 역할을 한다.
  • setUserInputs를 통해서 기존의 userInputs를 뿌려주고, {이벤트 타겟의 name속성: 이벤트 타겟 값} 이런 형식으로 객체에 넣어준다.
    👉🏻 D-day 설명을 넣어주는 input(DDayInputStyle)에는 name속성에 'dDayname'이 들어가있고, D-day 날짜를 넣어주는 input에는 'date'이라는 값이 들어가 있기 때문에 초기에는 요렇게 -> { dDayName: '', date: '' } 객체가 만들어진다.
	setUserInputs({ ...userInputs, [e.target.name]: e.target.value });
	console.log(userInputs) // { dDayName: '', date: '' }

handleSubmit

  • handleSubmit 함수는 D-day 등록버튼을 눌렀을 때(form이 submit 되었을 때) 실행이 된다.
  • 만약 input 값에 아무것도 없는데 렌더링이 되면 안되기 때문에 두개의 input 중 하나라도 빈 문자열이 있으면 아무것도 반환하지 않게 return해준다.
  • 둘 다 값이 있으면 false였던 isSubmit을 true로 바꿔줘서 렌더링 되게 해준다. 그리고 빈 배열이였던 list에 userInputs객체를 넣어준다.
	if (userInputs.dDayName === '' || userInputs.date === '') {
      return;
    } else {
      setIsSubmit(true);
      setList([...list, userInputs]);
    }
  • for문을 돌려서 submit 이벤트 타겟의 처음 두번째 요소, 즉 D-day 설명 input과 D-day 날짜 input 값을 초기화 시켜준다.
	for (let i = 0; i < 2; i++) {
      e.target[i].value = '';
    }
  • 그런 다음 userInputs 객체의 값도 초기화 해준다. 초기화를 안해주면 사용자에게 보여지는 입력란은 비어있는 상태에서 submit을 눌러보면면 처음 입력했던 값이 그대로 똑같이 렌더링 된다.
	setUserInputs({ dDayName: '', date: '' });

결론적으로, 사용자 화면에서 input에 값을 넣어주면 userInputs 객체에 값이 들어가고, 등록 버튼을 누르면 D-day가 렌더링이 되면서 list배열에 userInputs 객체가 들어간다. 렌더링 됨과 동시에 사용자에게 보여지는 input값은 초기화가 되고, userInputs객체 값도 초기화가 된다.

JSX

return (
    <>
      <FormStyle name='isSubmit' onSubmit={handleSubmit}>
        <DDayInputStyle
          name='dDayName'
          type='text'
          placeholder="What's your D-day?"
          onChange={handleChange}
        />
        <DDayInputStyle name='date' type='date' onChange={handleChange} />
        <AddDdayBtn type='submit' value='+' />
      </FormStyle>
      <DDayStyle>
        {isSubmit &&
          list
            .map((d, idx) => (
              <Dday info={d} key={idx} id={idx} delete={deleteItem} />
            ))
            .reverse()}
      </DDayStyle>
    </>
  );
  • D-day가 렌더링 되면 보여질 Dday 컴포넌트는 isSubmit이 true면 보여지고, false면 렌더링이 안되게 하기 위해 AND연산자를 써주었다.
  • D-day는 여러개 등록 될 것이기 때문에 map으로 뿌려주고, key는 idx로 넘겨주었다. D-day를 완전히 삭제하고 편집 할 수 있으려면 map의 idx말고 고유한 값을 주어야 하는데 그 방법을 아직 잘 몰라서 일단 idx로 주었다. id를 1씩 증가해주는 state를 만들어서 submit할때마다 1씩 증가되게 해주었는데 그렇게 해도 키가 고유하지 않다는 에러가 떴다...
  • D-day가 한개 이상이 렌더링 될 때 새로운 D-day가 아래로 추가된다. 그러면 계속 스크롤을 내리면서 봐야하기 때문에 reverse()를 써서 아래가 아닌 위로 추가되게 만들었다.
  • list 배열에 들어간 userInputs의 객체들을 렌더링 해줄 Dday컴포넌트에 넘겨야 하기 때문에 props를 넘겨주었다.
{isSubmit && list.map((d, idx) => <Dday info={d} key={idx} />).reverse()}

이제 Dday.js로 가보자 ✈️

[Dday.js]

함수

  const { dDayName, date } = props.info;
  const [hover, setHover] = useState(false);
  const [days, setDays] = useState(0);

  useEffect(() => {
    const today = new Date();
    const dday = new Date(`${date} 00:00:00`);
    const gapNum = dday - today;
    setDays(Math.ceil(gapNum / (1000 * 60 * 60 * 24)));
  }, [date]);

props

  • SetDday에서 넘겨받은 props를 가져온 후 dDayName은 그대로 렌더링해주고, date는 D-day를 계산해서 렌더링 해주었다.

state

  • boxDelete : 지우기 기능을 실행하기 위해 먼저 true로 초기화 해주었고 D-day를 클릭하면 false로 바뀌게 해줄 state이다.
  • hover : D-day에 마우스를 올리면 '지우기' 텍스트가 보여져야하기 때문에 초기값은 false이고 마우스가 올라갔을 땐 true, 밖으로 뺐을 땐 false로 다시 바꿔줄 state이다.
  • days는 D-day의 'day'부분을 담당한다. 초기값은 0으로 설정.

useEffect

  • useState 렌더링은 비동기로 이루어지기 때문에 만약 setDays함수가 useEffect 바깥에 있다면 무한루프가 돌기 때문에 'Too many re-renders' 에러가 날 수 있다.

  • 그래서 이러한 side effect로부터 보호하기 위해 useEffect를 쓴다. 사용자가 submit을 눌러 D-day가 렌더링 될 때, 즉 SetDday 컴포넌트에서 isSubmit이 true로 바뀌어 Dday 컴포넌트가 렌더링 되어야 할때 dDayname과 date를 props를 받아와서 D-day를 계산한다.
  useEffect(() => {
      const today = new Date();
      const dday = new Date(`${date} 00:00:00`);
      const gapNum = dday - today;
      setDays(Math.ceil(gapNum / (1000 * 60 * 60 * 24)));
  }, [date]);
  • D-day를 계산하려면 지금 날짜에서 사용자가 입력한 날짜를 빼줘야한다.
    처음에는 dday에 new Date(date)를 해주고 계산을 했더니 사용자가 입력한 값의 시간이 00:00:00가 아닌 09:00:00로 되어있어서 아침 9시전에는 하루가 아직 안 지난 것처럼 표시가 됐었다. 예를 들어 오늘 날짜를 입력하면 D-day가 되어야 하는데 D-1이 표시가 됐었다.
    그래서 dday는 시간을 직접 00:00:00 로 설정해주었다.
  • Math.floor가 아닌 Math.ceil을 사용한 이유:
    D-day가 다가왔을 때 gapNum이 -0.nnnnn...이 나온다. 이때 Math.floor를 쓰면 0아닌 -1이 된다. 날짜수가 아닌 D-day이기 때문에 오늘을 0으로 기준으로 해야 보기 좋을 것 같다고 생각해서 Math.ceil을 사용하였다.

JSX

return (
    <div
      id={props.id}
      onClick={props.delete}
      onMouseEnter={() => {
        setHover(true);
      }}
      onMouseLeave={() => {
        setHover(false);
      }}

      <BoxStyle
        style={{
          transform: hover ? 'scale(1.02)' : 'scale(1)',
        }}

        <DDayInfostyle>
          <DDayNameStyle>{dDayName}</DDayNameStyle>
          <DateStyle>{date}</DateStyle>
        </DDayInfostyle>
        <DDayStyle>
          <span>
            D{days >= 0 ? '-' : '+'}
            {days === 0 ? 'day' : Math.abs(days)}
            <br />
          </span>
        </DDayStyle>
        {hover && <CloseBtn>지우기</CloseBtn>}
      </BoxStyle>
    </div>
  );
  • D-day를 click하면 boxDelete 값을 false로 만들어서 지워준것처럼 만들어준다. 하지만 이렇게 하면 DOM에서 아예 지워지는게 아니라 시각적으로만 사라진다. 아예 지워주려면 key값을 고유하게 줘야 하는데 아직 고유한 key값을 주는 방법을 찾지 못했다.
  • D-day에 마우스를 올려 onMouseEnter 이벤트가 발생하면 hover를 true로 바꿔주어 CloseBtn요소를 띄워준다.
  • D-day에 마우스를 빼서 onMouseLeave 이벤트가 발생하면 hover가 false로 바뀌고 CloseBtn요소가 다시 사라진다.
  • dDayName과 date는 그대로 렌더링 해준다.
  • D-day날짜를 계산한 값은 과거일 때 '+'를 붙여주고, 미래일 때 '-'를 붙여주었다. days 값이 0보다 크거나 같다면 '-'를, 작다면(음수라면) '+'를 붙여준다.
  • 숫자부분을 만약 0이 되면 day를 반환해서 D-day가 나오도록 하고, 0이 아니라면 days를 렌더링 해주는데 만약 음수가 나오면 D+-n 이런식으로 나오기 때문에 Math.abs을 사용해서 절댓값으로 렌더링 해준다.
  • hover가 onMouseEnter와 onMouseLeave에 의해서 boolean값이 바뀌면 그에 따라 지우기 텍스트를 표시해준다.

Styled-Components

  • SetDday.js의 styled-components에서는 focus가 돼었을 때 background-color가 바뀌도록 해주었다.

&:hover {
    background-color: #6b705d;
  }
  • D-day등록 버튼도 마찬가지로 hover시에 background-color가 바뀌게 해주었다.

  • D-day는 여러개가 있다면 그 부분만 스크롤이 되게 position: fixed 해주었다.
  • D-day에 마우스를 올렸을 때 D-day 박스의 크기가 살짝 커지면서 '지우기' 텍스트가 나오게 해주었다.
    '지우기'텍스트가 나올때 background의 모서리를 숨기기 위해 D-day 박스에 overflow: hidden을 주어 border-radius가 박스에 딱 맞게 해주었다.

배운 점

  • 사용자가 input에 값을 입력했을 때 그 값을 받아 화면에 뿌려줘야 하는데 그걸 처음에는 submit버튼을 누르기도 전에 onChange 이벤트가 실행되면서 그냥 그대로 렌더링 해줘버려서 '등록'이 아닌 '실시간 편집'이었었다..

  • onChange는 값을 바꿔주고 저장하는 역할을 하고, onSubmit은 '등록'하는 역할을 할 수 있도록 역할분배를 해야 한다는 것.

  • 입력값을 받아오고 계산하는 것을 하나의 컴포넌트가 다 해야 하는 건줄 알았는데 받아오는 것은 부모 컴포넌트가 하고 그 값을 따로 계산해서 전달하는 것은 자식 컴포넌트가 할 수 있다는 것. (props와 usueEffect 사용)

  • 자바스크립트로만 구현해 오다가 react를 써보니까 state가 너무 유용하고 간편하다는 점!! (강의로만 보고 직접 구현해보진 않았을 때는 "오..좋네" 정도 였는데 직접 구현을 해보니까 또 느낌이 다르다.)

  • submit을 하면 input값이 비워지도록 하려면 이벤트 타겟으로 할 수 있다는 점. (onSubmit의 이벤트 타겟과 onChange의 이벤트 타겟은 연관성이 없어서 구현을 못할 줄 알았다.)

아쉬운 점

  1. state가 여러개 있을 때 하나의 state로 객체로 만들어 묶을 수 있었던 것 같은데 아직 접근 방법과 사용 방법에 익숙치 않아서 dDayName과 date를 제외하고는 전부 따로따로 설정해주었다.

  2. 고유한 key값을 주지 못해서 map의 idx로 주었다. 고유한 key값이 없으면 CloseBtn 클릭시 D-day를 DOM에서 완전히 삭제하는 것을 못한다.

동기분의 도움으로 해결! last update: 2022.07.19
  1. 부모 컴포넌트 SetDday.js에서 map의 인덱스를 props로 전달해주고, onClick 이벤트가 발생하는 요소에 속성으로 인덱스를 넣어 각 D-day의 고유한 id에 접근 할 수 있도록 해준다. 부모 컴포넌트에서 함수를 만들어 주고 인덱스와 함께 자식 컴포넌트에 전달해준다.
const deleteItem = (e) => {
    const key = e.target.id;
    list.splice(key, 1);
    setList([...list]);
};
/* e.target.id로 아래 props로 전달했던 id를 가져오고,
list배열에서 이 id를 가진 요소를 splice로 지워준 다음
다시 setList 함수를 써서 list를 업데이트 해준다.*/
{isSubmit && list.map((d, idx) => (<Dday info={d} key={idx} id={idx} delete={deleteItem} />)).reverse()}
/* map의 idx를 key에 부여하고 id 속성을 따로 만들어 props로 전달.
deleteItem 함수도 props로 전달해준다.*/
  1. 자식 컴포넌트에서는 이 모든걸 props로 받는다.
    id는 속성으로 설정해주고, onClick이 됐을 때는 deleteItem함수를 받아와서 실행 되게 한다. 이때 기존에 썼던 boxDelete state는 필요가 없으니 지워준다.
<div id={props.id} onClick={props.delete}>...</div>
  1. 또 todo list처럼 편집기능도 만들고 싶고, 위치 이동도 만들고 싶은데 그것도 다 고유한 키 값이 필요한 것 같다.

  2. submit 버튼을 누르면 dDayName 입력란에 자동으로 focus가 되게 하고 싶은데 이건 useRef를 써야 하는 것 같다. 아직 익숙치 않아서 쓰질 못했다.

  3. D-day 계산법 말고 날짜수도 만들어 보고 싶다. 예를 들면 '태어난지 몇일째'를 태어난 날을 1일로 해서 D+n 이런식으로...

이번 프로젝트는 일주일에 하나씩 하는 프로젝트이기 때문에 최대한 간단히 하기 위해 기능도 최소한으로 하였다. 구현하는 방법을 알아낸다면 여기서 더 기능도 추가하고 개선 및 수정을 진행 할 예정이다.

참고

profile
프론트엔드 개발 연습장 ✏️ 📗

0개의 댓글