회원가입 : RegisterPage : "/register"
로그인 : LoginPage : "/login"
포스트 작성 : WriterPage : "/write"
포스트 읽기 : PostPage : "/@username/:postId"
포스트 목록 : PostListPage : ["/@:username","/"]
컴포넌트 단위로 일정한 스타일을 적용하기 위해!
컴포넌트가 아닌 것에 스타일 적용시 CSS selector 활용
리덕스 스토어 생성 후 Provider 컴포넌트를 통해 리덕스 적용
Ducks 패턴 사용해 리덕스 모듈 작성
components 디렉토리 -> 프레젠테이셔널 컴포넌트
ㄴ common 디렉토리 -> 재사용되는 컴포넌트 (Button)
ㄴ auth 디렉토리 -> 회원 인증 관련 컴포넌트 (AuthForm, AuthTemplate)
ㄴ write 디렉토리 -> 글쓰기 관련 컴포넌트
ㄴ post 디렉토리 -> 포스트 읽기 관련 컴포넌트
ㄴ base 디렉토리 -> 프로젝트 기반 관련 컴포넌트 (Header)
1) Snippet 코드 작성
https://snippet-generator.app/ 사용
확장자를 제외한 파일 이름을 의미하는 ${TM_FILENAME_BASE} 쿼리를 통해 Snippet 코드 작성
Snippet 설명 (Description) : Styled React Functional Component
Snippet 줄임 단어 (Tab trigger) : srfc
2) VS Code 설정
file -> preferences -> user snippets
언어는 javascriptreact 사용, JSON 파일에 {}로 감싸서 Snippet 코드 복붙하기!
3) 컴포넌트 생성시 사용
js 파일 생성 후 언어를 javascriptreact로 바꾸기
Snippet 줄임 단어로 설정한 srfc 치면 자동 완성됨!
AuthTemplate 컴포넌트 생성
child 랜더링 되도록 작성
//AuthTemplate.js
import React from "react";
import styled from "styled-components";
// 회원가입/로그인 페이지의 레이아웃 담당하는 컴포넌트
const AuthTemplateBlock = styled.div``;
const AuthTemplate = ({children}) => {
//children 랜더링
return <AuthTemplateBlock>{children}</AuthTemplateBlock>;
};
export default AuthTemplate;
AuthForm 컴포넌트 생성
//AuthForm.js
import React from "react";
import styled from "styled-components";
// 회원가입 또는 로그인 폼
const AuthFormBlock = styled.div``; //최상위 컴포넌트로 Block 붙임
const AuthForm = () => {
return <AuthFormBlock>AuthForm</AuthFormBlock>;
};
export default AuthForm;
LoginPage와 RegisterPage 랜더링 확인
AuthForm과 AuthTemplate 사용하는 컴포넌트의 랜더링 확인
http://localhost:3000/register
http://localhost:3000/login
AuthTemplate 부모 영역 UI
배경은 회색, 중앙에 흰색 박스 띄우고, 홈 경로 /로 돌아가기
AuthTemplate에는 input
from
으로 회원가입이랑 로그인이 들어갈거임
AuthForm 스타일링
//AuthForm.js
(...)
//필요한 정보 입력하는 input 스타일링
const StyledInput = styled.input`
font-size: 1rem;
border: none;
border-bottom: 1px solid ${palette.gray[5]};
padding-bottom: 0.5rem;
outline: none;
width: 100%;
&:focus {
color: $0c-teal-7;
border-bottom: 1px solid ${palette.gray[7]};
}
& + & {
margin-top: 1rem;
}`;
//로그인에서 회원가입 이동, 회원가입에서 로그인 이동하는 Footer 스타일링
const Footer = styled.div`
margin-top: 2rem;
text-align: right;
a {
color: ${palette.gray[6]};
text-decoration: underline;
& :hover {
color: ${palette.gray[9]};
}
}
`;
//components/common/Button.js
const StyledButton = styled.button`
//기본 버튼 스타일링
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: bold;
padding: 0.25rem 1rem;
color: white;
outline: none;
cursor: pointer;
background: ${palette.gray[8]};
&:hover {
background: ${palette.gray[6]};
}
//button컴포넌트에 props로 full width 설정 시 적용 스타일 (너비)
${(props) =>
props.fullWidth &&
css`
padding-top: 0.75rem;
padding-bottom: 0.75rem;
width: 100%;
font-size: 1.125rem;
`}
//button 컴포넌트에 props로 cran 설정 시 적용되는 스타일 (색상)
${(props) =>
props.cran &&
css`
background: ${palette.cyan[5]};
&:hover {
background: ${palette.cyan[4]};
}
`}
버튼 자체에 필요한 스타일링 (너비/색상) Button 컴포넌트에 작성
실제 정보를 입력하는 input과 button 사이의 필요한 마진은 AuthForm에 작성
//component/auth/AuthForm.js
//버튼 위로 마진 넣어 input과 공백 생성
const ButtonWithMarginTop = styled(Button)`
margin-top: 1rem;
`;
(...)
//버튼의 props로 색상(cran)과 너비(fullWidth) 설정
//Styled-css로 버튼 위 마진 부여
<ButtonWithMarginTop fullWidth cran >
로그인
</ButtonWithMarginTop>
(...)
//components/auth/AuthForm.js
(...)
//페이지 컴포넌트에서 props를 전달하는 방식으로 페이지 타입 명시해 전달
const textMap = {
register: "회원가입",
login: "로그인",
};
const AuthForm = ({ type }) => {
const text = textMap[type];
return (
<AuthFormBlock>
<h3>{text}</h3>
<form>
<StyledInput
autoComplete="username"
name="username"
placeholder="아이디"
/>
<StyledInput
autoComplete="new-password"
name="password"
placeholder="비밀번호"
type="password"
/>
{/*회원가입 부분에는 비밀번호 확인하는 칸 필요*/}
{type === "register" && (
<StyledInput
autoComplete="new-password"
name="passwordConfirm"
placeholder="비밀번호 확인"
type="password"
/>
)}
<ButtonWithMarginTop
/*cran로 props로 설정해 버튼 색깔 파란색으로 스타일링 가능 */ fullWidth
>
로그인
</ButtonWithMarginTop>
</form>
<Footer>
{/*회원가입이면 클릭해 로그인으로 감, 로그인이면 클릭해 회원가입으로 감 */}
{type === "login" ? (
<Link to="/register">회원가입</Link>
) : (
<Link to="/login"> 로그인</Link>
)}
</Footer>
</AuthFormBlock>
);
};
export default AuthForm;
로그인/회원가입 페이지에서 props를 통해 페이지 타입 전달
로그인 페이지
import React from "react";
import AuthForm from "../components/auth/AuthForm";
import AuthTemplate from "../components/auth/AuthTemplate";
const LoginPage = () => {
return (
<AuthTemplate>
<AuthForm type="login" />
</AuthTemplate>
);
};
export default LoginPage;
회원 가입 페이지
import React from "react";
import AuthForm from "../components/auth/AuthForm";
import AuthTemplate from "../components/auth/AuthTemplate";
const RegisterPage = () => {
return (
<AuthTemplate>
<AuthForm type="register" />
</AuthTemplate>
);
};
export default RegisterPage;
modules/auth.js
import { createAction, handleActions } from "redux-actions";
import produce from "immer";
const CHANGE_FIELD = "auth/CHANGE_FIELD";
const INITIALIZE_FORM = "auth/INITIALLIZE_FORM";
export const changeField = createAction(
CHANGE_FIELD,
({ form, key, value }) => ({
form, //register,loign
key, //username, password, passwordConfirm
value, //실제 바꾸려는 값
}),
);
export const initializeForm = createAction(INITIALIZE_FORM, (form) => form);
//register, login
const initialState = {
register: {
username: "",
password: "",
passwordConfirm: "",
},
login: {
username: "",
password: "",
},
};
const auth = handleActions(
{
[CHANGE_FIELD]: (state, { payload: { form, key, value } }) =>
produce(state, (draft) => {
draft[form][key] = value;
//state.register.username 바꾸기
}),
[INITIALIZE_FORM]: (state, { payload: form }) => ({
...state,
[form]: initialState[form],
}),
},
initialState,
);
export default auth;
컨테이너 컴포넌트 : LoginForm 컴포넌트
container/auth/LoginForm.js
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { changeField, initializeForm } from "../../modules/auth";
import AuthForm from "../../components/auth/AuthForm";
const LoginForm = () => {
//컴포넌트를 리덕스와 연동
const dispatch = useDispatch();
const { form } = useSelector(({ auth }) => ({
form: auth.login,
}));
//input 변경 이벤트 핸들러
const onChange = (e) => {
const { value, name } = e.target;
dispatch(
changeField({
form: "login",
key: name,
value,
}),
);
};
//폼 등록 이벤트 핸들러
const onSubmit = (e) => {
e.preventDefault();
//구현 예정
};
//컴포넌트가 처음 랜더링될 때 form 초기화함
//useEffect 사용해 렌더링 후 initializeForm 액션 생성 함수 호출
useEffect(() => {
dispatch(initializeForm("login"));
}, [dispatch]);
return (
<AuthForm
type="login"
form={form}
//액션을 디스패치함
onChange={onChange}
onSubmit={onSubmit}
/>
);
};
export default LoginForm;
const LoginForm = () => {
//컴포넌트를 리덕스와 연동
const dispatch = useDispatch();
const { form } = useSelector(({ auth }) => ({
form: auth.login,
}));
(...)
//input 변경 이벤트 핸들러 함수 작성
const onChange = (e) => {
const { value, name } = e.target;
dispatch(
changeField({
form: "login",
key: name,
value,
}),
);
};
//폼 등록 이벤트 핸들러 함수 작성
const onSubmit = (e) => {
e.preventDefault();
//구현 예정
};
액션 디스패치
return (
<AuthForm
type="login"
form={form}
//액션을 디스패치함
onChange={onChange}
onSubmit={onSubmit}
/>
);
⭐ useEffect 사용해 처음 랜더링 후 initializeForm 액션 생성 함수 호출 ⭐
로그인 페이지에서 값 입력 후 다른 페이지로 갔다가 다시 로그인 페이지로 돌아왔을 때 입력했던 값이 남아 있지 않도록 함!!
//컴포넌트가 처음 랜더링될 때 form 초기화함
//useEffect 사용해 렌더링 후 initializeForm 액션 생성 함수 호출
useEffect(() => {
dispatch(initializeForm("login"));
}, [dispatch]);
pages/LoginPage.js
import React from "react";
import AuthForm from "../components/auth/AuthForm";
import AuthTemplate from "../components/auth/AuthTemplate";
import LoginForm from "../containers/auth/LoginForm";
const LoginPage = () => {
return (
<AuthTemplate>
<LoginForm />
</AuthTemplate>
);
};
export default LoginPage;
component/auth/AuthForm.js
onChange, onSubmit, form의 Props로 받아오기
const AuthForm = ({ type, form, onChange, onSubmit }) => {
const text = textMap[type];
return (...
<form onSubmit={onSubmit}> //form 제출
<StyledInput
autoComplete="username"
name="username"
placeholder="아이디"
onChange={onChange} //input 입력
value={form.username} //유저 이름 값
/>
<StyledInput
autoComplete="new-password"
name="password"
placeholder="비밀번호"
type="password"
onChange={onChange} //input 입력
value={form.password} //비밀번호 값
/>
{/*회원가입 부분에는 비밀번호 확인하는 칸 필요*/}
{type === "register" && (
<StyledInput
autoComplete="new-password"
name="passwordConfirm"
placeholder="비밀번호 확인"
type="password"
onChange={onChange} //input 입력
value={form.passwordConfirm} //비밀번호 확인 값
/>
)}
<ButtonWithMarginTop fullWidth
>
로그인
</ButtonWithMarginTop>
</form>
로그인 페이지 리덕스 연결 확인
컨테이너 컴포넌트 : RegisterForm 컴포넌트