SENTENCE U | Day 17 (포스트 날짜별 구분/시계/날씨/캘린더)

블로그 이사 완료·2023년 1월 26일
0
post-thumbnail

포스트 리스트 날짜별로 구분

최신글 처럼 날짜별로 보이면 좋을 것 같은 리스트는 섹션별로 나눠서 보여주는 것이 좋을 것 같았다.

dayjs를 사용해서 postList라는 매개변수를 받아 그 글의 createdAt을 최신순으로 MM월 DD일 ddd요일 형태로 새로운 배열에 추가한다.

import dayjs from 'dayjs';
import 'dayjs/locale/ko';

dayjs.locale('ko');

export const makeSection = (postList) => {
  const sections = {};
  postList.forEach((post) => {
    const monthDate = dayjs(post.createdAt).format('MM월 DD일 ddd요일');
    if (Array.isArray(sections[monthDate])) {
      sections[monthDate].push(post);
    } else {
      sections[monthDate] = [post];
    }
  });

  return sections;
};

사용하려는 컴포넌트 (예를 들어 최신 글)에서 전체포스트를 함수에 보내 실행시키면 날짜별로 섹션이 구분된 postSections라는 객체를 받게 되는 것이다.

이 때, 새로운 배열 만들 때는 불변성 지킬 것

const { allPosts } = useGetAllPosts();
const postSections = makeSection(allPosts ? [...allPosts] : []);

날짜별로 구분된 postSections 객체를 Object.entriesmap을 사용해서 모든 객체를 풀어주는데 이 때 dateposts로 구분되어진다.

{Object.entries(postSections).map(([date, posts]) => {return ()})};

이렇게 map을 통해 데이터에서 date는 날짜 구열을 나타내는 버튼에 추가하고, 날짜별로 묶여있는 posts데이터는 다시 map을 통해 렌더링 한다.

{Object.entries(postSections).map(([date, posts]) => {
  return (
    <DateSection key={date}>
      <DateHeader>
        <button>{date}</button>
      </DateHeader>
      {posts.map((post) => (
        <PostList pros.... />
      ))}
    </DateSection>
  );
})}

DateHeader라는 컴포넌트는 날짜버튼을 나타내는데 날짜를 한번만 보여주고 리스트가 계속 스크롤링 되면 유저는 해당 포스트의 날짜를 쉽게 볼 수 없을 수 있다.

그래서 DateHeader컴포넌트의 CSS(Emotion)에는 position:sticky라는 스타일을 꼭 적용 시켜야한다.

export const DateHeader = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  position: sticky; // sticky 필수!
  height: 50px;
  top: 0;
  z-index: 10;
  background-color: var(--background);
  button {
    font-size: 14px;
    height: 30px;
    line-height: 27px;
    padding: 0 16px;
    z-index: 2;
    border-radius: 50px;
    position: relative;
    background: var(--white);
    outline: none;
  }
`;

sticky는 평소엔 fixed처럼 고정되어 있지만 해당 컴포넌트나 요소가 끝나서 사라지면 함께 이동하는 효과를 보여준다.


홈에 시계, 날씨 기능 추가

유저리스트의 구역이 넓은 영역을 사용하는데 유저리스트만 사용하는 것에 대해 레이아웃 수정이 필요하다고 느꼈다.

그래서 초단위의 시계를 보여줄 수 있는 큰 시계와, 날씨 API를 사용해 위치 기반으로 날씨를 보여주는 기능을 추가했다.

시계 추가

시계는 간단하다.

현재 시간을 담을 nowTime이라는 state를 생성하고, setInterval을 1초마다 실행시켜 dayjs로 원하는 format에 맞춰 받아와 nowTime에 담아준다.

import { Container } from './Styles';
import dayjs from 'dayjs';
import React, { useState } from 'react';

const Time = () => {
  const [nowTime, setNowTime] = useState('');

  setInterval(() => {
    setNowTime(dayjs(Date.now()).format('HH:mm:ss'));
  }, 1000);

  return <Container>{nowTime}</Container>;
};

export default Time;

날씨 추가

일단 openweather API를 사용한다. 회원가입을 하면 API Key를 발급받을 수 있다.

마찬가지로 Weather컴포넌트 안에서 모든 기능을 구현하면 렌더링 될때마다 유저의 지역정보를 가져오고, API에 날씨를 요청한다.

그래서 따로 useGetWeather 함수를 만들었다.

useGetWeather함수는 useEffect를 사용해서 처음 실행됐을 때 한번만 사용자의 경도와 위도를 가져온다.

export const useGetWeather = () => {
  let lat;
  let lon;

  useEffect(() => {
    navigator.geolocation.getCurrentPosition((position) => {
      // 위치 가져오기 성공 시 경도,위도 저장
      lat = position.coords.latitude; // 경도
      lon = position.coords.longitude; // 위도
    });
  }, []);

lat과 lon 변수는 아래에 추가로 올 useQuery문에서도 사용해야 하기 때문에 useEffect스코프 밖에 선언해놨다.

실행되자마자 경도와 위도를 가져오면 openweather API로 날씨 정보를 요청해서 받은 res.data를 쿼리의 data에 할당한다.

자세한 API사용법은 공식 사이트에 나와있다.

const { data, isLoading, error } = useQuery(
    ['weather'],
    async () => {
      return await axios
        .get(
          `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${process.env.WEATHER_API_KEY}&units=metric`,
        )
        .then((res) => {
          return res.data;
        });
    },
    {
      refetchInterval: false,
    },
  );

  return { data, isLoading, error };
};

받아온 데이터를 함수 밖으로 내보내서 Weather컴포넌트에서 받아와 사용한다.

const Weather = () => {
  const { data, isLoading, error } = useGetWeather();

  const weather = data?.weather[data?.weather.length - 1];
  const weatherSrc = `http://openweathermap.org/img/wn/${weather?.icon}@2x.png`;

  if (isLoading) {
    return <Container>날씨 로딩중...</Container>;
  } else {
    return (
      <Container>
        <div>현재 위치 : {data?.name}</div>
        <div>현재 온도 : {Math.floor(data?.main.temp)} &#176;C</div>
        <div>{data?.main.temp}{weather?.main}</div>
        <img src={weatherSrc} alt='' />
      </Container>
    );
  }
};

