Skip to main content

Create React App

2021-09-07#

List#

  • Give Feedback While Logging In
  • Create a Custom React Hook to Handle Form Fields

Give Feedback While Logging In#

로그인 하는중간에 사용자에게 피드백 주기

src/containers/Login.js

const [isLoading, setIsLoading] = useState(false);
async function handleSubmit(event) {
event.preventDefault();
setIsLoading(true);
try {
await Auth.signIn(email, password);
userHasAuthenticated(true);
history.push("/");
} catch (e) {
alert(e.message);
setIsLoading(false);
}
}
mkdir src/components/

src/components/LoaderButton.js

import React from "react";
import Button from "react-bootstrap/Button";
import { BsArrowRepeat } from "react-icons/bs";
import "./LoaderButton.css";
export default function LoaderButton({
isLoading,
className = "",
disabled = false,
...props
}) {
return (
<Button
disabled={disabled || isLoading}
className={`LoaderButton ${className}`}
{...props}
>
{isLoading && <BsArrowRepeat className="spinning" />}
{props.children}
</Button>
);
}

src/components/LoaderButton.css

.LoaderButton .spinning {
margin-right: 7px;
top: 2px;
animation: spin 1s infinite linear;
}
@keyframes spin {
from {
transform: scale(1) rotate(0deg);
}
to {
transform: scale(1) rotate(360deg);
}
}
  • isLoading = ture 일때 로딩 바 구현

src/containers/Login.js

// import Button from "react-bootstrap/Button";
import LoaderButton from "../components/LoaderButton"; // 추가
// <Button block size="lg" type="submit" disabled={!validateForm()}>
// Login
// </Button>
<LoaderButton
block
size="lg"
type="submit"
isLoading={isLoading}
disabled={!validateForm()}
>
Login
</LoaderButton>

login-loading-state

Handling Errors#

src/lib/errorLib.js

export function onError(error) {
let message = error.toString();
// Auth errors
if (!(error instanceof Error) && error.message) {
message = error.message;
}
alert(message);
}

src/containers/Login.js

import { onError } from "../lib/errorLib";
// handleSubmit 함수에 추가
onError(e);

Create a Custom React Hook to Handle Form Fields#

가입 페이지에 대해 유사한 작업 및 공통 수행작업을 위해 Custom Hooks 작성

src/lib/hooksLib.js

import { useState } from "react";
export function useFormFields(initialState) {
const [fields, setValues] = useState(initialState);
return [
fields,
function(event) {
setValues({
...fields,
[event.target.id]: event.target.value
});
}
];
}

src/containers/Login.js

import React, { useState } from "react";
import { Auth } from "aws-amplify";
import Form from "react-bootstrap/Form";
import { useHistory } from "react-router-dom";
import LoaderButton from "../components/LoaderButton";
import { useAppContext } from "../lib/contextLib";
import { useFormFields } from "../lib/hooksLib";
import { onError } from "../lib/errorLib";
import "./Login.css";
export default function Login() {
const history = useHistory();
const { userHasAuthenticated } = useAppContext();
const [isLoading, setIsLoading] = useState(false);
const [fields, handleFieldChange] = useFormFields({
email: "",
password: ""
});
function validateForm() {
return fields.email.length > 0 && fields.password.length > 0;
}
async function handleSubmit(event) {
event.preventDefault();
setIsLoading(true);
try {
await Auth.signIn(fields.email, fields.password);
userHasAuthenticated(true);
history.push("/");
} catch (e) {
onError(e);
setIsLoading(false);
}
}
return (
<div className="Login">
<Form onSubmit={handleSubmit}>
<Form.Group size="lg" controlId="email">
<Form.Label>Email</Form.Label>
<Form.Control
autoFocus
type="email"
value={fields.email}
onChange={handleFieldChange}
/>
</Form.Group>
<Form.Group size="lg" controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
value={fields.password}
onChange={handleFieldChange}
/>
</Form.Group>
<LoaderButton
block
size="lg"
type="submit"
isLoading={isLoading}
disabled={!validateForm()}
>
Login
</LoaderButton>
</Form>
</div>
);
}

2021-09-14#

