full stack mysql, node js express,react

JS FULL STACK 10장 React로 Todo앱/회원 만들기

우주전사버즈 2024. 1. 23. 16:05

왜 만들었지?

Todo 웹앱을 만들기위해 백엔드(express + postgressql)와 배포 fly.io는 모두 준비되었다.

이제는 프론트엔드영역에서 Todo UI를 만들고 fly.io에 배포해둔 주소에 API를 요청해야한다.

Javascript의 수많은 라이브러리가 존재하지만 가장 대중적으로 많이사용되고있는 React를 통해 Ui를 만들었다.

 

사실 Todo 정도의 간단한 웹앱은 바닐라자바스크립트로 구현해도 되는 부분이다
약 한달 반정도 전에 바닐라자바스크립트로 운동기록 웹앱을 만들어보려다가 끝내 포기하게되었다.

바닐라자바스크립트로 무언가 동적인 웹앱을 만들기 위해서 일반적으로 DOM제어를 통해 HTML을 반복적으로 조작하고 조작할떄마다 전체화면을 새로 그려주면서 보여주는 형태인데 Dom을 통해 계속 화면을 새로그리려고 이것저것 코드를 추가하다가 나조차도 무슨 코드인지 도저히 이해가 할수 없는 경지로 코드가 길어지면서 머리가아파서 포기하게되었다.
깃허브 : https://github.com/MkBaek0229/--js/blob/main/loadRoutine.js

아주작은 동작 (ex 추가하기를 눌러서 운동작성폼이뜨게하는)을 구현할떄도 DOM제어를 통해 화면을 계속 새로그리다보니깐
좋지않다라는 사실을 알게되었다 React라이브러리는 가상 DOM이라는 강력한 장점을 통해 변화를 주고싶은부분만 새로그릴수있다는 강력한 장점이 존재하며 별도의 컴포넌트를 만들어두고 한공간에서 합쳐 사용하는 방식으로 코드를 재사용하는 방식까지 아주 좋다.

 

근데 사실 아직 React를 통해 뭔가 제대로 만들어본적이 없어서(물론바닐라로도) 겁이났는데 사실 React는 너무간단하기때문에
내가 원하는 부분은 찾아보거나 챗지피티한테 적극적으로 도움을 요청하면서 만들었다.
사실 react도 결국 자바스크립트를 편하게 도와주는 확장도구 같은거니깐 자바스크립트를 알면 React를 아는거나 마찬가지

물론 React의 특성을 더이해하기위해 이번 과정을 끝내고 조금더 공부를 해봐야될거같다 시간을 많이투자하지는않더라도

 

결과물  Vercel로 배포

https://reacttodo-2024-1.vercel.app/

 

Vite + React

 

reacttodo-2024-1.vercel.app

 

만들어둔 코드를 Vercel에서 그저 임포트해왓더니 아주 쉽게 배포 할 수 있었다. Vercel은 현재 인기있는 next.js프레임워크를 개발한 회사라고한다. 깃허브의 저장소에 있는 코드들을 임포트해와서 거의 10초도안걸리는 시간에 배포할수있었다.

 

프론트엔드(react) 코드 깃허브

https://github.com/MkBaek0229/reacttodo-2024.git

 

GitHub - MkBaek0229/reacttodo-2024

Contribute to MkBaek0229/reacttodo-2024 development by creating an account on GitHub.

github.com

 

백엔드 (express api) 코드 깃허브

https://github.com/MkBaek0229/express_Todo_2024-01.git

 

GitHub - MkBaek0229/express_Todo_2024-01

Contribute to MkBaek0229/express_Todo_2024-01 development by creating an account on GitHub.

github.com

힘들었던 과정

사실 react로 todo앱을 만드는 내용은 구글링해서 5초만에 찾을수있는자료이다. 

근데 문제는 내가만든 todo에서는 회원가입된 아이디만 검증해서 로그인 후 Todo를 사용할수 있다는 컨셉인데 이부분에서 좀 애를먹는 바람에 시간이 조금 걸렸다. (4일)

