[리액트 X 타입스크립트] redux-toolkit으로 간단한 날씨 어플리케이션 만들어보기

hoonie·2021년 8월 5일
0
post-thumbnail

안녕하세요. 이번시간에는 리액트와 타입스크립트 조합으로 redux-toolkit을 이용하여 날씨 어플리케이션을 만들어보겠습니다.

구현 레이아웃


개발 순서 및 코드

  1. 원하시는 레이아웃을 만들어줍니다.
  2. redux-toolkit 사용을 위한 폴더 구조 및 설정을 진행합니다
    • src 안에 redux 폴더를 만들어줍니다.
    • redux 폴더 안에 configStore.ts와 modules 폴더를 만들어줍니다
    • modules 폴더 안에 weather.ts를 만들어줍니다.
    • index.ts 안에ㅔ Provider를 선언하여 store를 넣어줍니다.
    • 각 파일안에 다음과 같이 코드를 작성합니다.
// weather.ts

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { RootState } from "../configStore";
import axios from "axios";

export interface Weather {
  description: string;
  icon: string;
  id: number;
  main: string;
}
export interface WeatherData {
  base: string;
  clouds: {
    all: number;
  };
  cod: number;
  coord: {
    lon: number;
    lat: number;
  };
  dt: number;
  id: number;
  main: {
    feels_like: number;
    humidity: number;
    pressure: number;
    temp: number;
    temp_max: number;
    temp_min: number;
  };
  name: string;
  sys: {
    country: string;
    id: number;
    sunrise: number;
    sunset: number;
    type: number;
  };
  timezone: number;
  visibility: number;
  weather: Weather[];
  wind: {
    speed: number;
    deg: number;
  };
}

export interface WeatherState {
  data: WeatherData | null;
  loading: boolean;
  error: boolean;
}

const initialState: WeatherState = {
  data: null,
  loading: false,
  error: false,
};

export const getWeatherThunk = createAsyncThunk(
  "weather/getWeather",
  async (city: string): Promise<WeatherData> => {
    const response = await axios.get(
      `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${process.env.REACT_APP_API_KEY}`
    );
    return response.data;
  }
);

const weatherSlice = createSlice({
  name: "weather",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder.addCase(getWeatherThunk.pending, (state, action) => {
      state.loading = true;
      state.error = false;
    });
    builder.addCase(getWeatherThunk.fulfilled, (state, action) => {
      state.data = action.payload;
      state.loading = false;
      state.error = false;
    });
    builder.addCase(getWeatherThunk.rejected, (state, action) => {
      state.error = true;
      state.loading = false;
    });
  },
});

export const getWeather = (state: RootState) => state.weather;
export default weatherSlice.reducer;
 //configStore.ts
 
import { configureStore } from "@reduxjs/toolkit";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import weather from "./modules/weather";
const store = configureStore({
  reducer: { weather },
});
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>(); // Export a hook that can be reused to resolve types

export type RootState = ReturnType<typeof store.getState>;
export const useAppSelect: TypedUseSelectorHook<RootState> = useSelector;

export default store;
//index.ts

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Provider } from "react-redux";
import store from "./redux/configStore";
import "../node_modules/bulma/css/bulma.min.css";
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

컴포넌트별 코드

1. Search.tsx