List#

  • Create a Signup Page
  • Create the signup form
  • Signup with AWS Cognito

Create a signup page#

가입 흐름#

  1. 사용자는 이메일, 비밀번호를&비밀번호 확인을 입력

  2. AWS Amplify 라이브러리를 사용하여 Amazon Cognito에 등록하고 사용자 객체를 Return

  3. 그런 다음 AWS Cognito에서 이메일로 보낸 확인 코드를 수락하는 양식을 렌더링

  4. AWS Cognito에 확인 코드를 전송하여 가입을 확인

  5. 새로 생성된 사용자를 인증

  6. 최종 세션으로 앱 상태를 업데이트

Create the Signup Form#

src/containers/Signup.js

import React, { useState } from "react";
import Form from "react-bootstrap/Form";
import { useHistory } from "react-router-dom";
import LoaderButton from "../components/LoaderButton";
import { useAppContext } from "../lib/contextLib";
import { useFormFields } from "../lib/hooksLib";
import { onError } from "../lib/errorLib";
import "./Signup.css";
export default function Signup() {
const [fields, handleFieldChange] = useFormFields({
email: "",
password: "",
confirmPassword: "",
confirmationCode: "",
});
const history = useHistory();
const [newUser, setNewUser] = useState(null);
const { userHasAuthenticated } = useAppContext();
const [isLoading, setIsLoading] = useState(false);
function validateForm() {
return (
fields.email.length > 0 &&
fields.password.length > 0 &&
fields.password === fields.confirmPassword
);
}
function validateConfirmationForm() {
return fields.confirmationCode.length > 0;
}
async function handleSubmit(event) {
event.preventDefault();
setIsLoading(true);
setNewUser("test");
setIsLoading(false);
}
async function handleConfirmationSubmit(event) {
event.preventDefault();
setIsLoading(true);
}
function renderConfirmationForm() {
return (
<Form onSubmit={handleConfirmationSubmit}>
<Form.Group controlId="confirmationCode" size="lg">
<Form.Label>Confirmation Code</Form.Label>
<Form.Control
autoFocus
type="tel"
onChange={handleFieldChange}
value={fields.confirmationCode}
/>
<Form.Text muted>Please check your email for the code.</Form.Text>
</Form.Group>
<LoaderButton
block
size="lg"
type="submit"
variant="success"
isLoading={isLoading}
disabled={!validateConfirmationForm()}
>
Verify
</LoaderButton>
</Form>
);
}
function renderForm() {
return (
<Form onSubmit={handleSubmit}>
<Form.Group controlId="email" size="lg">
<Form.Label>Email</Form.Label>
<Form.Control
autoFocus
type="email"
value={fields.email}
onChange={handleFieldChange}
/>
</Form.Group>
<Form.Group controlId="password" size="lg">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
value={fields.password}
onChange={handleFieldChange}
/>
</Form.Group>
<Form.Group controlId="confirmPassword" size="lg">
<Form.Label>Confirm Password</Form.Label>
<Form.Control
type="password"
onChange={handleFieldChange}
value={fields.confirmPassword}
/>
</Form.Group>
<LoaderButton
block
size="lg"
type="submit"
variant="success"
isLoading={isLoading}
disabled={!validateForm()}
>
Signup
</LoaderButton>
</Form>
);
}
return (
<div className="Signup">
// 조건부 렌더링
{newUser === null ? renderForm() : renderConfirmationForm()}
</div>
);
}

src/containers/Signup.css

@media all and (min-width: 480px) {
.Signup {
padding: 60px 0;
}
.Signup form {
margin: 0 auto;
max-width: 320px;
}
}

Add the Route#

src/Routes.js

import Signup from "./containers/Signup"; // 추가
<Route exact path="/signup">
<Signup />
</Route>

signup-page-added

Signup with AWS Cognito#

src/containers/Signup.js

