[React] 기초 리엑트 개념(state,props)로 토이프로젝트 구현하기

SuamKang·2023년 7월 4일
2

React

목록 보기
13/34

기초적인 리엑트 개념으로 사용자의 입력을 받아 데이터로 저장한뒤, 출력하여 렌더링해주어 리스트를 보여주는 아주 간단한 프로젝트를 구현해 보았다.


내가 여기서 핵심적으로 정한 목표는 리엑트 라이브러리를 사용하는 가장 큰 이유이자 아이디어인 재사용성을 염두하여 각 컴포넌트 모듈의 역할을 분리하고 재사용할 수 있는 컴포넌트로 구현하는 것이다.

이번 연습이 간단하지만 가장 기본적인 리엑트 개념을 다지기에 적합할것으로 생각하였다.

📍 구현된 프로젝트

해당 구현된 모듈은 입력폼안에서 사용자의 이름과 나이를 입력받고, 입력받은 새로운 데이터를 저장하여 렌더링해서 보여주며, 유효하지 않는 입력에 대한 에러처리를 모달 방식으로 처리해준 프로젝트이다.


📍 디렉토리 구조

디렉토리 구조

위처럼 우선 components 디렉토리 안에 실질적 기능이 담겨지는 Users 폴더와 UI 화면렌더링만 담당하는 UI 폴더로 나누어 진행했다.

이는 가독성면이나 협업시, 나중에 더복잡해지는 프로젝트 구현에 대비하여 미리 나누어 주는 연습이 굉장히 효과적이다고 하여 적용해 보았다.


📍 컴포넌트 분리 및 연결

위 사진처럼 적용하는 컴포넌트끼리 어떤 관계를 가지고 있으며 상태를 누가 관리해야하는지 대략적으로 그려본 트리이다.

이걸 토대로 로직을 구현해 나갔다.

그리고, css스타일링은 css모듈 식으로 작성하여 각 컴포넌트에 독립적으로 적용해주었다.


1. "AddUserForm 추가"


먼저, 사용자의 입력을 받을 수 있는 폼컴포넌트를 구성했다.

import { useState } from "react";

import Modal from "../../UI/Modal";
import Card from "../../UI/Card";
import Button from "../../UI/Button";

import styles from "./AddUserForm.module.css";


function AddUserForm(props) {
  const [userName, setUserName] = useState("");
  const [userAge, setUserAge] = useState("");
  const [isAgeCheck, setIsAgeCheck] = useState(true);
  const [isOpenModal, setIsOpenModal] = useState(false);

 // 이름 입력받기
  const nameChangeHandler = (event) => {
    setUserName(event.target.value);
  };

// 나이 입력받기
  const ageChangeHandler = (event) => {
    setUserAge(event.target.value);
  };

// 입력 이벤트 함수
  const formSubmitHandler = (event) => {
    // 기본적으로 렌더링되도록 정의된 form동작 방지 함수 추가
    event.preventDefault();

// 유효성 검사 
    if ((userName.trim().length && userAge.trim().length) === 0) {
      setIsOpenModal(true);
      setIsAgeCheck(true);
      return;
    }
    if (Number(userAge.trim()) < 1) {
      setIsOpenModal(true);
      setIsAgeCheck(false);
      setUserName("");
      setUserAge("");
      return;
    }

// 새로운 유저 정보 추가(객체형태)
    const newUserInfo = {
      id: Math.random().toString(),
      name: userName,
      age: userAge,
    };

// props로 전달한 상태 변경하여 상태 끌어올리기
    props.onAddItem(newUserInfo);
// 인풋값 초기화    
    setUserName("");
    setUserAge("");
  };

  return (
    <Card className={styles.container}>
      <form onSubmit={formSubmitHandler}>
        <div className={styles["form-userName"]}>
          <label htmlFor="username">Username</label>
          <input
            type="text"
            id="username"
            value={userName}
            onChange={nameChangeHandler}
          />
        </div>
        <div className={styles["form-age"]}>
          <label htmlFor="age">Age (Years)</label>
          <input
            type="number"
            id="age"
            value={userAge}
            onChange={ageChangeHandler}
          />
        </div>
        <Button type="submit">Add User</Button>
      </form>
      {isOpenModal && (
        <Modal
          title="Invalid input"
          message={
            !isAgeCheck
              ? "Please enter a valid age (Must be a number greater than 0)."
              : "Please enter a valid name and age(non-empty values)."
          }
          onOpen={setIsOpenModal}
        />
      )}
    </Card>
  );
}