파일구조

 

 

FirstPage :  메인페이지로 회원가입창을 누르면 회원가입창을 로그인창을 누르면 로그인창을 보여주고 로그인 해서 각회원마다 todo사용가능

TodoPage : 숨겨져있다가 로그인 성공시 FirstPage에서 보여줄수있도록 소환되고있음

Login : 로그인 컴포넌트 로그인 시도하면 서버에 axios를통해 데이터를 요청함

Signup : 회원가입 컴포넌트로 회원가입 시도하면 axios를 통해 서버에 데이터 요청(회원생성요청)

Todolist : todolist 컴포넌트로 todolistitem 컴포넌트를 불러오고있음

Todolistitem : todolistitem 컴포넌트 추가되있는 할일들을 불러옴 axios요청을 통해서 삭제, 완료 요청가능

Todotemplate : todo관련 컴포넌트를 전부 감싸안고있는 큰틀같은 컴포넌트

TodoPage에서 관련 컴포넌트를 다 모아 소환시킴

 

 

메인페이지 

// FirstPage.js

import React, { useState } from "react";
import Signup from "../Signup";
import Login from "../Login";
import TodoPage from "./TodoPage";

function FirstPage() {
  // visible이 True일때는 로그인창을 false일떄는 회원가입창을 보여줌
  const [visible, setVisible] = useState(false);
  // 로그인을 하게되면 로그인 창을 보여주도록 함
  const [loggedIn, setLoggedIn] = useState(false);
  // 로그인 시도시 입력창에 입력된 이름을 검증하고자 저장함
  const [username, setUsername] = useState("");


  // 사용자가 로그인창 / 회원가입창을 볼수있도록 만든 함수 setVisible로 visible의 상태를 클릭이 일어날때마다 true / false로 반복 변경되도록 만든 스위치
  const toggleVisibility = () => {
    setVisible(!visible);
  };

  // 사용자가 로그인을 시도할경우 입력창에 입력된 이름이 존재하는 회원인지 검증하기위해 정보요청 후 성공하면 회원이 이용가능한 투두폼을 보여줌
  const handleLogin = (username) => {
    setLoggedIn(true);
    setUsername(username);
  };

  // 로그아웃을 누를시 로그아웃이 되는것처럼 화면을 렌더링하기위해서 loggedin을 false로 바꿈으로써 투두폼을 다시사라지게하고 회원이름을 비움
  const handleLogout = () => {
    setLoggedIn(false);
    setUsername("");
  };

  return (
    <div>
      {loggedIn ? (
        <div>
          <h2>{`${username}님의 Todo 페이지`}</h2>
          <button onClick={handleLogout}>로그아웃</button>
          <hr />
          <TodoPage username={username} />
        </div>
      ) : (
        <div>
          <h2>메인 페이지</h2>
          <button onClick={toggleVisibility}>
            {visible ? "로그인창" : "회원가입창"}
          </button>
          <hr />
          {visible ? <Signup /> : <Login onLogin={handleLogin} />}
          <h1>회원가입 구현완료</h1>
          <p>현재 문제점 todo하나남앗을떄 삭제하면 곧바로 렌더링이안되는 문제 존재함<br />
            + 비밀번호 숫자+영문자+특수문자 조합으로 8자리 이상아니여도 가입됨<br />
            + 전화번호 작성시 - 000-0000-0000 틀에 맞혀져있어서 010작성하면 바로 -가생김 근데 안지워지는게문제<br />

          </p>
        </div>
        
      )}
    </div>
  );
}

export default FirstPage;

 

로그인 컴포넌트

import React, { useState, useEffect } from "react";
import axios from "axios";

import TodoPage from "./Page/TodoPage";

