이야기 정리

[리액트] 제주 관광사이트 만들기 본문

Project/React project

[리액트] 제주 관광사이트 만들기

jinhistory 2023. 3. 5. 14:59

제주관상 사이트, 오늘의 제주

사이트 바로가기 : https://todays-jeju.netlify.app/

코드 보기 : https://github.com/beren-105/jeju_tourism_web

 

 

개발동기

'오늘의 제주'는 사용자가 제주도의 날씨를 확인하고 원하는 관광지를 찾을 수 있는 플랫폼이다.

 
"제주도에 여행왔는데 오늘은 어디를 갈까?"
여행에 있어 목적지를 정하는 것은 선택이 아닌 필수다. 목적지를 정하려면 많은 정보가 필요하다. 어디에 무엇이 있는지, 그게 어떤 것인지도 알아야하고 또, 그 장소에 어울리는 날씨인지도 중요하다.
오늘의 제주는 사용자가 제주도의 정보를 사이트 하나로 알 수 있는 편의성을 제공하기 위해 개발되었다.
제주도에서 오늘의 일정을 계획하려는 관광객을 주 타겟으로 삼고 있으며, 사이트에 들어가자마자 메인 비주얼의 이미지를 통해 손쉽게 오늘의 날씨를 확인할 수 있다. 그 외에 테마여행, 숙박업소, 출제, 음식점에 대한 다양한 정보를 통해 사용자의 편의성을 제공한다.
 
 
사용툴
  • 프레임워크 : React, Tailwind
  • API : 비짓제주, 기상청 단기예보
  • 라이브러리 : React Slick

 

 폴더구조

 

1. App 페이지


- 문제상황(1) : 날씨 API 로딩 지연

날씨데이터와 관광데이터 두개를 가져오는데에는 성공했으나, 날씨 API의 데이터 양이 많이 로딩 시간이 지연되었다. 그로인해 데이터를 찾을 수 없다는 에러가 반복되었다.

해결

처음에는 삼항연산자를 사용해 해결했으나, 가독성을 위해 단축평가를 이용했다. 전달하는 데이터에 따라 날씨데이터, 혹은 관광데이터가 있을 시 컴포넌트를 보여줌으로써 오류를 방지했다.

<Router>
  <Routes>
    <Route path='/' element={<Layout />}>
      <Route index element={
        weatherData &&
          <MainContent
            weatherData = {weatherData.response.body.items.item}
            visitJejuData = {visitJejuData.items}
            nowWeather = {nowWeather}
            tags = {tags}
            today = {today}
          />
      }/>
      <Route path='theme' element={
        visitJejuData &&
        <Theme
          visitJejuData = {visitJejuData.items}
          tags = {tags}
        />
      }/>
      <Route path='theme/:id' element={<Detail />} />
      {weatherData &&
        <Route path='kakaomap' element={
          <KakaoMap
            weatherData = {weatherData.response.body.items.item}
            visitJejuData = {visitJejuData.items}
            nowWeather = {nowWeather}
            today = {today}
          />
        } />
      }
    </Route>
    <Route path='*' element={<NotFound />} />
  </Routes>
</Router>

 

 

- 문제상황(2) : 달이 변경되자 날씨 데이터에 에러 발생

원인은 간단했다. 날씨 데이터는 어제 예보된 날씨를 바탕으로 오늘의 날씨를 알려주고 있다. 이때, 어제 일자를 date.getDate()-1하나로만 처리했기 때문에 달이 바뀌자 1일이 0일이 되어 오류가 발생한 것이다.

해결

이전 캘린더를 만들었을 때 사용한 코드를 활용해 일자를 생성하는 함수를 수정했다. 어제 일자는 데이터를 불러오기 위해 필요하고, 오늘 일자는 불러온 데이터를 바탕으로 오늘 일자만을 뽑아내기 위해 필요하다. 두가지 날짜가 동시에 필요했기 때문에 각각의 날짜를 구해주었다.

 

// 지난달과 지난달의 마지막 날짜 구하기
const _prevMonth = new Date(date.getFullYear(), date.getMonth()-1, 1)
const _prevDay = new Date(date.getFullYear(), date.getMonth(), 0)

 

위 내용을 이용해 아래와 같이 코드를 수정했다.

let yesterday;
let today;

// 어제/오늘 일자
function getDays() {
  if (date.getDate() === 1 && date.getMonth() < 10) {
    const year = date.getFullYear().toString();
    const month = ("0" + (date.getMonth() + 1)).slice(-2);
    const day = ('0' + date.getDate()).slice(-2);

    const _prevMonth = new Date(date.getFullYear(), date.getMonth()-1, 1)
    const _prevDay = new Date(date.getFullYear(), date.getMonth(), 0)
    const prevMonth = ("0" + (_prevMonth.getMonth() + 1)).slice(-2);
    const prevDay = ('0' + _prevDay.getDate()).slice(-2);

    yesterday = (year + prevMonth + prevDay);
    today = (year + month + day);
  }
  if (date.getDate() > 1) {
    const year = date.getFullYear().toString();
    const month = ("0" + (date.getMonth() + 1)).slice(-2);
    const prevDay = ('0' + (date.getDate() - 1)).slice(-2);
    const day = ('0' + date.getDate()).slice(-2);
    
    yesterday = (year + month + prevDay);
    today = (year + month + day);
  }
}
getDays()

 

 

