본문 바로가기

웹_프론트_백엔드/프론트엔드

2021.04.03

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