[리액트 X 타입스크립트] redux-toolkit으로 간단한 날씨 어플리케이션 만들어보기
안녕하세요. 이번시간에는 리액트와 타입스크립트 조합으로 redux-toolkit을 이용하여 날씨 어플리케이션을 만들어보겠습니다.
구현 레이아웃
개발 순서 및 코드
- 원하시는 레이아웃을 만들어줍니다.
- 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>℉</sup>
</p>
<p className="mb-2">
{celsius}
<sup>℃</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
에 올라가있습니다!
감사합니다 :)
Author And Source
이 문제에 관하여([리액트 X 타입스크립트] redux-toolkit으로 간단한 날씨 어플리케이션 만들어보기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@hoon_dev/리액트-X-타입스크립트-redux-toolkit으로-간단한-날씨-어플리케이션-만들어보기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)