function Login() {
  // 회원이름 
  const [username, setUsername] = useState("");
  // 회원의 비밀번호
  const [password, setPassword] = useState("");
  // True라면 로그아웃 화면을 보여주고 false일땐 로그인 화면을 보여줌
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  // 로그인을 시도하면 form 기본전송을 e.preventDefault()을 통해 막고 axios를 통해 내가 배포해둔 API주소로 로그인 요청
  const handleSubmit = async (e) => {
    e.preventDefault();

    try {
      // 서버에 로그인 요청
      const response = await axios.post(
        'https://todoapp-spring-brook-5982-little-grass-565-silent-shape-3149.fly.dev/login',
        { username, password }
      );

      // 로그인 성공 여부를 확인하고 상태 업데이트
      if (response.data.resultCode === "S-1") {
        setIsAuthenticated(true);
      } else {
        // 로그인 실패 시에 대한 처리
        console.error("로그인 실패:", response.data.msg);
      }
    } catch (error) {
      console.error('로그인 요청 에러:', error);
      alert("존재하지 않는 이름 혹은 비밀번호입니다.")
    }
  };
 // 로그아웃 시 필요한 동작
  const handleLogout = () => {
    setIsAuthenticated(false);
    setUsername(""); // 로그아웃 시에 username을 초기화
    setPassword(""); // 로그아웃 시에 password를 초기화
  };

// 로그인 입력창에 입력한값이 올바르지않아 지우고싶을때 버튼눌러서 입력 초기화
  const inputreset = () => {
    setUsername(""); // username을 초기화
    setPassword(""); // password를 초기화
  }
  
  return (
    <>
      {isAuthenticated ? (
        <>
          <h2>로그아웃</h2>
          <button onClick={handleLogout}>로그아웃</button>
        </>
      ) : (
        <>
          <h2>로그인</h2>
          <form onSubmit={handleSubmit}>
            <div className="form-el">
              <label htmlFor="username">이름</label> <br />
              <input
                id="username"
                name="username"
                value={username}
                onChange={(e) => setUsername(e.target.value)}
              />
              {username && <button onClick={inputreset}>X</button>}
            </div>

            <div className="form-el">
              <label htmlFor="password">비밀번호</label> <br />
              <input
                id="password"
                name="password"
                type="password"
                value={password}
                onChange={(e) => setPassword(e.target.value)}
              />
              {password && <button onClick={inputreset}>X</button>}
            </div>
            <button type="submit">
              로그인
            </button>
          </form>
        </>
      )}
      {isAuthenticated && <TodoPage username={username} />}
    </>
  );
}

export default Login;

회원가입 컴포넌트

import axios from "axios";
import { useState } from "react";
import React from "react";