data안의 많은 객체를 사용해서 원하는 데이터들을 가져올 수 있다.


날씨 API KEY를 위한 dotenv 추가

CRA가 아닌 직접 리액트 환경을 만들 다 보니 필요한 라이브러리들도 없고, 많은 세팅들도 필요했다.

초반에 웹팩 설정을 잘해서 더이상 문제가 없을 줄 알았는데 날씨API를 사용하기 위해 받은 API키를 숨길 .env파일을 사용했다.

CRA를 사용해서 프로젝트를 생성했을 때는 문제없이 process.env.API_KEY를 사용해서 숨겨놓은 API키값을 가져올 수 있었다.

날씨 API키를 .env에 작성해놓고 불러와서 사용하는데 process is not defined 오류가 발생했다.

CRA환경이였다면 문제없이 작동하던 코드인데 되지 않는 걸 보아하니 웹팩쪽에서 뭔가 설정이 부족하다고 바로 판단할 수 있었다.

node.js에서 서버구축할때도 사용했던 dotenv를 필요로 할 것 같았고 구글링을 해서 결국 방법을 찾았다.

CRA 없이 리액트 프로젝트 생성 시 .env파일 사용하기

npm i -D dotenv // dotenv 설치
// webpack.config.js
const dotenv = require('dotenv');
const { DefinePlugin } = require('webpack');
dotenv.config();

module.exports={
  ....

  // 플러그인에 아래 코드 추가
  plugins:[
 	 new DefinePlugin({ 'process.env': JSON.stringify(process.env) }),
  ]
}

위와 같이 설정해주면 원하는 컴포넌트에서 process에 접근할 수 있게 됐다.


다이어리 페이지에 캘린더 추가

다이어리 페이지에는 캘린더와 개인 한줄 일기를 쓸 수 있는 기능을 추가한다.

일단 캘린더가 필요해서 react-calendar 라이브러리를 사용해서 캘린더를 불러왔다.

npm i react-calendar

사용법은 간단하다.

import 'react-calendar/dist/Calendar.css' // css 불러오기

const ReactCalender = () => {
  const [value, onChange] = useState(new Date()); // Calendar의 기본 설정
  
  return (
    <Calendar onChange={onChange} value={value} />
  );
};

캘린더 CSS 커스터마이징

그런데.. 이게 무슨 디자인이야...나는 참을 수 없었다..

node_modules/react-calendar/dist/Calendar.css 를 확인해보면 저 위의 디자인을 보여주는 참혹한 코드들이 있다.

컴포넌트에서 css를 불러온 import코드는 지우고 직접 저 css코드를 복사해와서 커스터마이징 했다.

연도, 월, 주, 일, 화살표, 호버, 포커스, 클릭, 폰트, 컬러, 레이아웃 등 하나하나 클래스명을 확인하면서 수정했다.

원래는 호버하면 버튼 전체가 색상이 바뀐다. 버튼박스의 전체 호버는 삭제하고 일과 월만 호버시 색상이 변하도록 했다.

월과 일은 모든 버튼 안에 abbr 태그가 있어 텍스트만 CSS를 적용 할 수 있었다.

abbr {
  background: var(--darkgray) !important; 
  color: var(--white) !important;
}

!important를 적용해야 기존 라이브러리에서 element style로 넣은 css를 덮을 수 있다.

그렇게 디자인 적으로 커스터마이징을 완료하고 마지막 남은 문제는 모든 일에 '일'이 붙어있었다. 1일, 2일, 3일...

처음엔 abbr태그 안에 있으니 태그 안에 있는 문자열을 이라는 단어를 기준으로 나눠서 useEffect안에서 모든 abbr태그의 innerHTML을 변경하는 방식으로 했다.

그럼 연산량도 너무 많고 렌더링이 계속 되는 문제가 발생했다. 그리고 월이나 연도 이동시에는 초기화도 됐다.

라이브러리에 대한 옵션들을 찾아보다가 formatDay라는 옵션이 있다는 것을 찾았다.

[일]을 지우려면 formatDay locale이 ko라고 되어있으면 [일]이 나온다고 한다.

해결하기 위해서 formatDay에 사용된 locale을 en으로 고정시키면 숫자만 나온다고 하는데 설정하면 에러가 발생한다.

커스터마이징의 완벽한 마무리를 위해 다른 분들의 글을 찾아보다가 나랑 같은 문제를 겪으신 분이 계셨다.

그분의 해결방법은 dayjs를 사용해서 date의 포맷을 DD혹은 D로 설정해서 숫자만 나오도록 하는 것이었다.

<Calendar
  onChange={onChange}
  value={value}
  formatDay={(locale, date) => dayjs(date).format('D')}
  />

그렇게 완성된 캘린더

profile
https://kyledev.tistory.com/

0개의 댓글