"weather" ๋ react, redux, redux-middleware(thunk, saga)๋ฅผ ์ฌ์ฉํด kakao local REST API์ ๊ณต๊ณต ๋ฐ์ดํฐ ํฌํธ์์ ์ ๊ณต๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ํตํด ์ฌ์ฉ์์๊ฒ ์ฌ์ฉ์์ ํ์ฌ ์์น์ ๋ฐ๋ฅธ ๊ตญ๋ด์ ๋ ์จ ์ ๋ณด๋ฅผ ์๋ ค์ฃผ๋ ๋ ์จ ์ ๋ณด ์ฌ์ดํธ๋ก ์ฌ์ฉ์๋ ํ์ฌ์ ์์ ์ ์์น์ ๋ฐ๋ฅธ ํ ์์ ์ ๋ ์จ, ์์ผ๋ก 3์ผ ์ด๋ด์ ์๊ฐ๋ณ ๋ ์จ, ์ผ์ฃผ์ผ๊ฐ์ ์ ๊ตญ ๋ ์จ, ์ผ์ถ ์ผ๋ชฐ ์๊ฐ์ ์ ๊ณต๋ฐ์ ์ ์๋ค. weather์ ์น๋์์ธ์ ๋ค์ด๋ฒ ๋ ์จ๋ฅผ ์ฐธ๊ณ ํ๋ค.
weather ํ๋ก์ ํธ๋ ๋ค์๊ณผ ๊ฐ์ ์๋๋ค์ ํด๋ณด๊ธฐ ์ํด ์์๋ ํ๋ก์ ํธ์ด๋ค.
CORS ์ ์ฑ ์ ์ค์ํ๋ฉฐ ์ธ๋ถ์์ ์ ๊ณตํ๋ ๋ฐ์ดํฐ๋ฅผ REST FULํ๊ฒ ๋ฐ์์ค๊ธฐ
redux-toolkit , redux-middleware์ธ thunk ์ saga ์ ์ฐจ์ด์ ์ ๊ฒฝํํด๋ณด๊ณ ๊ฐ๊ฐ์ ์ฅ๋จ์ ๊ณผ ์ธ์ ์ฌ์ฉํด์ผํ๋์ง ์๊ฐํด๋ณด๊ธฐ
Github์ Actions ์ Secrets๋ฅผ ์ฌ์ฉํด API key ๋ฅผ ๊นํ์ ์ฌ๋ฆฌ์ง ์๊ณ ๋ ์ฌ์ฉํ ์ ์๊ฒ ํ๊ธฐ
table,th,td,tr์ ํ๊ทธ ์ฌ์ฉ์ ์ต์ํด์ง๊ธฐ
์๊ฐ๋ณ ๋ ์จ๋ฅผ ๊ทธ๋ํ๋ก ํํํด๋ณด๊ธฐ
rootState์ ํ์ํ ๋ฐ์ดํฐ์ ํ์์ ๋ด๊ฐ ์ง์ ๋ง๋ค์๋ ๋์๋ ๋ฌ๋ฆฌ ์ด๋ฒ weather ํ๋ก์ ํธ๋ ํ์ธ์ด ๋ง๋ ์ฌ๋ฌ ์ธ๋ถ ๋ฐ์ดํฐ๋ค์ ์ฌ์ฉํ๊ฒ ๋์๋ค.
๋ด๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ง๋ค๋ ์๋ ๋ฌ๋ฆฌ API ๋ง๋ค ๋ค๋ฅธ ๋ฐ์ดํฐ ์์ฒญ ์กฐ๊ฑด(ex: ์์1)๊ณผ ๋ฐ์ดํฐ ํ์์ ๋ถ์ํ๊ณ ๋ด๊ฐ ์ํ๋ ํ์์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ฌ๊ฐ๊ณตํ๋ ๊ณผ์ ์ ๊ฑฐ์ณ์ผํ๋ค.
//changeBaseDate : Date๋ฅผ string type ์ "yyyymmdd" ํํ๋ก ๋ฐ๊พธ์ด์ฃผ๋ ํจ์
// ๋ฐ์ดํฐ ์กฐํ ์๊ฐ์ ๋ฐ๋ผ baseDate์ ๊ธฐ์ค์ ์ค๋๋ก ํ ์ง ์ด์ ๋ก ํ ์ง๊ฐ ๋ฌ๋ผ์ง
const baseDate_today =changeBaseDate(today);
const baseDate_yesterday =getYesterDay(date);
// ํ๋์ํ๋ฅผ ์กฐํํ ๋ ํ์ํ baseDate
const baseDate_skyCode = minutes < 30 && hours === 0 ?
baseDate_yesterday
:
baseDate_today ;
//๋จ๊ธฐ์๋ณด ์กฐํ์ ํ์ํ baseDate
const baseDate_svf = hours < 2 ? baseDate_yesterday :baseDate_today;
navigator๋ฅผ ์ด์ฉํ ์ฌ์ฉ์์ ์๋, ๊ฒฝ๋๊ฐ์ด ๊ธฐ์์ฒญ์์ ์ ๊ณตํ ์์น ๋ฐ์ดํฐ ์๋ฃ์ ์ผ์นํ์ง ์์์ ์นด์นด์ค API๋ฅผ ํ์ฉํด ์ฌ์ฉ์์ ์์น๊ฐ์ ๋ฐ๋ฅธ ํ์ ๊ตฌ์ญ๋ช ์ ์ฐพ์๋ด๊ณ ์ด๋ฅผ ํตํด ๋ ์จ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ ๋ ํ์ํ ๊ฐ๋ค์ ๊ตฌํด์ผํ๋ค.
ํ๋ก์ ํธ๋ฅผ ๊นํ์ ๋ฐฐํฌํ๋ ๋ณด์์ ์ํด API Key๋ ๊นํ์ ๋ฐฐํฌํ์ง ์์ผ๋ฉด์๋ ๋ฐฐํฌ๋ ํ๋ก์ ํธ์์ API์ ์ ๊ทผํ ์ ์๋๋ก ํด์ผ ํ๋ค.
๊ทธ๋์ ์ ํํ ๋ฐฉ๋ฒ์ ๊ฐ๋ฐ ํ๊ฒฝ์์๋ .env์ ํ๊ฒฝ๋ณ์๋ก API Key๋ฅผ ๋ฑ๋กํ๊ณ ๋ฐฐํฌํ๊ฒฝ์์๋ Github Secrets์์ ์์ฑํ key๋ฅผ Github Actions๋ฅผ ํตํด ๋ฐฐํฌ ํ๊ฒฝ์์๋ ์ฌ์ฉํ ์ ์๋๋ก ํ๋ ๋ฐฉ๋ฒ์ด์๋ค.
์ด๋ฒ ํ๋ก์ ํธ๋ฅผ ํตํด Redux Toolkit,Thunk์ Saga๋ฅผ ์ฌ์ฉํด๋ณด๊ณ ์ด ์
์ ์ฅ๋จ์ ๊ณผ ์ฐจ์ด์ ์ ์์๋ณด๊ณ ์ถ์๋ค.
์ฝ๋์ ๊ฒฐ๊ณผ๋ฌธ์์๋ ์ฒด๊ฐ์ ์ ๋๊ปด์ง๋ ์ฐจ์ด๋ ์์๋ค.
position | weather | |
---|---|---|
tooklit(createAsycnThunk ์ฌ์ฉ) | toolkitPosition | toolkitWeather |
thunk | getPositionThunk | getWeatherThunk |
saga | getPositionSaga, positionSaga | getWeatherSaga, weatherSaga |
๊ทธ๋ฌ๋ action์ type์ ์์ด์๋ ์ฐจ์ด๊ฐ ์์๋ค.
action type์ ๋ค์์ 4๊ฐ์ง๋ก ์ค์ ํด์ ์์
์ ์งํํ๋ค.
Redux Middleware๋ createSlice์ ๋ฆฌ๋์์์ ์ ํ ๊ฒ๋ค์ ํ์ฉํ ์ ์์ง๋ง createAsynThunk ๋ ๋น๋๊ธฐ ์์ ์ ๋ฐ๋ผ "pending, fulfilled, rejected"์ ์ก์ ํ์ ์ ์๋์ผ๋ก ์์ฑํ๊ธฐ๋๋ฌธ์ ์ผ๋ฐ์ ์ธ ๋ฆฌ๋์์๋ ๋ค๋ฅด๊ฒ ์ฒ๋ฆฌํด์ฃผ์ด์ผํด์ extraReducers ์ ์ฌ์ฉํด ๋ฆฌ๋์๋ฅผ ์ฒ๋ฆฌํด์ฃผ์ด์ผํ๋ค.
export const weatherSlice =createSlice({
name:"weather",
initialState,
//thunk, saga
reducers :{
reset :(state)=>({
...noneState_weather
}),
request :(state , action:PayloadAction<PositionSuccessData>)=>({
...noneState_weather,
state:"pending"
}),
success :(state, action:PayloadAction<WeatherState>) =>({
...action.payload,
state:"success"
}),
failure : (state, action:PayloadAction< Error>)=>({
...noneState_weather,
state:"failure",
error:action.payload
})
},
//toolkitWeather
extraReducers(builder) {
builder
.addCase( toolkitWeather.pending , (state, action)=>{
return {
...noneState_weather,
state:"pending"
}
})
.addCase(toolkitWeather.fulfilled , (state, action)=>{
return{
...action.payload
}
})
.addCase(toolkitWeather.rejected, (state, action)=>{
return {
...noneState_weather,
state:"failure",
error:action.error as Error
}
})
}
});
๊ทธ๋ฆฌ๊ณ ๊ฐ์ weahterAction ์ด๋ผ๋ action์ ์ฌ์ฉํ์ง๋ง, dispatch์ ํ์ ์ ์ ๋ค ๋ค๋ฅด๊ฒ ํด์ค์ผํ๋ค.
//redux-thunk
const weatherThunkDispatch =useDispatch<ThunkDispatch<WeatherState, unknown,WeatherAction>>();
//redux-saga
const dispatch =useDispatch();
//redux-toolkit
const toolkitDispatch =useDispatch<ThunkDispatch<PositionState|WeatherState, CurrentPosition|PositionSuccessData, AnyAction>>();
if(startThunk.current){
// redux-thunk ์ด์ฉ
weatherThunkDispatch(getWeatherThunk(positionSuccessDate))
startThunk.current = false;
}else if(startSaga.current){
// redux-saga ์ด์ฉ
dispatch(weatherActions.request(positionSuccessDate));
startSaga.current =false;
}else{
// redux-toolkit ์ด์ฉ
startToolkit.current =false;
toolkitDispatch(toolkitWeather(positionSuccessDate))
}
๊ทธ๋ฆฌ๊ณ ์ด๋ฒ ํ๋ก์ ํธ๋ด์์ ์ฝ๋ ์คํ ๊ฒฐ๊ณผ์ ์์ด์ ๋ณ๋ค๋ฅธ ์ฐจ์ด์ ์ด ์์์ง๋ง Redux Toolkit์ Redux์ ํธํ ์ฌ์ฉ์ ์ํด ๋ง๋ค์ด์ก๊ณ , Redux Middleware๋ ๋น๋๊ธฐ ์์ ์ ๊ด๋ฆฌํ๋ ๊ฒ์ ๋ชฉ์ ์ ํ๊ธฐ ๋๋ฌธ์ ๋ณด๋ค ๋ณต์กํ ๋น๋๊ธฐ ์์ ์ ์์ด์๋ Redux Middleware๊ฐ ๋ ์ ํฉํ๋ค.
action์ reducer๋ฅผ ๊ฐํธํ๊ฒ ์์ฑํ ์ ์๋ ๋๊ตฌ๋ค๋ก typesafe-actions์ Redux Toolkit๋ฑ์ด ์๋ค. ํ๋ก์ ํธ๋ฅผ ํ๋ฉด์ ๋ ๋ค ์ฌ์ฉํด๋ดค๋๋ฐ, Redux Toolkit๋ง์ผ๋ก๋ ์ถฉ๋ถํด์ ๋ณ๋๋ก ์ค์นํด์ผํ๋ typesafe-actions ์ ์ฌ์ฉํ์ง ์๊ธฐ๋ ํ๋ค.
์๊ฐ๋ณ ๊ธฐ์จ์ ๋ํ ๊ทธ๋ํ๋ฅผ Chart.js๋ฅผ ์ฌ์ฉํด ๋ง๋ค์๋ค.
์ฒ์ ์ฌ์ฉํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์๋ค.
์ต์ ๊ธฐ์จ๊ณผ ์ต๋ ๊ธฐ์จ์ ๊ธฐ์ค์ผ๋ก ํ y์ถ, ์๊ฐ๋ณ x์ถ์ ํญ ๋ฑ ์๊ฐ๋ณด๋ค ์ ๋ง๋ค์๋ค.
ํ์ง๋ง ์์ฌ์๋ ์์๋ค.Chart.js๋ canvas์ ๊ทธ๋ํ ์ด๋ฏธ์ง๋ฅผ ๊ทธ๋ฆฌ๋ ๋ฐฉ์์ด๋ผ weather ํ๋ก์ ํธ์ฒ๋ผ x์ถ๊ณผ y์ถ์ ๊ธธ์ด๊ฐ ์ฐจ์ด๊ฐ ๋ง์ด ๋๋ฉด ๊ทธ๋ํ ์ด๋ฏธ์ง๊ฐ ๋์ด๋์ ๊ทธ๋ํ ์ ๊ณผ ๊ทธ๋ํ์ ์์น๋ฅผ ๋ํ๋ด๋ ๋ถ๋ถ์ด ๊นจ์ง๋ ๋ฌธ์ ์ ์ด ์์๋ค.
๊ทธ๋์ ๊ธฐ์จ์ ๋ํ๋ด๋ ์์น์ ๊ฒฝ์ฐ,Chart.js ๋ฅผ ์ด์ฉํ์ง ์๊ณ ๊ฐ ์๊ฐ๋๋ณ ์จ๋์ ๋ฐ๋ฅด y๊ฐ์ ๊ณ์ฐํ๋ ๋ฐฉ์์ผ๋ก ๋ฐ๋ก element๋ฅผ ์์ฑํด์ ํํํ๋ค.
๊ฐ๋ฐํ๊ฒฝ์์๋ ๊ด์ฐฎ์์ง๋ง ๋ฐฐํฌ ํ๊ฒฝ์์๋ API์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ฌ ๋ ์ฐจ๋จ๋จ(mixed-content) ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค.
mixed content(ํผํฉ ์ฝํ ์ธ )๋ HTTP ์ฝํ ์ธ ์ HTTPS ์ฝํ ์ธ ๊ฐ ํจ๊ป ๋ก๋๋๋ ๊ฒ์ผ๋ก ํฌ๋กฌ๊ณผ ํ์ด์ดํญ์ค๋ฑ ์น๋ธ๋ผ์ฐ์ ์์ ์์ ์์ ์ด์ ๋ก https ํ์ด์ง์์ HTTPS๊ฐ ์๋ HTTP ๋ก ๋ฐ์์ค๋ ๋ฆฌ์์ค๋ฅผ ์ฐจ๋จํ๊ณ ์๋ค.
weather ํ์ด์ง์์ mixed-content ์๋ฌ๊ฐ ๋ฐ์ํ ์ด์ ๋ weather ํ์ด์ง๋ HTTPS ํ๋กํ ์ฝ์ ์ฌ์ฉํ๋๋ฐ ์ผ์ถ์ผ๋ชฐ์ ๋ํ ๋ฐ์ดํฐ์ ๋ฏธ์ธ๋จผ์ง,์ด๋ฏธ์ธ๋จผ์ง์ ๋ํ ๋ฐ์ดํฐ๋ http์์ ์ ๊ณตํ๋ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ด์๋ค.
โ API๋ฅผ HTTP๊ฐ ์๋ HTTPS ํ๋กํ ์ฝ์ ์ฌ์ฉํ๋๋ก API ์ ๊ณต์๊ฐ ์์
โก ๋ฐฑ์๋์์ API ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ ํ๋ก ํธ์๋์ ๋๊ธฐ๊ธฐ
โข META ํ๊ทธ๋ฅผ ํค๋์ ์ถ๊ฐํ์ฌ, http ์ฝํ
์ธ ๋ฅผ ์๋์ผ๋ก https๋ก ๋ณํํ์ฌ ๋ก๋ฉํ๊ธฐ
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
C-โ ์ API ์ ๊ณต์๊ฐ ํด์ผํ๋ ๋ฐฉ๋ฒ์ผ๋ก ๋ด๊ฐ ํ ์ ์๊ณ , C-โข์ ์๋ํ์ง๋ง ์ค๋ฅ๊ฐ ํด๊ฒฐ๋์ง๋ ์์๋ค. ๊ทธ๋์ C-โก์ ๋ฐฉ๋ฒ์ผ๋ก ์ค๋ฅ๋ฅผ ํด๊ฒฐํด์ผํ๋ค๋ ๊ฒฐ๋ก ์ ๋ด๋ ธ๊ณ Node.js๋ฅผ ๊ณต๋ถํด์ผ ํ๋ ํ์์ฑ์ ๋ ๋๋ผ๊ฒ ๋์๋ค.
mixed-content๋ฅผ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ์ํด Node.js ์ express ๋ชจ๋๋ฅผ ์ด์ฉํด node ์๋ฒ๋ฅผ ๋ง๋ค๊ณ ์๋ฒ์์ ์ธ๋ถ api์ ์์ฒญ์ ๋ณด๋ด ๋ ์จ ์ ๋ณด์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์จ ํ ์ด๋ฅผ ํ๋ก ํธ์ ๋๊ฒจ์ฃผ๋ ๋ฐฉ์์ mixed-content ์ค๋ฅ๋ฅผ ํด๊ฒฐํ๋ค.
๐ฑ๏ธ"Node.js๋ฅผ ํตํด mixed-content ์ค๋ฅ ํด๊ฒฐ" ์ ๋ํ ๋ธ๋ก๊ทธ ๊ธ ๋ณด๋ฌ๊ฐ๊ธฐ
๐ฉโ๐ป"React + Node(express)์ฐ๋"์ ๋ํ ๊ธ ๋ณด๋ฌ๊ฐ๊ธฐ
๐ฉโ๐ป"React์ Node.js ์ฐ๋ ์ 404 error"์ ๋ํ ๊ธ ๋ณด๋ฌ๊ฐ๊ธฐ
๐ฉโ๐ป"Node.js๋ฅผ ํตํด mixed-content ์ค๋ฅ ํด๊ฒฐ"์ ๋ํ ๋ธ๋ก๊ทธ ๊ธ ๋ณด๋ฌ๊ฐ๊ธฐ
6)์ mixed-content ์ค๋ฅ๋ฅผ ๋ง๋๋ฉด์ ์ธ์ ๊ฐ ํ๊ฒ ์ง๋ผ๋ฉด ์๊ฐํ Node.js์ ๋ํ ๊ณต๋ถ๋ฅผ ์์ํ๋ค. Node.js๋ฅผ ๊ณต๋ถํ๋ฉด์ ์๋ฒ์์ React ํ๋ก์ ํธ๋ฅผ ์ด๋ป๊ฒ ์ด ์ ์์ ๊ฒ์ธ์ง, ์๋ฒ์ ํ๋ก ํธ์ํธ๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ์ฃผ๊ณ ๋ฐ์ ์ ์์์ง, ๊ทธ ๊ณผ์ ์์ ๋น๋๊ธฐ๋ฅผ ์ด๋ป๊ฒ ํด๊ฒฐํ ๊ฒ์ธ์ง๋ฅผ ์ค์ ์ ์ผ๋ก ๋ดค๋ค.
๊ทธ๋ฆฌ๊ณ Node์๋ฒ๋ฅผ ํตํด React๋ฅผ ์ด์์ ๋, css์ ํ์ผ์ ์์ฑ๋ค์ ์์๊ฐ ๋ณ๊ฒฝ๋๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์๋ค. ์ด๋ฅผ ํตํด css ์์ฑ์ ์์ฑํ ๋ ํด๋น ์ฐ์ ์์๊ฐ ์ ์ฉ๋์ด์ผํ๋ ๊ฒฝ์ฐ๋ผ๋ฉด ๋ค์ ์ฝ๋๋ฅผ ์ถ๊ฐํ๋ ๊ฒ๋ณด๋ค ํ๋์ ์ฝ๋๋ฅผ ๋ณด๋ค ์ ํํ๊ฒ ์ค์ ํ๋ ๊ฒ ์ข๊ฒ ๋ค๋ ๊ฒฝํ๋ ํ ์ ์์๋ค.
.bar{
//...
border : 7px solid $bar_bg;
border-bottom:7px solid $bar_percent_bg;
border-right: 7px solid $bar_percent_bg;
}
border-bottom์ด border ๋ณด๋ค ์ฐ์ ์ ์ฉ๋๋๋ก ์ฝ๋๋ฅผ ์์ฑํ๋๋ฐ Node ์๋ฒ๋ฅผ ์คํํ๋ฉด border-bottom ํ์ border๊ฐ ๋์์ border-bottom์ด ์๋ border๊ฐ ์ฐ์ ์คํ๋๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค.
๊ทธ๋์ ์ผ๋ชฐ ์ํฉ์ ํํํ๋ ๊ทธ๋ํ๊ฐ ์ค๊ฐ์ ๋๊ธฐ๋ ๋ฌธ์ ๊ฐ ์๊ฒผ๋ค.
.bar{
border-style:solid;
border-width: 7px;
border-color: $bar_bg $bar_percent_bg $bar_percent_bg $bar_bg;
}
์๋์ ์ฌ์ง์ฒ๋ผ, ์๋ ๊ณํํ ๋ฒ์ ์๋ ์ ๊ตญ์ ๋ ์จ๋ฅผ ์ผ์ฃผ์ผ๋ณ๋ก ์๋ ค์ฃผ๋ ๊ธฐ๋ฅ์ด ์์์ง๋ง ๋ฐฐํฌ๋ ์ต์ข
๋ฒ์ ์๋ ํด๋น ๊ธฐ๋ฅ์ ์ญ์ ํ๋ค.
์ด์ ๋ ETIMEDOUT ์๋ฌ ๋๋ฌธ์ด์๋ค.
ETIMEDOUT์ ์ฐ๊ฒฐ ์๊ฐ ์ด๊ณผ์ค๋ฅ๋ก, ๋ก์ปฌ ํ๊ฒฝ์์๋ ์ ๊ตญ ๋ ์จ์ ๋ํ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ฌ ์ ์์์ง๋ง, cloudeType์ผ๋ก Node์ React๋ฅผ ๋ฐฐํฌํ ๋ฐฐํฌ์ฌ์ดํธ์์๋ ETIMEDOUT ์ค๋ฅ๊ฐ ๋ํ๋ฌ๋ค.
ํด๋น ์๋ฌ์ ์์ธ์ ๋ค์๊ณผ ๊ฐ๋ค.
๊ณต๊ณต API๋ฅผ ๋ฐ์์ค๋ ๊ฒ์๋ ๋ฌธ์ ๊ฐ ์์๊ณ , ์ ๊ตญ ์๋๋ณ ์ผ์ฃผ์ผ ๋ฐ์ดํฐ๋ฅผ ๊ณต๊ณตAPI์์ ์ ๊ณตํด์ฃผ๋๊ฒ ์๋๋ผ ์ฌ๋ฌ ๋ฐ์ดํฐ๋ค(ํ๋์ํ,์ด๋จ๊ธฐ์๋ณด,๋จ๊ธฐ์๋ณด,์ค๊ธฐ์๋ณด๋ฑ๋ฑ)์ ์๋๋ณ๋ก ์ผ์ฃผ์ผ์น ๋ฐ์์์ ๋ฐ์์ค๋ค๋ณด๋, ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋ ์๊ฐ์ด ํน์ ์๊ฐ์ ๋๋ค๋ณด๋ "ETIMEDOUT" ์ค๋ฅ๊ฐ ์๊ธด๊ฒ ๊ฐ๋ค.
๋ค๋ฅธ ๋ฐฐํฌ ์ฌ์ดํธ๋ฅผ ์ด์ฉํ ์ ๋ ์์ง๋ง, ๋ก์ปฌ ํ๊ฒฝ์์๋ ์ ๊ตญ ๋ ์จ๋ฅผ ๋ฐ์์ค๋ ์์ ์ผ๋ก ์ธํด ๋ก๋ฉ ์๊ฐ์ด 5๋ถ์ ๋ ๊ฑธ๋ ค์ ์ฌ์ฉ์ ๊ฒฝํ์ธก๋ฉด์์ ์ข์ง ๋ชปํ๋ค๊ณ ์๊ฐํด์ ์ ๊ตญ ๋ ์จ๋ฅผ ์๋ ค์ฃผ๋ ๊ธฐ๋ฅ์ ์ ์ธํ๊ธฐ๋ ํ๋ค.
twitter์ notion์ ํด๋ก ์ฝ๋ฉํ๋ ํ๋ก์ ํธ๋ค์ ํผ์ ์ฒ์๋ถํฐ ๋ชจ๋ ๊ฒ์ ๋ถ๋ช์น๋ฉด์ ํ๋ค๋ณด๋ ๋ช๋ฌ ์ฉ ์๊ฐ์ด ๊ฑธ๋ ธ๊ณ ์ธ์ ์ด๊ฑธ ๋๋ผ ์ ์์๊นํ๋ฉฐ ๊ณ์๋๋ ์ค๋ฅ์ ์์ ์ ํ๋ค์๋ค. ์ด์ ์ธ์ด๋ฅผ ๋ง ๋ฐฐ์ด ๋ณ์๋ฆฌ ์์ค์์ ๋๋ฌด ํธ๊ธฐ๋กญ๊ฒ ๊ฑฐ๋ํ ํ๋ก์ ํธ๋ฅผ ๊ดํ ์์ํ ๊ฑฐ ์๋๊นํ๋ ์๊ฐ๋ ๋ค์๋๋ฐ weather ํ๋ก์ ํธ๋ฅผ ํ๋ฉด์ ๊ทธ๋๋ ์์ ํ๋ก์ ํธ๋ฅผ ํ๋ฉด์ ๋ฐฐ์ฐ๊ณ ์ฒด๋ํ ๊ฒ๋ค์ด ์ด๋์ ๋๋ ๋ด๊ฒ์ด ๋์๋ค๋ ๊ฒ์ ํ์ธํ ์ ์๋ ์๊ฐ์ด์๋ค.
์์ ๋ ํ๋ก์ ํธ์ ๋นํด์ ํ๋ก์ ํธ์ ๋ณต์ก๋๋ ํฌ๊ธฐ๊ฐ ํฌ์ง๋ ์์ง๋ง, ๊ทธ๋์ ๊ถ๊ธํ๊ณ ํด๊ฒฐํ๊ณ ์ถ์๋ ์ธ๋ถ API ์ด์ฉ, Redux Middleware์ Redux Toolkit์ฌ์ฉ, Node ์๋ฒ์ React ์ฐ๋ํ๊ธฐ ๋ฑ์ ํด๋ดค๊ณ ์ฌ๋ฌ ์ค๋ฅ๋ค์ ์์ ํ๊ณ "๋ค์ด๋ฒ ๋ ์จ"์์ ์ฌ์ฉํ ์น๋์์ธ๊ณผ class, css ์์๋ค์ ๊ตฌ๊ฒฝํ๋ฉด์ ์๋ก์ด ๊ฒ๋ค์ ์์๊ฐ๊ธฐ๋ ํ ๋ฟ๋ฏํ ๊ฒฝํ์ด์๋ค.
๋๋ฌด๋๋ฌด ์ ๋ดค์ต๋๋ค! ์กด๊ฒฝ์ฌ์ด ๋๋๋ฐ์
๋ ์จ ๊ณต๊ณต๋ฐ์ดํฐ ์ฌ์ฉ๊ณผ ๊ด๋ จํด์ ์ฌ์ญค๋ณด๊ณ ์ถ์๊ฒ ์๋๋ฐ ๋ฐ๋ฌ ์ฐ๋ฝ๋๋ฆด ๋ฐฉ๋ฒ์ด ์์๊น์?