2. MainContent - 메인화면


- 메인 비주얼

메인 비주얼은 현재 시간대의 날씨를 이미지, 아이콘, 텍스트를 사용해 보여준다. 비가 오면 메인 이미지가 비가 내리는 이미지로 바뀌고, 맑으면 맑은 사진으로 바뀐다. 바뀌는 내용이 많기 때문에 if문 보다 switch문을 사용해 정리했다.

현재 날씨 세팅
function weatherSetting() {
    switch (props.nowWeather) {
        case '비' :
            rain = true;
            setMainImg(rainImg);
            setMainIcon(faCloudRain);
            setMainText('비가 내립니다!');
            break;
        case '소나기' :
            rain = true;
            setMainImg(rainImg);
            setMainIcon(faCloudRain);
            setMainText('소나기가 내립니다!');
            break;
        case '눈' : 
            rain = true;
            setMainImg(snowImg);
            setMainIcon(faSnowflake);
            setMainText('눈이 내립니다!');
        case '맑음' :
            rain = false;
            setMainImg(sunImg);
            setMainIcon(faSun);
            setMainText('화창한 맑음입니다!');
            break;
        case '조금 흐림' : 
            rain = false;
            setMainImg(bitCloudyImg);
            setMainIcon(faCloudSun);
            setMainText('조금 흐립니다!');
            break;
        case '흐림' :
            rain = false;
            setMainImg(cloudyImg);
            setMainIcon(faCloud);
            setMainText('흐린 날씨입니다!');
            break;
    }
}

 

 

메인 비주얼 하단에는 사용자가 현재 날씨뿐 아니라 시간대별 날씨를 확인할 수 있는 칸이 함께 있다. 총 5개의 시간대를 가져오는데, 모두 같은 함수에 값만 넣어 해당 값을 return 시키게 만들고 싶었다.

고민하던 중 생성자 함수를 이용했다. HourlyWeather라는 함수를 만들어 해당 함수 안에 index를 넣으면 해당 시간 대의 정보(날씨, 날씨 아이콘, 몇시인지 등)를 반환한다.

 

// 시간대별 날씨 세팅
function HourlyWeather(index) {
    let timeSting = [];

    timeArr.map((time)=>{
        if (time < 10) {
            return timeSting.push('0'+time+'00')
        } else {
            return timeSting.push(time+'00')
        }
    });

    const filterPTY = weatherData
        .filter(weatherData => weatherData.fcstDate === today && weatherData.fcstTime === timeSting[index] && weatherData.category === 'PTY')[0].fcstValue;
    const filterSKY = weatherData
        .filter(weatherData => weatherData.fcstDate === today && weatherData.fcstTime === timeSting[index] && weatherData.category === 'SKY')[0].fcstValue;

    this.index = index;
    this.setTime = timeArr[index];
    this.setIcon = function () {
        if (filterPTY > 0) {
            return faCloudRain;
        } else if (filterSKY < 2) {
            return faSun;
        } else if (1 < filterSKY < 4) {
            return faCloudSun;
        } else if (filterSKY > 3) {
            return faCloud;
        } else {
            return faQuestion;
        }
    }
    this.setWeather = function () {
        if (filterPTY > 0) {
            return '비';
        } else if (filterSKY < 2) {
            return '맑음';
        } else if (1 < filterSKY < 4) {
            return '조금 흐림';
        } else if (filterSKY > 3) {
            return '흐림';
        } else {
            return '알 수 없음';
        }
    }
}

return
	(
    <div className='max-w-5xl mx-auto flex justify-center justify-between'>
        {timeArr.map((time, i) => (
            <div
                key={'div'+i}
                className={`flex flex-col items-center ${date.getHours() === time ? 'opacity-90' : 'opacity-50'}`}
            >
                <FontAwesomeIcon
                    key={'icon'+i}
                    className='mb-2'
                    size='2xl'
                    icon={new HourlyWeather(i).setIcon()}
                />
                <p className='text-xs font-bold' key={'text'+i}>{new HourlyWeather(i).setWeather()}</p>
                <p className='text-xs font-bold' key={'time'+i}>{new HourlyWeather(i).setTime}시</p>
            </div>
        ))}
    </div>
	)

 

3. Theme - 서브페이지


 

- 문제상황(3) : 검색어가 없을 시 출력 결과가 없다.

검색 결과가 없을 시 모든 내용이 보이게, 즉 새로고침을 한 것처럼 초기화를 시키고 싶었다. 그리고 검색한 결과가 없을 시에만 '결과가 없습니다'라는 메시지를 출력하게 했다. 그러나 의도와 다르게 검색어가 빈칸이면 결과가 없다는 메시지가 떴다.