export default AddUserForm;

입력을 요하는 input에 label을 연결하고 입력 후 클릭이벤트가 발생하는 버튼은 재사용가능한 Button컴포넌트를 적용했으며, 전반적으로 동일한 UI를 구현하기에 감싸는 컴포넌트는 Card컴포넌트를 적용했다.


1-1 "Button.js 와 Card.js 추가"

  • Button.js
import styles from "./Button.module.css";

const Button = (props) => {
  return (
    <button type={props.type || "button"} className={styles.button}>
      {props.children}
    </button>
  );
};

export default Button;

버튼컴포넌트에선 해당 컴포넌트를 사용하는 곳에서 props로 전달 받는 type과 css모듈로 지정한 className속성으로 버튼요소를 지정하며, props.children으로 사용되는 컴포넌트의 컨텐츠를 렌더링 해주도록 구성했다.


  • Card.js
import styles from "./Card.module.css";

function Card(props) {
  return (
    <div className={`${styles.card} ${props.className}`}>{props.children}</div>
  );
}

export default Card;

Button컴포넌트와 마찬가지로 적용된 컴포넌트에서 전달받은 props로 조건에따라 className을 설정해 주도록 했다.


1-2 "Modal.js 추가"

import Button from "./Button";
import Card from "./Card";

import styles from "./Modal.module.css";

function Modal(props) {
  return (
    <div
      className={styles["modal-backDrop"]}
      onClick={() => props.onOpen(false)}
    >
      <Card className={styles["modal-container"]}>
        <div onClick={(event) => event.stopPropagation()}>
          <header className={styles["modal-title"]}>
            <h2>{props.title}</h2>
          </header>
          <div className={styles["modal-content"]}>
            <p>{props.message}</p>
          </div>
        </div>
        <footer>
          <Button type="button" onClick={() => props.onOpen(false)}>
            Okay
          </Button>
        </footer>
      </Card>
    </div>
  );
}

export default Modal;

그리고, 유효하지 않는 사용자 입력값에대해 오류가 발생하면 Modal컴포넌트를 호출하도록 상태를 지정하여 조건에 맞게 관리해 주었다.



트러블 슈팅❗️

이벤트 버블링?

해당 모달을 추가하면서 클릭 이벤트가 해당 버튼과 바깥영역을 클릭했을때가 아닌 모달 내부까지 적용되는 일이 발생했다.

이는 자바스크립트의 이벤트특성에서 "이벤트 버블링"에 해당하는 개념으로 기본적으로 한 요소에 이벤트가 발생하면 그 요소에 할당된 헨들러가 동작하며 이어 상위로 연결되는 최상단의 부모까지 반복되며 발생하는 현상을 말한다.

그래서 이를 모달상태를 없애는 이벤트를 전달되지 못하도록 클릭되는 타켓요소만 발생시키고 아닌 요소는 event.stopPropagation() 메소드를 사용하여 방지하도록 구현했다.



2. "userList 추가"


입력받은 사용자 데이터를 화면에 그려줄 컴포넌트가 필요했고, 이를 userList컴포넌트와 그 리스트 컴포넌트 안에 모든 사용자 데이터에 해당하는 userItem을 보여줄 컴포넌트를 임포트해서 매핑시켜 구현하였다.

이 컴포넌트에서도 공통으로 활용되는 부분은 재사용하여 적용해주었다.(Card.js)

  • UserList.js