import { Auth } from "aws-amplify"; // Amplify 추가
// Cognitio와 연결
async function handleSubmit(event) {
event.preventDefault();
setIsLoading(true);
try {
const newUser = await Auth.signUp({
username: fields.email,
password: fields.password,
});
setIsLoading(false);
setNewUser(newUser);
} catch (e) {
onError(e);
setIsLoading(false);
}
}
async function handleConfirmationSubmit(event) {
event.preventDefault();
setIsLoading(true);
try {
await Auth.confirmSignUp(fields.email, fields.confirmationCode);
await Auth.signIn(fields.email, fields.password);
userHasAuthenticated(true);
history.push("/");
} catch (e) {
onError(e);
setIsLoading(false);
}
}

Sign Up 흐름#

  1. handleSubmit 사용자가 Auth.signUp() 을 통해 새 사용자 생성

  2. setNewUser를 사용하여 해당 사용자 객체를 상태에 저장

  3. Auth.confirmSignUp()에서 사용자의 전송코드 확인

  4. 사용자가 확인되었으므로 이제 Cognito는 앱에 로그인할 수 있는 새 사용자가 있음

  5. 이메일과 비밀번호를 사용하여 로그인 페이지에서 했던 것과 똑같은 방식으로 Auth.signIn() 호출하여 인증

  6. userHasAuthenticated 함수를 사용하여 앱의 컨텍스트를 업데이트

  7. 마지막으로 홈페이지로 이동

2021-09-28#

List#

  • Add the Create Note Page
  • Call the Create API
  • Upload a File to S3
  • List All the Notes

Add the Create Note Page#

Add the Container#

src/containers/NewNote.js

import React, { useRef, useState } from "react";
import Form from "react-bootstrap/Form";
import { useHistory } from "react-router-dom";
import LoaderButton from "../components/LoaderButton";
import { onError } from "../lib/errorLib";
import config from "../config";
import "./NewNote.css";
export default function NewNote() {
const file = useRef(null);
const history = useHistory();
const [content, setContent] = useState("");
const [isLoading, setIsLoading] = useState(false);
function validateForm() {
return content.length > 0;
}
function handleFileChange(event) {
file.current = event.target.files[0];
}
async function handleSubmit(event) {
event.preventDefault();
if (file.current && file.current.size > config.MAX_ATTACHMENT_SIZE) {
alert(
`Please pick a file smaller than ${config.MAX_ATTACHMENT_SIZE /
1000000} MB.`
);
return;
}
setIsLoading(true);
}
return (
<div className="NewNote">
<Form onSubmit={handleSubmit}>
<Form.Group controlId="content">
<Form.Control
value={content}
as="textarea"
onChange={(e) => setContent(e.target.value)}
/>
</Form.Group>
<Form.Group controlId="file">
<Form.Label>Attachment</Form.Label>
<Form.Control onChange={handleFileChange} type="file" />
</Form.Group>
<LoaderButton
block
type="submit"
size="lg"
variant="primary"
isLoading={isLoading}
disabled={!validateForm()}
>
Create
</LoaderButton>
</Form>
</div>
);
}

src/containers/NewNote.css

.NewNote form textarea {
height: 300px;
font-size: 1.5rem;
}

Add the Route#

src/Routes.js

import NewNote from "./containers/NewNote";
<Route exact path="/notes/new">
<NewNote />
</Route>

new-note-page-added

Call the Create API#

src/containers/NewNote.js

import { API } from "aws-amplify";
// API 연결
async function handleSubmit(event) {
event.preventDefault();
if (file.current && file.current.size > config.MAX_ATTACHMENT_SIZE) {
alert(
`Please pick a file smaller than ${config.MAX_ATTACHMENT_SIZE /
1000000} MB.`
);
return;
}
setIsLoading(true);
try {
await createNote({ content });
history.push("/");
} catch (e) {
onError(e);
setIsLoading(false);
}
}
function createNote(note) {
return API.post("notes", "/notes", {
body: note
});
}

Create Note 흐름#

  1. 메모 객체에 createNote POST 요청을 /notes 주소에 전달 함으로써 생성 API 호출 진행, API.post()메서드에 대한 처음 두 인수 는 notes및 /notes (AWS Amplify 구성 장에서 이러한 API 세트를 이름으로 설정했기 때문에 notes이다.)

  2. Memo Object는 단순히 메모내용만 전송

  3. 마지막으로 메모가 생성된 후 홈페이지로 리다이렉션 진행

