📝 DAY 02 - 220519 (2)
- 헤더 컴포넌트 생성
- 로그인 유지
- 로그아웃
Responsive
컴포넌트를 생성.components/common/Responsive.js
import styled from 'styled-components';
const ResponsiveBlock = styled.div`
padding-left: 1rem;
padding-right: 1rem;
width: 1024px;
margin: 0 auto;
@media screen and (max-width: 1024px) {
width: 768px;
}
@media screen and (max-width: 768px) {
width: 100%;
}
`;
const Responsive = ({ children, ...rest }) => {
return <ResponsiveBlock {...rest}>{children}</ResponsiveBlock>;
};
export default Responsive;
components/common/Header.js 생성
import styled from 'styled-components';
import Responsive from './Responsive';
import Button from './Button';
const HeaderBlock = styled.div`
position: fixed;
width: 100%;
background: #fff;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
`;
const Wrapper = styled(Responsive)`
height: 4rem;
display: flex;
align-items: center;
justify-content: space-between;
.logo {
font-size: 1.2rem;
font-weight: 800;
letter-spacing: 0.1em;
}
.right {
display: flex;
align-items: center;
}
`;
// 헤더 높이만큼 콘텐츠를 내려야함. (position: fixed이므로)
const Spacer = styled.div`
height: 4rem;
`;
const Header = () => {
return (
<>
<HeaderBlock>
<Wrapper>
<div className="logo">BLOGROW</div>
<div className="right">
<Button>로그인</Button>
</div>
</Wrapper>
</HeaderBlock>
<Spacer />
</>
);
};
export default Header;
position: fixed로 헤더를 고정시킴.
-> 컨텐츠 부분을 헤더 높이만큼 내려줘야 함.
-> height: 4rem인 div를 만들어서 끼워넣기.
이제 여기에서 로그인 버튼 클릭시 /login
경로로 이동해야 함.
-> 원래대로라면 Link 컴포넌트로 만들면 됨.
방법 1. Button 컴포넌트에서
withRouter
사용.
- react-router-dom의 withRouter 함수 사용.
-> 현재 react-router-dom v6로 가면서 withRouter
가 없어짐.
대신해서 withLocation을 만들어서 아래와 같이 사용 가능.
import { useLocation } from "react-router-dom";
const withLocation = Component => props => {
const location = useLocation();
return <Component {...props} location={location} />;
};
export default withLocation( MyComponent)
방법 2. Link 컴포넌트를 직접 사용함.
->styled(Link)
로 스타일링 똑같이 해주기.
components/common/Button.js 수정
import styled, { css } from 'styled-components';
import { Link } from 'react-router-dom';
import palette from '../../lib/styles/palette';
const buttonStyle = css`
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: 700;
padding: 0.5rem 1rem;
color: #fff;
outline: none;
cursor: pointer;
background: ${palette.gray[8]};
&:hover {
background: ${palette.gray[6]};
}
${(props) =>
props.fullWidth &&
css`
padding-top: 0.75rem;
padding-bottom: 0.75rem;
width: 100%;
font-size: 1.2rem;
`}
${(props) =>
props.teal &&
css`
background: ${palette.teal[7]};
&:hover {
background: ${palette.teal[6]};
}
`}
`;
const StyledButton = styled.button`
${buttonStyle}
`;
const StyledLink = styled(Link)`
${buttonStyle}
`;
const Button = (props) => {
return props.to ? (
<StyledLink {...props} teal={props.teal ? 1 : 0} />
) : (
<StyledButton {...props} />
);
};
export default Button;
css`` 안에 스타일을 작성한 후 buttonStyle이라 저장함.
StyledButton은 styled.button 이고
StyledLink는 styled(Link)로 작성.
삼항연산자 -> Button 컴포넌트의 props로 to
가 존재하면 - StyledLink
/ 그렇지 않으면 StyledButton
렌더링함.
만약 Button의 props로 'teal'이 있다면 적용. (true,false를 1,0으로 작성함.)
a 태그는 boolean 값이 임의의 props로 들어가는 것을 허용하지 않으므로, 1과 0으로 구분해야 한다.
잘 적용됨!
✅ 참고
- 웹 접근성을 위해서라도 방법 2인
styled(Link)
를 이용하는 것을 권장함!
container/common/HeaderContainer.js 생성.
import { useSelector } from 'react-redux';
import Header from '../../components/common/Header';
const HeaderContainer = () => {
const { user } = useSelector(({ user }) => ({ user: user.user }));
return <Header user={user} />;
};
export default HeaderContainer;
import styled from 'styled-components';
import Responsive from './Responsive';
import Button from './Button';
import palette from '../../lib/styles/palette';
import { Link } from 'react-router-dom';
const HeaderBlock = styled.div`
position: fixed;
width: 100%;
background: #fff;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
`;
const Wrapper = styled(Responsive)`
height: 4rem;
display: flex;
align-items: center;
justify-content: space-between;
.logo {
font-size: 1.2rem;
font-weight: 800;
letter-spacing: 0.25em;
cursor: pointer;
&:hover {
color: ${palette.teal[7]};
}
}
.right {
display: flex;
align-items: center;
}
`;
const Spacer = styled.div`
height: 4rem;
`;
// 🔻 username 나타내게
const UserInfo = styled.div`
font-weight: 800;
margin-right: 1rem;
`;
const Header = ({ user }) => {
return (
<>
<HeaderBlock>
<Wrapper>
<Link to="/" className="logo">
BLOGROW
</Link>
// 🔻 user이 null인지 아닌지애 따라 렌더링 다르게
{user ? (
<div className="right">
<UserInfo>{user.username}</UserInfo>
<Button>로그아웃</Button>
</div>
) : (
<div className="right">
<Button to="/login" teal>
로그인
</Button>
</div>
)}
</Wrapper>
</HeaderBlock>
<Spacer />
</>
);
};
export default Header;
pages/PostListPage.js 수정
import HeaderContainer from '../containers/common/HeaderContainer';
const PostListPage = () => {
return (
<div>
<HeaderContainer />
<div>this is contents!</div>
</div>
);
};
export default PostListPage;
🔻 참고 - state (user.username)
LocalStorage
를 이용해보자!containers/auth/LoginForm.js 수정
...
useEffect(() => {
if (user) {
navigate('/');
// 🔻 localStorage.setItem
try {
localStorage.setItem('user', JSON.stringify(user));
} catch (e) {
console.log('local storage error');
}
}
}, [navigate, user]);
❔ 왜
JSON.stringify
를 해주는가?
JSON.stringify()
: 배열이나 객체 등 모든것을 다 string으로 바꿔줌.
user 객체를 string 형식으로 저장하기 위함.
-> 내 이전 블로그 글을 참조!
containers/auth/RegisterForm.js 수정
const navigate = useNavigate();
useEffect(() => {
if (user) {
navigate('/'); // 홈으로 이동
try {
localStorage.setItem('user', JSON.stringify(user));
} catch (e) {
console.log('local storage error');
}
}
}, [navigate, user]);
리액트 앱이 맨 처음 렌더링 될때 localStorage에서 값을 불러와 리덕스 스토어 안에 넣도록 구현해줘야 함.
->componenetDidMount 또는 useEffect()로 한다며, 첫 렌더링 직후 실행되므로 깜빡임 현상이 나타남
따라서, entry 폴더인 index.js
에서 처리해주는 것이 좋음.
-> index.js 에서 사용자 정보를 불러오도록 처리.
-> user 모듈의 tempsetUser
/ check
모듈을 불러옴
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga';
import rootReducer, { rootSaga } from './modules';
import { tempSetUser, check } from './modules/user';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(sagaMiddleware)),
);
// 🔻 localStorage에 'user'가 있으면 tempSetUser, check 액션생성함수를 dispatch 해주는 함수.
function loadUser() {
try {
const user = localStorage.getItem('user');
if (!user) return; // 로그인 상태가 아닐 시
store.dispatch(tempSetUser(JSON.parse(user))); // 객체로 다시 바꾼 후 payload로 넣어줌
store.dispatch(check());
} catch (e) {
console.log('local storge error');
}
}
sagaMiddleware.run(rootSaga);
// 🔻 함수 호출. (반드시 sagaMiddleware.run 이후에 호출해야 함)
loadUser();
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
);
loadUser 함수는 반드시 sagaMiddleware.run() 이후에 호출해야 한다.
-> 먼저 호출시, CHECK 액션 디스패치시 saga에서 제대로 처리하지 않기 때문.
⚡️ 과정
- CHECK 액션 디스패치
- saga를 통해
client.get(/api/check)
-> axios API 요청- 만약 check API 호출 실패시 - user state를 초기화하고, localStorage도 비워줘야 함.
modules/user.js 수정
...
// saga 생성
const checkSaga = createRequestSaga(CHECK, authAPI.check); // request = client.get('/api/auth/check')
// 🔻검증 실패시 정보 초기화
function checkFailureSaga() {
try {
localStorage.removeItem('user');
} catch (e) {
console.log('local storage error');
}
}
export function* userSaga() {
yield takeLatest(CHECK, checkSaga);
// 🔻 CHECK_FAILURE 액션이 발생할 시 checkFailureSaga가 실행되도록.
yield takeLatest(CHECK_FAILURE, checkFailureSaga);
} // userSaga를 rootSaga로 합쳐줌
checkFailureSaga 함수에서는 yield를 사용하지 않는 일반 함수이므로,
function*
으로 만들어주지 않아도 됨.
개발자도구 - Application 탭에서 [모든 쿠키 삭제] 아이콘을 눌러주면 된다.
-> 쿠키 삭제 후 새로고침을 하면, 로그인 상태가 초기화 되어있다.
어플리케이션 탭에서 로컬스토리지를 보면 아무 값도 없이 비어있다!
auth 모듈
에 logout 함수 추가)lib/api/auth.js 수정
// axios 요청
import client from './client';
export const login = ({ username, password }) =>
client.post('/api/auth/login', { username, password });
// Path, payload 를 인자로 받음.
export const register = ({ username, password }) =>
client.post('/api/auth/register', { username, password });
export const check = () => client.get('/api/auth/check');
// 🔻 logout 함수 추가
export const logout = () => client.post('/api/auth/logout');
LOGOUT
이라는 액션 생성.modules/user.js
import { createAction, handleActions } from 'redux-actions';
import { takeLatest, call } from 'redux-saga/effects';
import * as authAPI from '../lib/api/auth';
import createRequestSaga, {
createRequestActionTypes,
} from '../lib/createRequestSaga';
const TEMP_SET_USER = 'user/TEMP_SET_USER';
const [CHECK, CHECK_SUCCESS, CHECK_FAILURE] =
createRequestActionTypes('user/CHECK');
const LOGOUT = 'user/LOGOUT';
export const tempSetUser = createAction(TEMP_SET_USER, (user) => user);
export const check = createAction(CHECK);
export const logout = createAction(LOGOUT);
// saga 생성
const checkSaga = createRequestSaga(CHECK, authAPI.check); // request = client.get('/api/auth/check')
// 검증 실패시 정보 초기화
function checkFailureSaga() {
try {
localStorage.removeItem('user');
} catch (e) {
console.log('local storage error');
}
}
// 🔻 사가 생성. (API 호출이므로, yield 필요)
function* logoutSaga() {
try {
yield call(authAPI.logout); // logoutAPI 호출
localStorage.removeItem('user');
} catch (e) {
console.log(e);
}
}
export function* userSaga() {
yield takeLatest(CHECK, checkSaga);
yield takeLatest(CHECK_FAILURE, checkFailureSaga);
// 🔻 LOGOUT 디스패치시 logoutSaga 함수 실행
yield takeLatest(LOGOUT, logoutSaga);
} // userSaga를 rootSaga로 합쳐줌
const initialState = {
user: null,
checkError: null,
};
export default handleActions(
{
[TEMP_SET_USER]: (state, { payload: user }) => ({
...state,
user,
}),
[CHECK_SUCCESS]: (state, { payload: user }) => ({
...state,
user,
checkError: null,
}),
[CHECK_FAILURE]: (state, { payload: error }) => ({
...state,
user: null,
checkError: error,
}),
// 🔻 state.user도 지워줌
[LOGOUT]: (state) => ({
...state,
user: null,
}),
},
initialState,
);
call
함수 이용 (call = 첫인자에 두번째 인자를 전달하여 호출)yield call(api명)
✅ 참고 - call()과 put()의 차이
1. call = API 요청 등
yield call(request, action.payload)
// request에 action.payload를 전달하여 호출
yield call(API.logout)
// logout API 호출
- put = 액션 디스패치
yield put(startLoading(type))
// 액션생성함수(페이로드) 가 인자로 들어감.
yield put({ type: SUCCESS, payload: response.data, })
// 페이로드(객체)만 적어줌
containers/common/HeaderContainer.js 수정
import { useDispatch, useSelector } from 'react-redux';
import Header from '../../components/common/Header';
import { logout } from '../../modules/user';
const HeaderContainer = () => {
const { user } = useSelector(({ user }) => ({ user: user.user })); // null 또는 username이 전달됨
const dispatch = useDispatch();
const onLogout = () => {
dispatch(logout());
};
return <Header user={user} onLogout={onLogout} />;
};
export default HeaderContainer;
props로 받아온 onLogout함수를 onClick에 연결시킴.
...
const Header = ({ user, onLogout }) => {
return (
<>
<HeaderBlock>
<Wrapper>
<Link to="/" className="logo">
BLOGROW
</Link>
{user ? (
<div className="right">
<UserInfo>{user.username}</UserInfo>
<Button onClick={onLogout}>로그아웃</Button>
</div>
) : (
<div className="right">
<Button to="/login" teal>
로그인
</Button>
</div>
)}
</Wrapper>
</HeaderBlock>
<Spacer />
</>
);
};
export default Header;