import UserItem from "../Item/UserItem";
import Card from "../../UI/Card";

import styels from "./UserList.module.css";


function UserList(props) {
  return (
    <Card className={styels["user-container"]}>
      <ul className={styels["user-list"]}>
        {props.users.map((user) => {
          return (
            <UserItem key={user.id} id={user.id} onDelete={props.onDeleteItem}>
              {user.name} ({user.age} years old)
            </UserItem>
          );
        })}
      </ul>
    </Card>
  );
}

export default UserList;

트러블 슈팅❗️

배열렌더링 오류?

리액트에서 데이터상태가 배열타입인경우, 이를 map함수를 사용해서 렌더링하게 될때 배열의 타입이 정확해야 오류가 나지 않고 화면에 올바르게 나타나게된다.

그래서 항상, 배열을 렌더링 하는 작업을 할때 타입과 해당 배열이 undefined가 아닌지 확인을 꼭 해주고 진행 시켜야한다.


  • userItem.js
import "./UserItem.css";

function UserItem(props) {
  const deleteHandler = () => {
    props.onDelete(props.id);
  };

  return (
    <li className="user-item" onClick={deleteHandler}>
      {props.children}
    </li>
  );
}

export default UserItem;

해당 userItem컴포넌트에선 삭제를 할 수 있도록 클릭을 받는 이벤트가 여기서 직접적으로 사용자의 입력을 받기 때문에 삭제헨들러를 적용해주지만, 기본적으로 App.js에서 전체 배열의 상태를 관리하기때문에 props로 전달받은 갱신함수로 삭제할 id를 받아 업데이트 시켜주도록 전달하여 구성했다.



3. "App.js 상태 관리 및 컴포넌트 배치"


앞서 언급한 컴포넌트들은 직접적으로 상태를 서로 공유하지 못하기때문에 그들의 상태를 공유해주기 위해 공통컴포넌트가 필요하다.

이를 App컴포넌트가 해결해줄 수 있으며, 여기서 사용자데이터를 관리해주고 해당하는 다양한 이벤트(추가, 삭제)를 선언해 공유해줄수도 있다.

import { useState } from "react";

import AddUserForm from "./components/Users/AddForm/AddUserForm";
import UserList from "./components/Users/List/UserList";

import Card from "./components/UI/Card";

function App() {
  const [userInfo, setUserInfo] = useState([
    { name: "suam", age: 27, id: "1" },
  ]);

  const addItemHandler = (newUser) => {
    setUserInfo((prevInfo) => {
      return [...prevInfo, newUser];
    });
  };

  const deleteItemHandler = (userId) => {
    setUserInfo((prevInfo) => {
      return prevInfo.filter((user) => user.id !== userId);
    });
  };

  return (
    <>
      <AddUserForm onAddItem={addItemHandler} />
      {userInfo.length > 0 ? (
        <UserList users={userInfo} onDeleteItem={deleteItemHandler} />
      ) : (
        <Card>
          <p style={{ padding: "1.45rem" }}>"no user Information"</p>
        </Card>
      )}
    </>
  );
}

export default App;

이렇게 상태를 관리해주고, 상태가 업데이트 되는 부분은 props(상태 및 상태갱신함수)로 전달해주는 하위컴포넌트에서 데이터를 받아오면 업데이트는 여기서 수행한다.



(추가) 개선해본 점


사용자의 입력을 받는 컴포넌트인 AddUserForm에서
앞서, 유효하지 못한 입력값에 따라 유효여부상태와 모달의상태 두개로 나누어 적용했는데
이를 하나의 상태로 합쳐볼까 생각을 해보았다.

결국 모달이 나오는것은 입력에 대한 오류로 발생되는 모듈이기때문에 error라는 하나의 상태로 유효여부와 모달오픈 상태를 적용해 코드를 간소화 하는게 더 효율적일 것이라고 생각했다.


