1. useReducer
: 상태를 업데이트 로직을 분리할 때 사용
└ state : 컴포넌트에서 앞으로 사용할 수 있는 상태
└ dispatch : 액션을 발생시키는 함수
2. 저번시간에 만든 할일 메모장 만들기에서 Context API를 활용한 상태관리 추가
** /components/todoContext.js
import React, {useReducer, createContext, useContext, useRef} from 'react';
const initialTodos = [
{
id: 1,
text: '프론트엔드 프로젝트 만들기',
done: false
},
{
id: 2,
text: '밥 잘 챙겨먹기',
done: true
},
{
id: 3,
text: '운동하기',
done: true
},
{
id: 4,
text: '일기쓰기',
done: true
}
];
function todoReducer(state, action) {
switch(action.type) {
case 'CREATE':
return state.concat(action.todo);
case 'TOGGLE':
return state.map(todo => todo.id === action.id ? {...todo, done: !todo.done} : todo);
case 'REMOVE':
return state.filter(todo => todo.id !== action.id);
default:
throw new Error(`${action.type}이 잘못 전달됨`);
}
}
const TodoStateContext = createContext();
const TodoDispatchContext = createContext();
const TodoNextIdContext = createContext();
export function TodoProvider({ children }) {
const [state, dispatch] = useReducer(todoReducer, initialTodos);
const nextId = useRef(5);
return (
<TodoStateContext.Provider value={state}>
<TodoDispatchContext.Provider value={dispatch}>
<TodoNextIdContext.Provider value={nextId}>
{children}
</TodoNextIdContext.Provider>
</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
);
}
export function useTodoState() {
const context = useContext(TodoStateContext);
if(!context) {
throw new Error('TodoProvider를 찾을 수 없음');
}
return context;
}
export function useTodoDispatch() {
const context = useContext(TodoDispatchContext);
if(!context) {
throw new Error('TodoProvider를 찾을 수 없음!');
}
return context;
}
export function useTodoNextId() {
const context = useContext(TodoNextIdContext);
if(!context) {
throw new Error('TodoProvider를 찾을 수 없음!');
}
return context;
}
** app.jsx
import React from 'react';
import {createGlobalStyle} from 'styled-components';
import TodoTemplate from './components/todoTemplate';
import TodoHead from './components/todoHead';
import TodoList from './components/todoList';
import TodoCreate from './components/todoCreate';
import {TodoProvider} from './todoContext';
const GlobalStyle = createGlobalStyle`
body {
background-color: #e9ecef;
}
`
function App() {
return (
<>
<TodoProvider>
<GlobalStyle/>
<TodoTemplate>
<TodoHead/>
<TodoList/>
<TodoCreate/>
</TodoTemplate>
</TodoProvider>
</>
);
}
export default App;
** /components/todoHead.jsx
import React from 'react';
import styled from 'styled-components';
import {useTodoState} from '../todoContext';
const TodoHeadBlock = styled.div`
padding: 48px 32px 24px 32px;
border-botton: 1px solid #e9ecef;
h1 {
magin: 0;
font-size: 36px;
color: #343a40;
}
.day {
margin-top: 4px;
color: #868e96;
font-size: 21px
}
.tasks-left {
color: #20c997;
font-size: 18px;
margin-top: 40px;
font-weight: bold;
}
`;
function TodoHead() {
const todos = useTodoState();
const undoneTask = todos.filter(todo => !todo.done);
const today = new Date();
const dateString = today.toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
const dayName = today.toLocaleDateString('ko-KR', {weekday: 'long'});
return (
<TodoHeadBlock>
<h1>{dateString}</h1>
<div className="day">{dayName}</div>
<div className="tasks-left">할 일 {undoneTask.length}개 남음</div>
</TodoHeadBlock>
);
}
export default TodoHead;
** /components/todoList.jsx
import React from 'react';
import styled from 'styled-components';
import TodoItem from './todoItem';
import {useTodoState} from '../todoContext';
const TodoListBlock = styled.div`
flex: 1;
padding: 20px 32px;
padding-bottom: 48px;
overflow-y: auto;
`;
function TodoList() {
const todos = useTodoState();
return (
<TodoListBlock>
{todos.map(todo => (
<TodoItem
key={todo.id}
id={todo.id}
text={todo.text}
done={todo.done}
/>
))}
</TodoListBlock>
);
}
export default TodoList;
** /components/todoItem.jsx
import React from 'react';
import styled, {css} from 'styled-components';
import {MdDone, MdDelete} from 'react-icons/md';
import {useTodoDispatch} from '../todoContext';
const Remove = styled.div`
display: flex;
align-items: center;
justify-content: center;
color: #dee2e6;
font-size: 24px;
cursor: pointer;
&:hover {
color: #ff6b6b;
}
display: none;
`;
const TodoItemBlock = styled.div`
display: flex;
align-items: center;
padding-top: 12px;
padding-botton: 12px;
&:hover {
${Remove} {
display: block;
}
}
`;
const CheckCircle = styled.div`
width: 32px;
height: 32px;
border-radius: 16px;
border: 1px solid #ced4da;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20px;
cursor: pointer;
${props => props.done && css`
border: 1px solid #38d9a9;
`}
`;
const Text = styled.div`
flex: 1;
font-size: 21px;
color: #495057;
${props => props.done && css`
color: #ced4da;
`}
`;
function TodoItem({id, done, text}) {
const dispatch = useTodoDispatch();
const onToggle = () => dispatch({type: 'TOGGLE', id});
const onRemove = () => dispatch({type: 'REMOVE', id});
return (
<TodoItemBlock>
<CheckCircle done={done} onClick={onToggle}>{done && <MdDone/>}</CheckCircle>
<Text done={done}>{text}</Text>
<Remove onClick={onRemove}>
<MdDelete/>
</Remove>
</TodoItemBlock>
);
}
export default React.memo(TodoItem);
** /components/todoCreate.jsx
import React, {useState} from 'react';
import styled, {css} from 'styled-components';
import {MdAdd} from 'react-icons/md';
import {useTodoDispatch, useTodoNextId} from '../todoContext';
const CircleButton = styled.button`
background-color: #39d9a9;
&:hover {
background-color: #63e6be;
}
&:active {
background: #20c997;
}
z-index: 5;
cursor: pointer;
width: 80px;
height: 80px;
display: flex;
align-items: center;
jusify-content: center;
font-size: 60px;
position: absolute;
left: 50%;
bottom: 0px;
transform: translate(-50%, 50%);
color: white;
border-radius: 50%;
border: none;
outline: none;
transition: 0.2s all ease-in;
${props => props.open && css`
background-color: #ff6b6b;
&:hover {
background: #ff8787;
}
&:active {
background: #fa5252;
}
transform: translate(-50%, 50%) rotate(45deg);
`}
`;
const InsertFormPositioner = styled.div`
width: 100%;
bottom: 0;
left: 0;
position: absolute;
`;
const InsertForm = styled.form`
background: #f8f9fa;
padding: 32px 32px 72px 32px;
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
border-top: 1px solid #e9ecef;
`;
const Input = styled.input`
padding: 12px;
border-radius: 4px;
border: 1px solid #dee2e6;
width: 100%;
outline: none;
font-size: 18px;
box-sizing: border-box;
`;
function TodoCreate() {
const [open, setOpen] = useState(false);
const [value, setValue] = useState('');
const dispatch = useTodoDispatch();
const nextId = useTodoNextId();
const onToggle = () => setOpen(!open);
const onChange = e => setValue(e.target.value);
const onSubmit = e => {
e.preventDefault(); // 서버로 데이터를 전송하는 기능을 막은(새로고침도 불가!)
dispatch({
type: 'CREATE',
todo: {
id: nextId.current,
text: value,
done: false
}
});
setValue('');
setOpen(false);
nextId.current += 1;
}
return (
<>
{open && (
<InsertFormPositioner>
<InsertForm onSubmit={onSubmit}>
<Input autoFocus placeholder="할 일을 입력 후 Enter를 누르세요"
onChange={onChange}
value={value}
/>
</InsertForm>
</InsertFormPositioner>
)}
<CircleButton onClick={onToggle} open={open}>
<MdAdd/>
</CircleButton>
</>
);
}
export default React.memo(TodoCreate);
3. axios 모듈
: GET, PUT, POST, DELETE 등의 메소드로 API 요청을 한다.
"yarn add axios"
** API 연동 실습
https://jsonplaceholder.typicode.com
** API
https://jsonplaceholder.typicode.com/users
** /components/users.jsx
import React, {useState} from 'react';
import axios from 'axios';
import useAsync from '../useAsync';
import User from './user'
// useAsync에서 Promise의 결과 바로 data에 담기 때문에,
// 요청을 한 이유 response에서 data 추출하여 반환하는 함수를 따로 만듦
async function getUsers() {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
return response.data;
}
function Users() {
const [userId, setUserId] = useState(null)
const [state, refetch] = useAsync(getUsers, [], true);
const {loading, data: users, error} = state;
if(loading) return <div>로딩중 ... </div>
if(error) return <div>에러가 발생!!</div>
if(!users) return <button onClick={refetch}>불러오기</button>;
return (
<>
<ul>
{users.map(user => (
<li key={user.id} onClick={() => setUserId(user.id)} style={{cursor: 'pointer'}}>
{user.username} ({user.name})
</li>
))}
</ul>
<button onClick={refetch}>다시 불러오기</button>
{userId && <User id={userId}/>}
</>
)
}
export default Users;
** app.jsx
import React from 'react';
import Users from './components/users';
function App() {
return <Users/>
}
export default App;
** /components/useAsync.js
import {useReducer, useEffect} from 'react';
function reducer(state, action) {
switch(action.type) {
case 'LOADING':
return {
loading: true,
data: null,
error: null
}
case 'SUCCESS':
return {
loading: false,
data: action.data,
error: null
}
case 'ERROR':
return {
loading: false,
data: null,
error: action.error
}
default:
throw new Error('${action.type} 예외발생!')
}
}
function useAsync(callback, deps=[], skip=false) {
const [state, dispatch] = useReducer(reducer, {
loading: false,
data: null,
error: null
});
const fetchData = async () => {
dispatch({type: 'LOADING'});
try {
const data = await callback();
dispatch({type: 'SUCCESS', data})
} catch(e) {
dispatch({type:'ERROR', error: e});
}
}
useEffect(() => {
if(skip) return;
fetchData();
}, deps);
return [state, fetchData]
}
export default useAsync;
** use.jsx
import React from 'react';
import axios from 'axios';
import useAsync from '../useAsync';
// useAsync에서 Promise의 결과 바로 data에 담기 때문에,
// 요청을 한 이유 response에서 data 추출하여 반환하는 함수를 따로 만듦
async function getUser(id) {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/users/${id}`
);
return response.data;
}
function User({id}) {
const [state] = useAsync(() => getUser(id), [id]);
const {loading, data: user, error} = state;
if(loading) return <div>로딩중 ... </div>
if(error) return <div>에러가 발생!!</div>
if(!user) return null;
return (
<div>
<h2>{user.username}</h2>
<p>
<b>Email : </b> {user.email}
</p>
</div>
)
}
export default User;
4. 라우터 적용
: react-router-dom
라우터 관련 함수를 내장
"yarn add react-router-dom"
** 특정 주소에 컴포넌트 연결하는 방법
<Route path="주소" component={보여줄 컴포넌트}>
** index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom'
import App from './app';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
** about.jsx
import React from 'react';
const About = () => {
return (
<div>
<h1>소개</h1>
<p>리액트 라우터를 공부하고 있어요.</p>
</div>
)
}
export default About;
** home.jsx
import React from 'react';
const Home = () => {
return (
<div>
<h1>홈</h1>
<p>안녕하세요. 샤인 웹 사이트에 오신 걸 환영합니다..!</p>
</div>
)
}
export default Home;
** app.jsx
import React from 'react'
import {Route} from 'react-router-dom';
import Home from './home';
import About from './about';
const App = () => {
return (
<div>
<Route path="/" exact={true} component={Home}/>
<Route path="/about" component={About}/>
</div>
)
}
export default App;
'웹_프론트_백엔드 > 프론트엔드' 카테고리의 다른 글
2021.04.07(보충) (0) | 2021.05.25 |
---|---|
2021.04.04 (0) | 2021.05.24 |
2021.03.28 (0) | 2021.05.16 |
2021.03.27 (0) | 2021.05.14 |
2021.03.21 (0) | 2021.05.12 |