또, input에 타자로 입력한 결과와 하단에 태그 버튼을 클릭했을 때의 결과가 중첩되길 원했다. 음식점 태그를 클릭하면 음식점에서만 결과가 검색되는 것이다. 이 기능 또한 잦은 에러로 함께 수정했다.

 

해결

본래 total이라는 하나의 변수에서 데이터를 처리했으나, 변수를 여러개로 나누었다.

태그를 클릭했을 때의 데이터는 select, 검색했을 때의 데이터는 search로 나누고, 마지막으로 total에서 두 데이터를 합쳤다.

그리고 input의 value 값을 useState(null)이라 적은 것을 깨닫고 useState('')로 고쳤다.

 

// 분류 버튼과 검색창 결과 필터링
function matchingData(search, tag) {
    if (search.length === 0 || tag.length === 0) {
        setTotal(null);
        setTotal(null);
    } else if (search.length >= tag.length) {
        const tagArr = []
        tag.map(tag => tagArr.push(tag.contentsid))
        const arr = search.filter((search) => {
                return tagArr.includes(search.contentsid)
        });
        setTotal(arr);
    } else {
        const searchArr = []
        search.map(search => searchArr.push(search.contentsid))
        const arr = tag.filter((tag) => {
                return searchArr.includes(tag.contentsid)
        });
        setTotal(arr);
    }
}

 

 

4. Detail - 상세페이지


useNavigate를 통해 페이지를 구성할 데이터를 Detail에 전달하고, 해당 데이터를 이용해 페이지를 구성했다.

 

- 문제상황(4) : 댓글 수정기능 시 모든 댓글들이 선택된다.

로그인 구현 예정이 없었기에, 댓글은 이전 to do list를 만들었을 때처럼 별도 로그인 없이 추가 및 삭제가 가능하게 만들었다.

문제는 여러개의 댓글이 있을 때 하나의 댓글을 선택하면 모든 댓글들이 수정모드로 전환됐다. 선택한 댓글만이 수정모드로 바뀌어야 했기에 이 에러 역시 고쳐야 했다.

 

해결

별도의 컴포넌트가 각각 만들어지게 수정했다.

기존 방식은 컴포넌트를 별도로 사용하지 않았으나, map을 이용해 각 배열 안에 있는 각 객체를 새로운 컴포넌트로 넘기며 문제를 해결했다.

{comments.length === 0 ?
    <li className="text-center py-8">현재 리뷰가 없습니다.</li>
:
    comments.map((comment) => (
        <Comment
            key = {comment.id}
            comment = {comment}
            comments = {comments}
            setComments = {setComments}
        />
    ))
}

 

 

5. KakaoMap- 맵 페이지


- 문제상황(5) : 검색 후 페이지 버튼의 개수가 새로고침을 해야 고쳐진다.

맵 리스트에 검색을 했을 때 결과가 많으면 페이지가 여러개, 하나면 1개의 페이지만 보여야하는데 한박자씩 늦게 업데이트가 되는 현상을 발견했다. 즉, 다음 행동을 해야지 페이지가 변경되는 것이다.

 

해결

5개 이상의 버튼을 자르고, 자른 뒤 바로 화면에 보이는 버튼을 업데이트하며 생긴 문제로, 비동기라는 특징에 대한 이해가 부족해 때문에 발생했다.

리액트는 비동기다. 여러개의 버튼을 slice한 뒤, slice한 버튼을 바로 다른 곳에 적용하려고 하니 다른 곳에 적용은 안되고 slice까지만 적용된 문제였다.

이를 해결하기 위해 useEffect를 두개를 사용하고, 각각 다른 타이밍에 업데이트가 되게 만들었다.

 

// 페이지네이션
const allDatas = props.allDatas
const [pegeData, setPegeDate] = useState(allDatas.slice(0, 5));
const [pege, setPege] = useState(1);
const [pegeBtn, setPegeBtn] = useState([]);
const [btns, setBtns] = useState([]);

useEffect(() => {
    const arr = [];
    for (let i=1; i<=Math.ceil((allDatas.length)/5); i++) {
        arr.push(i)
    }

    setPegeBtn(arr);
    setPegeDate(allDatas.slice(0, 5));
}, [allDatas])

useEffect(() => {
    setBtns(pegeBtn.slice(0, 5));
}, [pegeBtn])

 

 

느낀점


이번 코드에서 가장 아쉬웠던 부분은 날씨에 따른 지역 추천 기능이 많지 않다는 것이다.

메인 화면 외의 장소에서 날씨 기능을 사용하는 곳이 없기 때문에, 사용자가 날씨에 따른 검색을 하기 어렵다. 어떻게 하면 날씨 API를 더욱 다양하게 사용할 수 있을지에 대한 고민이 필요하다.

또 아쉬운 점은 로그인 기능을 사용하지 못한다는 점이다. 로그인 기능을 사용하려면 서버를 어느정도 다룰 줄 알아야하기에 제외했으나, 추후 프론트엔드에 익숙해진다면 백엔드 영역을 공부해 로그인을 확실히 구현하고 싶다.

Comments