변경점

  • 기존 AddUserForm의 상태인 isAgeCheck와 isOpenModal로 불린값형태로 관리해주었다면, error라는 객체형태로 상태를 지정해 오류의 조건에따라 다르게 title과 message를 바인딩해서 렌더링 될 수 있게 만들어 주었다.

  • error상태를 초기화해주는 errorHandler함수를 Modal컴포넌트에 props로 전달해주어 모달창 바깥과 Okay버튼 클릭시 error상태가 업데이트 되도록 했다.


AddUserForm.js

import { useState } from "react";

import Modal from "../../UI/Modal";
import Card from "../../UI/Card";
import Button from "../../UI/Button";

import styles from "./AddUserForm.module.css";

function AddUserForm(props) {
  const [userName, setUserName] = useState("");
  const [userAge, setUserAge] = useState("");
  const [error, setError] = useState();

  const nameChangeHandler = (event) => {
    setUserName(event.target.value);
  };

  const ageChangeHandler = (event) => {
    setUserAge(event.target.value);
  };

  const formSubmitHandler = (event) => {
    event.preventDefault();

    if ((userName.trim().length && userAge.trim().length) === 0) {
      setError({
        title: "Invalid input",
        message: "Please enter a valid name and age(non-empty values).",
      });
      return;
    }
    if (Number(userAge.trim()) < 1) {
      setError({
        title: "Invalid age",
        message: "Please enter a valid age (> 0).",
      });
      setUserName("");
      setUserAge("");
      return;
    }

    const newUserInfo = {
      id: Math.random().toString(),
      name: userName,
      age: userAge,
    };

    props.onAddItem(newUserInfo);
    setUserName("");
    setUserAge("");
  };

  const errorHandler = () => {
    setError(null);
  };

  return (
    <Card className={styles.container}>
      <form onSubmit={formSubmitHandler}>
        <div className={styles["form-userName"]}>
          <label htmlFor="username">Username</label>
          <input
            type="text"
            id="username"
            value={userName}
            onChange={nameChangeHandler}
          />
        </div>
        <div className={styles["form-age"]}>
          <label htmlFor="age">Age (Years)</label>
          <input
            type="number"
            id="age"
            value={userAge}
            onChange={ageChangeHandler}
          />
        </div>
        <Button type="submit">Add User</Button>
      </form>
      {error && (
        <Modal
          title={error.title}
          message={error.message}
          onToggle={errorHandler}
        />
      )}
    </Card>
  );
}

export default AddUserForm;

Modal.js

import Button from "./Button";
import Card from "./Card";

import styles from "./Modal.module.css";

function Modal(props) {
  return (
    <div className={styles["modal-backDrop"]} onClick={props.onToggle}>
      <Card className={styles["modal-container"]}>
        <div onClick={(event) => event.stopPropagation()}>
          <header className={styles["modal-title"]}>
            <h2>{props.title}</h2>
          </header>
          <div className={styles["modal-content"]}>
            <p>{props.message}</p>
          </div>
        </div>
        <footer>
          <Button type="button" onClick={props.onToggle}>
            Okay
          </Button>
        </footer>
      </Card>
    </div>
  );
}

export default Modal;

Button.js

import styles from "./Button.module.css";

const Button = (props) => {
  return (
    <button
      type={props.type || "button"}
      className={styles.button}
      onClick={props.onClick}
    >
      {props.children}
    </button>
  );
};

export default Button;




느낀점


위 프로젝트는 아주 간단하지만 앞으로의 리액트의 다양한 기능과 포맷을 적용하기 앞서 연습해보기 좋았던 모듈이였다.

컴포넌트, 빌딩 state, props, state끌어올리기, useState활용, css모듈방식적용, 함수전달 하는것까지 배울 수 있었다.

좀 더 다양한 리엑트 기능을 점목하고 서버와 데이터 패칭 그리고 코드를 최적화 하는것 까지 더 학습하여 또 다른 프로젝트를 만들어 보고싶다.!!

profile
마라토너같은 개발자가 되어보자

0개의 댓글