// Signup 컴포넌트 정의
function Signup() {
  
    // 사용자 입력 값에 대한 상태 변수들
    const [name, setName] = React.useState(""); 
    const [password, setPassword] = React.useState("");
    const [phone, setPhone] = React.useState("");

    // 사용자 입력 유효성과 관련된 메시지를 위한 상태 변수들
    const [nameMessage, setNameMessage] = React.useState("");
    const [passwordMessage, setPasswordMessage] = React.useState("");
    const [phoneMessage, setPhoneMessage] = React.useState("");

    // 유효성 검사 상태를 추적하는 상태 변수들
    const [isName, setIsName] = React.useState(false);
    const [isPassword, setIsPassword] = React.useState(false);
    const [isPhone, setIsPhone] = React.useState(false);
  
    // 회원가입 성공 여부를 나타내는 상태 변수
    const [signupSuccess, setSignupSuccess] = React.useState(false);

    // 폼 제출을 처리하는 함수
    const handleSubmit = async (e) => {
      e.preventDefault();
      // CreateMember 함수를 호출하여 사용자 데이터를 서버에 전송
      await CreateMember(name, password, phone);
      // 회원가입 성공 상태를 true로 설정
      setSignupSuccess(true);
    }

    // 사용자 데이터를 서버에 전송하는 함수
    const CreateMember = async (name, password, callnum) => {
      try {
          // 입력 필드 중 하나라도 비어 있는지 확인
          if (!name.trim() && !password.trim() && !callnum.trim()) {
              // 하나라도 비어 있으면 요청을 보내지 않음
              return;
          }

          // 사용자 데이터를 포함한 POST 요청을 서버에 보냄
          const response = await axios.post(
            'https://todoapp-spring-brook-5982-little-grass-565-silent-shape-3149.fly.dev/signup',
            {  username: name, password, callnum },
            {
              headers: {
                'Content-Type': 'application/json',
              },
            }
          );

          // 서버 응답을 콘솔에 출력
          console.log(response.data);

          // 서버 응답을 기반으로 성공 또는 실패에 따라 처리
          if (response.data.resultCode === 'S-1') {
            // 회원가입 성공 시 회원가입 성공 상태를 true로 설정
            setSignupSuccess(true);
          } else {
            // 회원가입 실패 시 알림 메시지 표시
            alert(`회원가입 실패: ${response.data.msg}`);
          }
      } catch (error) {
        // 에러가 발생한 경우 처리
        console.error('작업 수행 중 오류 발생:', error);
        alert('서버 에러가 발생했습니다.');
      }
    };

    // 'name' 입력 필드 변경을 처리하는 함수
    const onChangeName = (e) => {
        const currentName = e.target.value;
        // 현재 입력 값으로 'name' 상태를 업데이트
        setName(currentName);
        // 입력 길이를 검증하고 피드백 제공
        if (currentName.length < 1 || currentName.length > 10) {
          setNameMessage("이름은 1글자

투두 페이지

// TodoPage.js

import React, { useState, useEffect } from "react";
import TodoWrite from "../TodoWrite";
import TodoList from "../TodoList";
import TodoTemplate from "../TodoTemplate";
import axios from "axios";

// TodoPage 컴포넌트 정의
function TodoPage({ username }) {
  // 할 일 목록을 관리하는 상태 변수
  const [todos, setTodos] = useState([]);

  // 컴포넌트가 처음 마운트될 때 실행되는 useEffect 훅
  useEffect(() => {
    // 할 일 목록을 가져오는 함수 호출
    getTodos();
  }, []);

  // 서버에서 할 일 목록을 가져오는 비동기 함수
  const getTodos = async () => {
    try {
      // 서버에서 할 일 목록을 가져온 후 상태 업데이트
      const response = await axios.get(`https://todoapp-spring-brook-5982-little-grass-565-silent-shape-3149.fly.dev/${username}/todos`);
      setTodos(response.data.data);
    } catch (error) {
      // 에러가 발생한 경우 콘솔에 에러 메시지 출력
      console.error("할 일 목록을 불러오는 중 에러 발생:", error);
    }
  };

  // TodoPage 컴포넌트의 렌더링 결과
  return (
    <div>
      {/* TodoTemplate을 이용하여 레이아웃 구성 */}
      <TodoTemplate username={username}>
        {/* 할 일을 작성하는 컴포넌트 */}
        <TodoWrite username={username} setTodos={setTodos} />
        {/* 할 일 목록을 표시하는 컴포넌트 */}
        <TodoList username={username} todos={todos} setTodos={setTodos} />
      </TodoTemplate>
    </div>
  );
}

export default TodoPage;

 

투두 템플릿 컴포넌트

// TodoTemplate.js

import React from "react";

// 할 일 앱의 전반적인 레이아웃을 구성하는 컴포넌트
function TodoTemplate({ children, username }) {
    return (
        <> 
            {/* 사용자 이름과 함께 TODO 앱의 제목을 표시 */}
            <h1>{username}'s TODO-APP</h1>
            {/* 자식 컴포넌트를 렌더링하는 부분 */}
            <div>{children}</div> 
        </>
    );
}

export default TodoTemplate;

투두 리스트 컴포넌트

// TodoList.js

import React from "react";
import TodoListItem from "./TodoListItem";

// 할 일 목록을 표시하는 컴포넌트
function TodoList({ username, todos, setTodos }) {
    return (
        <div>
            {/* 할 일이 있는 경우 목록을 매핑하여 TodoListItem으로 생성 */}
            {todos.length > 0 ? (
                todos.map(todo => (
                    <TodoListItem key={todo.id} todo={todo} username={username} setTodos={setTodos} />
                ))
            ) : (
                // 할 일이 없는 경우 메시지 표시
                <p>등록된 todos가 없어요.</p>
            )}
        </div>
    );
}

export default TodoList;

투두 리스트아이템 컴포넌트

// TodoListItem.js

import axios from 'axios';

// 할 일 항목을 표시하고 관리하는 컴포넌트
function TodoListItem({ username, todo, setTodos }) {
    const { id, contents, completed } = todo;

    // 할 일 항목 삭제 처리 함수
    const onDelete = async () => {
        try {
            await axios.delete(`https://todoapp-spring-brook-5982-little-grass-565-silent-shape-3149.fly.dev/${username}/todos/${id}`);
            
            // 삭제 후 업데이트된 데이터를 서버에서 가져오는 GET 요청
            const response = await axios.get(`https://todoapp-spring-brook-5982-little-grass-565-silent-shape-3149.fly.dev/${username}/todos`);
           
            // setTodos를 호출하여 할 일 목록 상태를 업데이트
            setTodos(response.data.data);
    
        } catch (error) {
            // 필요한 경우 에러 처리
            console.error('데이터 삭제 오류:', error);
        }
    };

    // 할 일 항목 완료 여부 토글 처리 함수
    const onToggle = async () => {
        try {
            await axios.patch(
                `https://todoapp-spring-brook-5982-little-grass-565-silent-shape-3149.fly.dev/${username}/todos/${id}`,
                { completed: !completed },
                {
                    headers: {
                        'Content-Type': 'application/json',
                    },
                }
            );

            // 완료 여부 토글 후 업데이트된 데이터를 서버에서 가져오는 GET 요청
            const response = await axios.get(`https://todoapp-spring-brook-5982-little-grass-565-silent-shape-3149.fly.dev/${username}/todos`);
            setTodos(response.data.data);
        } catch (error) {
            // 필요한 경우 에러 처리
            console.error('데이터 토글 오류:', error);
        }
    };

    // 할 일 항목 렌더링
    return (
        <div style={{ textDecoration: completed ? 'line-through' : 'none' }}>
            <input type="checkbox" checked={completed} onChange={() => onToggle(id)} />
            <span> <strong>할일</strong> : {contents} </span>
            <button onClick={() => onDelete(id)}>삭제</button>
        </div>
    );
}
export default TodoListItem;

투두 작성 컴포넌트

// TodoWrite.js

import React, { useState } from "react";
import axios from 'axios';

// 할 일을 작성하는 컴포넌트
function TodoWrite({ username, setTodos }) {
    const [text, setText] = useState("");

    // 할 일을 추가하는 함수
    const onInsert = async (text) => {
        try {
            if (!text.trim()) {
                // 입력 값이 공백인 경우 요청을 보내지 않음
                return;
            }

            // 할 일을 추가하기 위한 POST 요청
            await axios.post(
                `https://todoapp-spring-brook-5982-little-grass-565-silent-shape-3149.fly.dev/${username}/todos`,
                { contents: text },
                {
                    headers: {
                        'Content-Type': 'application/json',
                    },
                }
            );

            // 추가 후 업데이트된 데이터를 서버에서 가져오는 GET 요청
            const response = await axios.get(`https://todoapp-spring-brook-5982-little-grass-565-silent-shape-3149.fly.dev/${username}/todos`);
            setTodos(response.data.data);
        } catch (error) {
            // 필요한 경우 에러 처리
            console.error('작업 수행 중 오류 발생:', error);
        }
    };

    // 입력 값 변경 이벤트 핸들러
    const onChange = (e) => {
        setText(e.target.value);
    };

    // 폼 제출 이벤트 핸들러
    const onSubmit = (e) => {
        onInsert(text);
        setText('');
        e.preventDefault();
    };

    // 할 일 작성 컴포넌트 렌더링
    return (
        <div>
            <form onSubmit={onSubmit}>
                <input
                    placeholder="할 일을 입력하세요."
                    type="text"
                    value={text}
                    onChange={onChange}
                />
                <button type="submit">저장</button>
            </form>
        </div>
    );
}

export default TodoWrite;