Upload a File to S3#

  1. 사용자가 업로드할 파일을 선택
  2. 파일이 사용자 폴더 아래의 S3에 업로드되고 키를 Return
  3. 파일 키를 첨부 파일로 사용하여 메모를 Create

src/lib/awsLib.js

import { Storage } from "aws-amplify";
export async function s3Upload(file) {
const filename = `${Date.now()}-${file.name}`;
const stored = await Storage.vault.put(filename, file, {
contentType: file.type,
});
return stored.key;
}
  1. 파일 객체를 매개변수로 사용
  2. 현재 타임스탬프( Date.now())를 사용하여 고유한 파일 이름을 생성 (물론 앱을 많이 사용하는 경우 고유한 파일 이름을 만드는 가장 좋은 방법이 아닐 수 있다.)
  3. Storage.vault.put()객체를 사용하여 S3의 사용자 폴더에 파일을 업로드, 또는 공개적으로 업로드하는 경우 Storage.put() 방법을 사용할 수 있음
  4. 저장된 객체의 키를 반환

Upload Before Creating a Note#

src/containers/NewNote.js

import { s3Upload } from "../lib/awsLib";
// createNote 되기전 업로드 진행
async function handleSubmit(event) {
event.preventDefault();
if (file.current && file.current.size > config.MAX_ATTACHMENT_SIZE) {
alert(
`Please pick a file smaller than ${
config.MAX_ATTACHMENT_SIZE / 1000000
} MB.`
);
return;
}
setIsLoading(true);
try {
const attachment = file.current ? await s3Upload(file.current) : null;
await createNote({ content, attachment });
history.push("/");
} catch (e) {
onError(e);
setIsLoading(false);
}
}

List All the Notes#

src/containers/Home.js

import React, { useState, useEffect } from "react";
import ListGroup from "react-bootstrap/ListGroup";
import { useAppContext } from "../lib/contextLib";
import { onError } from "../lib/errorLib";
import "./Home.css";
export default function Home() {
const [notes, setNotes] = useState([]);
const { isAuthenticated } = useAppContext();
const [isLoading, setIsLoading] = useState(true);
function renderNotesList(notes) {
return null;
}
function renderLander() {
return (
<div className="lander">
<h1>Scratch</h1>
<p className="text-muted">A simple note taking app</p>
</div>
);
}
function renderNotes() {
return (
<div className="notes">
<h2 className="pb-3 mt-4 mb-3 border-bottom">Your Notes</h2>
<ListGroup>{!isLoading && renderNotesList(notes)}</ListGroup>
</div>
);
}
return (
<div className="Home">
{isAuthenticated ? renderNotes() : renderLander()}
</div>
);
}

empty-homepage-loaded

Call the List API#

src/containers/Home.js

import { API } from "aws-amplify";
import { BsPencilSquare } from "react-icons/bs";
import { LinkContainer } from "react-router-bootstrap";
useEffect(() => {
async function onLoad() {
if (!isAuthenticated) {
return;
}
try {
const notes = await loadNotes();
setNotes(notes);
} catch (e) {
onError(e);
}
setIsLoading(false);
}
onLoad();
}, [isAuthenticated]);
function loadNotes() {
return API.get("notes", "/notes");
}
function renderNotesList(notes) {
return (
<>
{/* 새로운 노트 추가 */}
<LinkContainer to="/notes/new">
<ListGroup.Item action className="py-3 text-nowrap text-truncate">
{/* BsPencilSquare 아이콘 사용 */}
<BsPencilSquare size={17} />
<span className="ml-2 font-weight-bold">Create a new note</span>
</ListGroup.Item>
</LinkContainer>
{/* note 데이터 렌더 */}
{notes.map(({ noteId, content, createdAt }) => (
<LinkContainer key={noteId} to={`/notes/${noteId}`}>
<ListGroup.Item action>
<span className="font-weight-bold">
{content.trim().split("\n")[0]}
</span>
<br />
<span className="text-muted">
Created: {new Date(createdAt).toLocaleString()}
</span>
</ListGroup.Item>
</LinkContainer>
))}
</>
);
}

homepage-list-loaded