import React, { useState, FormEvent } from "react";
import { useAppDispatch } from "../redux/configStore";
import { getWeatherThunk } from "../redux/modules/weather";
interface SearchProps {
  title: string;
}
const Search: React.FC<SearchProps> = ({ title }) => {
  const dispatch = useAppDispatch();
  const [city, setCity] = useState<string>("");
  
  //input change 이벤트 발생시 city State 설정
  const handleChange = (e: FormEvent<HTMLInputElement>) => {
    setCity(e.currentTarget.value);
  };
  
  //form 버튼 클릭시 선언한 createAsyncThunk 함수 실행
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (city.trim() === "") {
      alert("도시를 입력하주세요");
    }
    dispatch(getWeatherThunk(city));
    setCity("");
  };
  
  return (
    <div className="hero is-lign has-text-centered">
      <div className="hero-body">
        <div className="container">
          <h1 className="title">{title}</h1>
          <form action="" className="py-5" onSubmit={handleSubmit}>
            <input
              type="text"
              className="input has-text-centered mb-2"
              placeholder="도시명을 입력하세요"
              style={{ maxWidth: 300 }}
              value={city}
              onChange={handleChange}
            />
            <button
              type="submit"
              className="button is-primary is-fullWidth"
              style={{ maxWidth: 300, margin: "0 auto" }}
            >
              Search
            </button>
          </form>
        </div>
      </div>
    </div>
  );
};

export default Search;

2. Weather.tsx


import React from "react";
import { WeatherData } from "../redux/modules/weather";

interface WeatherProps {
  data: WeatherData;
}

const Weather: React.FC<WeatherProps> = ({ data }) => {
  const fahrenheit = (data.main.temp * 1.8 - 459.67).toFixed(2);
  const celsius = (data.main.temp - 273 - 15).toFixed(2);
  return (
    <section className="section">
      <div className="container">
        <h1 className="title has-text-centered" style={{ marginBottom: 50 }}>
          {data.name} - {data.sys.country}
        </h1>
        <div className="level" style={{ alignItems: "flex-start" }}>
          <div className="level-item has-text-centered">
            <div>
              <p className="heading">{data.weather[0].description}</p>
              <div className="title">
                <img
                  src={`http://openweathermap.org/img/wn/${data.weather[0].icon}.png`}
                  alt=""
                />
              </div>
            </div>
          </div>
          <div className="level-item has text centered">
            <div>
              <p className="heading">temp</p>
              <div className="title">
                <p className="mb-2">{data.main.temp}K</p>
                <p className="mb-2">
                  {fahrenheit}
                  <sup>&#8457;</sup>
                </p>
                <p className="mb-2">
                  {celsius}
                  <sup>&#8451;</sup>
                </p>
              </div>
            </div>
          </div>
          <div className="level-item has-text-centered">
            <div>
              <p className="heading">pressure</p>
              <p className="title">{data.main.pressure}</p>
            </div>
          </div>
          <div className="level-item has-text-centered">
            <div>
              <p className="heading">wind</p>
              <p className="title">{data.wind.speed} m/s</p>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
};

export default Weather;

3. App.tsx


import React, { FC } from "react";
import "./App.css";
import Search from "./components/Search";
import Weather from "./components/Weather";
import { useAppSelect } from "./redux/configStore";
import { getWeather } from "./redux/modules/weather";
import LinearProgress from "@material-ui/core/LinearProgress";
import Alert from "@material-ui/lab/Alert";

const App: FC = () => {
  const weather = useAppSelect(getWeather);
  const weatherData = weather.data;
  const weatherLoading = weather.loading;
  const weatherError = weather.error;
  return (
    <div className="App" style={{ maxWidth: "1024px", margin: "0 auto" }}>
      <Search title="간단한 날씨 웹어플리케이션"></Search>
      {weatherError && (
        <Alert severity="error" style={{ width: "100%" }}>
          잘못된 도시명을 입력하셨습니다. 다시 입력해주세요.
        </Alert>
      )}
      //로딩중일때 로딩 컴포넌트 등장
      {weatherLoading ? (
        <LinearProgress style={{ width: "100%" }} />
      ) : (
        weatherData !== null && <Weather data={weatherData}></Weather>
      )}
    </div>
  );
};

export default App;

총평

타입스크립트를 공부하면서 리액트와의 조합을 계속 연습하고 있는데
간단하게 redux-toolkit 사용법을 익히면서 개발하기 좋았던거 같습니다!

해당 코드는
https://github.com/zlzlzlmo/weather_app_typescript
에 올라가있습니다!

감사합니다 :)

0개의 댓글