프로젝트

[React & Typescript 프로젝트] 코드리뷰 / 피드백

sian han 2022. 12. 31. 01:22

Github : https://github.com/HAN-SEOHYUN/Happy-Christmas

 

※ BackEnd

 

▶ data-source.ts

require("dotenv").config();
export const AppDataSource = new DataSource({
    ~ other code ~
    host: process.env.MYSQL_HOST, 
    port: Number(process.env.MYSQL_PORT),
    username: process.env.MYSQL_USERNAME,
    password: process.env.MYSQL_PASSWORD,
    database: process.env.MYSQL_DATABASE,
  });

DataSource 객체를 생성해 DB 연결을 관리하기 위한 환경변수들을 입력해줌.
먼저 [.env] 파일을 생성해 환경변수를 입력하고, 

dotenv 모듈을 설치해서 process.env 객체에 저장된 환경변수들을 불러옴

 

 

▷ .env 란 ? 

  • Node.js 에서 사용하는 환경변수를 저장하기 위한 파일.
  • git 과 같은 VCS 에 관리되지 않는다.
  • 일반적으로 .env 파일은 어플리케이션을 실행할 때 읽어지고, 읽어진 환경변수들은 process.env 객체에 저장된다.
  • 이 객체는 Node.js 어플리케이션 전역에서 접근이 가능하다. 

 

▷ dotenv 는 무엇인가 ?

  • .env 파일을 읽어서 환경변수를 설정해주는 Node.js 모듈
  • dotenv 를 통해 .env 환경변수를 process.env 객체에 저장할 수 있다. 

 

  DataSource 객체 ?

  • DB 연결을 관리하기 위한 객체

 

 

 

index.ts

AppDataSource.initialize()
  .then(async () => {
    // create express app
    const app = express();
    
    //cors
    app.use(cors({
        origin:'http://localhost:3000'
    }));
    
    //body-parser
    app.use(express.json());
    app.use(express.urlencoded({ extended: true }));

    //routes
    app.use("/api/to", toRouter);
    app.use("/api/from", fromRouter);

    // run app
    app.listen(PORT);

    console.log("Server running on port: " + PORT);
  })
  .catch((error) => console.log(error));

initialize 메소드가 실행되고, 초기화 작업이 완료되면 콜백함수가 실행됨.

 

 

  콜백함수 실행단계

  1. express 앱 생성
  2. cors 설정
  3. body-parser
  4. 라우팅

 

initialize() 란 ?

  • initialize() 메소드는 앱을 시작할 때 필요한 초기화 작업을 수행하는 메소드
  • 실행할때마다 데이터베이스 스키마가 동기화됨

 

 body parser 사용이유 ?

  • Express.js에서 body-parser 미들웨어를 사용하는 이유는 HTTP request 에서 데이터를 추출하기 위함

 

 

 

 

Entity-FromSian

@Entity()
  export class FromSian {
    
    @PrimaryGeneratedColumn()
    id: number;
  
    @Column()
    recipient: string;

    @Column()
    pwd : string;
    
    @Column()
    message : string;

    @Column({default : ()=>'CURRENT_TIMESTAMP'})
    createdAt : Date;
  }

 

@Entity 데코레이터를 사용해서 내가 작성한 메세지 정보를 관리하는 FromSian 테이블 생성

 

 

 

 

 

 Entity-ToSian

@Entity()
  export class ToSian {
    @PrimaryGeneratedColumn()
    id: number;
  
    @Column()
    sender: string;
    
    @Column()
    message : string;

    @Column({default : ()=>'CURRENT_TIMESTAMP'})
    createdAt : Date;
  }

 

나에게 작성된 메세지 정보를 관리하는 ToSian 테이블

 

 

 

 

▶ 라우터-from.routes.ts

const router = express.Router();

router.get('/recipient/:recipient',fromController.getCountByName); //수정 전
router.get('/recipient',fromController.getCountByName);//수정 후
router.get('/:id', fromController.getMessageById);
router.post('/',fromController.confirmPassword);

export default router;

피드백 : path parameter => query string 으로 변경하면 역할을 명확하게 구분할 수 있을 것 같음

 

 

 

 

 라우터-to.routes.ts

const router = express.Router();

router.post('/',toController.saveMesage);

export default router;

데이터가 향하는 테이블 별로 라우터 파일을 분리.

 

 

 

 

▶ 컨트롤러-FromController

const fromSianRepository = AppDataSource.getRepository(FromSian);

FromSian 엔티티에 관련된 작업을 처리하는 Repository 를 가져옴

 

//이름을 넣으면 이름에 해당하는 row 의 count를 알려줌
export async function getCountByName(req:Request, res:Response){
    let inputRecipient = req.params.recipient;

    await fromSianRepository
    .findAndCount({where : {recipient: inputRecipient}})
    .then((object)=> {
        res.send({
            success: true,
            data : {
                count: object[1]
            }
        })
    })
    .catch((err)=>console.log(err));
}
.
.
.

 피드백 

1. async await 를 사용하면 then() 을 사용할 필요가 없음 ! 

2. path parameter => query string 변경

3. 변수명 object 적절하지 않음

4. if 문 사용할때는 != 사항을 먼저 기재하면 가독성이 높아짐

 

 

피드백 적용 후

const fromSianRepository = AppDataSource.getRepository(FromSian);

export async function getCountByName(req:Request, res:Response){
    const inputRecipient = req.query.recipient as string;

    try{
        const [fromSian_object,count] = await fromSianRepository
        .findAndCount({where : {recipient: inputRecipient}});

        res.send({
            success: true,
            data : {
                fromSian_object, 
                count
            }
        })
    }catch(err){
        console.log(err);
    }
}

export async function getMessageById(req:Request, res:Response){
    const inputId = parseInt(req.params.id);

    try{
        const fromSian_object = await fromSianRepository
        .findOneBy({
            id: inputId,
        });
    
        res.send({
            data : {
                id : fromSian_object?.id,
                recipient : fromSian_object?.recipient,
                createdAt : fromSian_object?.createdAt,
                message : fromSian_object?.message,
            }
        })
    }catch(err){
        console.log(err);
    }
}

export async function confirmPassword(req:Request, res:Response){
    const inputRecipient = req.body.recipient;
    const  inputPwd = req.body.pwd;

    try{
        const fromSian_object = await fromSianRepository
        .findOneBy({
            recipient: inputRecipient
        });
    
        if(fromSian_object?.pwd !== inputPwd){
            res.send({
                success:false
            });
        }
    
        res.send({
            success: true,
            data : {id : fromSian_object?.id}
        });
    }catch(err){
        console.log(err);
    }
}

 

  • getCountByName : 요청으로 받은 이름에 해당하는 데이터가 있는지 확인을 하고, 데이터가 존재할 경우에 응답을 보내줌
  • getMessageById : 요청으로 받은 아이디에 해당하는 메세지 객체를 응답하는데, pwd 는 보내주지 않음
  • confirmPassword : 요청 받은 name 과 pwd 가 일치할 경우 success 여부와 data 를 응답하고, 그렇지 않을 경우 false 를 응답

 

 

 

 

 컨트롤러-ToController

export async function saveMesage(req:Request, res:Response){

    try{
        const toSian_object = await toSianRepository.save(req.body);
        
        res.send({
            status : "success",
            data : toSian_object
        })
    }catch(err){
        console.log(err);
    }
}

saveMessage : 레포지토리의 save() 메서드를 사용해서 요청된 값을 저장해주고 (메세지 작성) status와 data 를 리턴

 

 

▷ Repository 란 ?

  • 각 엔티티는 엔티티관련 작업을 처리하는 Repository 를 가지고 있다.

 

 


※ FrontEnd

 

▶ routes.tsx

const routes: Route[] = [
  {
    path: "/",
    component: HomePage,
  },
  {
    path: "/detail",
    component: DetailMessage,
  },
  {
    path: "/register",
    component: CreateMessage,
  },
  {
    path: "*",
    component: NotFoundPage,
  },
];

export default routes;

 

Route 배열에 경로와 해당 컴포넌트를 넣어줌

위의 경로중 어떤 것도 해당하지 않을 때 404 페이지로 이동됨

 

 

▶ App.tsx

const App: React.FC = () => {
  return (
    <Router>
      <NavBar />
      <div className="container mt-3">
        <Switch>
          {routes.map((route,index) => {
            return (
              <Route
                key={route.path}
                exact
                path={route.path}
                component={route.component}
              />
            );
          })}
        </Switch>
      </div>
    </Router>
  );
};

App.tsx 에서 map 함수의 인자로 import 된 routes 배열을 넣고 routes 값을 Route 컴포넌트 속성의 값으로 각각 할당

=> 여러개의 Route 컴포넌트를 리턴하는 구조


▷ map 함수 ?

인자로 주어진 요소에 어떤 함수를 적용한 결과를 새로운 배열로 만들어 리턴한다.


 Route 컴포넌트 ?

Route 컴포넌트는 주소가 지정된 경로와 일치할 때 컴포넌트를 렌더링한다.


 Switch 컴포넌트 ?

Route 컴포넌트 중 첫번째로 매칭되는 path 를 가진 컴포넌트를 렌더링시킨다.

주소가 일치하는 Route 컴포넌트가 찾아지면 그 이후의 Route 컴포넌트는 처리되지 않는다. 


 Switch 를 사용하지 않는다면 ?

컴포넌트가 중복되어 렌더링 될 수 있다. 

 

 

▶ NavBar.tsx

const NavBar = () => {
  return (
	~ other codes ~
          <Link
            className="btn btn-success me-2"
            type="button"
            id="from-Btn"
            onClick={() => {
              if (window.location.pathname === "/") {
                window.location.reload();
              }
            }}
            to="/"
          >
        ~ other codes ~
  );
};
export default NavBar;

HomePage에서 From.Sian 버튼을 누르면 새로고침이 되도록.

NavBar 컴포넌트는 페이지가 이동해도 상단에 고정되어야하기 때문에 App.tsx 에서 import

 

 

▶ HomePage.tsx

const HomePage: React.FC = () => {
  const [recipient, setRecipient] = useState<string>("");
  const [count, setCount] = useState<number>();

  const handleMessageCount = async () => {//
    try {
      const res = await getCountByName(recipient);
      setCount(res.count);

      if (!res.count) {
        alert(recipient + "에게 등록된 메세지가 없습니다");
      }
    } catch (err) {
      alert(err);
    }
  };

 피드백 

string 연결 시 '+' 연산자 사용 지양

 

if (!res.count) {
	alert(`${recipient}에게 등록된 메세지가 없습니다`);
}
return (
    <div>
      <div className="d-flex justify-content-center mb-3">
        <img
          className="w-50 "
          src="img/merry-christmas-word.jpg"
          alt="merry-christmas-word"
        ></img>
      </div>
      <div className="my-4">
        <p className="font-size-md">
          <b>
            이름을 입력하면 등록된 편지가 있는지 확인할 수 있습니다.
            <br />
            방문해주신 모든분들 남은 한 해 즐겁고, 행복하게 마무리 잘 하시고
            <br /> 즐거운 크리스마스 보내시길 바랍니다.
          </b>
        </p>
      </div>

      <div>
        <div className="input-group mb-3">
          <input
            type="text"
            name="recipient"
            className="form-control"
            placeholder="이름을 입력하세요 ex) 홍길동"
            aria-label="name"
            aria-describedby="button-addon2"
            onChange={(event) => {
              setRecipient(event.target.value);
              //alert 카드 이름이 수정되는 오류 막아주기
              setCount(0); //등록된 이름 없음(0) 으로 처리해서 value 수정 시 alert 카드가 안보이도록
              
            }}
          />
          <button
            className="btn btn-outline-secondary"
            name='check-Btn'
            type="button"
            disabled={!recipient}
            onClick={handleMessageCount}
          >
            확인하기
          </button>
        </div>
        {count ? <AlertCard recipient={recipient} />: null}
      </div>
    </div>
  );
};

export default HomePage;

 

이름을 입력하고 확인하기 버튼을 누르면 getCount() 메서드 실행됨

getCount() 메서드는 입력된 이름을 서버에 보내서 해당이름으로 등록된 메세지가 있는지 확인해줌

응답받은 count 데이터를 useState 에 넣고, 만약 응답받은 데이터에 count 가 없을 경우 Alert

count 가 있을 경우 AlertCard 컴포넌트를 보여주고, 이 컴포넌트에 props 로 입력된 이름(recipient)을 전달

 

▷ React.FC 란 ? 

  • 함수형 컴포넌트를 정의할 때 사용하는 타입

 

 피드백 

삼항연산자보다 && 연산자 사용

 

 

 

▶ AlertCard.tsx

const AlertCard: React.FC<Props> = ({ recipient }) => {
  const history = useHistory();
  const [pwd, setPwd] = useState<string>("");
  let isConfirmed = false;

  const handleMoveDetail = (id: number, isConfirmed: boolean) => {
    history.push({
      pathname: "/detail",
      state: {
        id: id,
        isConfirmed: isConfirmed,
      },
    });
  };

  //비밀번호가 일치하는지 확인해줌
  const handleCheckPwd = async () => {
    try {
      const res = await checkPwd(recipient, pwd);

      if(!res){
        alert("비밀번호가 올바르지 않습니다 !");
      }else{
        isConfirmed = true;
        handleMoveDetail(res.id, isConfirmed);
      }
      
    } catch (err) {
      alert(err);
      history.push(`/`);
    }
  };

  return (
    <div className="alert-card">
      <hr />
      <div className="card text-center">
        <div className="card-body">
          <h5 className="card-title">
            <FontAwesomeIcon icon={faEnvelope} />
            &nbsp;{recipient} 님 에게
          </h5>
          <p className="card-text">전달된 크리스마스 메세지가 1개 있습니다</p>

          <div className="input-group mb-3">
            <span className="input-group-text" id="basic-addon1">
              비밀번호
            </span>
            <input
              type="text"
              name ="pwd-input"
              className="form-control"
              placeholder="비밀번호입력"
              onChange={(event) => {
                setPwd(event.target.value);
              }}
            />
          </div>
          <button 
          className="btn btn-primary" 
          name="checkPwd-Btn" 
          onClick={handleCheckPwd}>
            메세지 읽기
          </button>
        </div>
      </div>
    </div>
  );
};

export default AlertCard;

AlertCard 컴포넌트에서 Props(recipient) 를 전달 받음

비밀번호를 입력하고 확인 버튼을 누르면 checkPwdHandler 메서드 실행

 

checkPwdHandler :입력된 비밀번호를 서버에 요청해서 일치 여부를 응답받고 비밀번호가 일치할 경우 moveDetail 메서드 실행, 일치하지 않을 경우에는 Alert 발생

 

moveDetail : 상세페이지로 이동을 시켜주고, state 로 id 와 비밀번호를 일치하는지에 대한 정보를 담은 isConfirmed 를 전달

 

 

 

▶ DetailMessage.tsx

const DetailMessage: React.FC = () => {
  const location = useLocation();
  const history = useHistory();

  const [message_object, setMessage_object] = useState<Message | null>(null);
  const [loading, setLoading] = useState(true);

  const state = location.state as PropsType;

  //메세지 객체를 가져와서 페이지에 정보를 뿌려줌
  const handleShowMessage = async (id: number, isConfirmed: boolean) => {
    if (!isConfirmed) {
      alert("올바른 접근이 아닙니다 !");
      history.push(`/`);
    }
    try {
      const message = await getMessageById(id);
      setMessage_object(message);
    } catch (err) {
      alert(err);
    }
    setLoading(false);
  };

  useEffect(() => {
    handleShowMessage(state.id, state.isConfirmed);
    console.log(location);
  }, [state.id]); //eslint-disable-line

  return (
    <>
      {loading ? (
        <LoadingSpinner />
      ) : (
        <>
          <div>
            <div className="d-flex">
              <h1 className="flex-grow-1">{message_object?.recipient || "없음"}</h1>
            </div>
            <small className="text-muted">{message_object?.createdAt}</small>
            <hr />
            <p>{message_object?.message}</p>
          </div>
          <Link id="write-Btn" className="btn btn-primary" to={`/register`}>
            편지쓰기
          </Link>
        </>
      )}
    </>
  );
};
export default DetailMessage;

moveDetail 에서 state 로 보낸 props 들을 state 에 담고,
useEffect를 통해서 컴포넌트가 마운트 될 때 getMessage 메서드가 실행되도록.

getMessage 메서드는 서버로부터 id 에 해당하는 메세지 객체를 받아오고

만약 응답받은 메세지 객체가 있다면, Object useState 에 할당되어 화면에 정보가 뿌려지고, loading useState 가 false 로 변경됨
따라서 서버로부터 응답을 받기 전까지 사용자는 loadingSpinner 라는 컴포넌트를 보게됨

 

 

▷ 마운트 ? 

 리액트에서는 컴포넌트가 처음 렌더링될 때 '마운트' 된다고 함 

 

Link ?

새로고침하지 않고 서로 다른 라우트 사이를 이동하는 것을 돕는 컴포넌트

 

 

 

▶ CreateMessage.tsx

const CreateMessage: React.FC = () => {
  const history = useHistory();
  const [sender, setSender] = useState<string>("");
  const [message, setMessage] = useState<string>("");

  const handleMessageSubmit = () => {
    try {
      postToSian(sender, message);
      alert(`${sender}님의 메세지가 등록되었습니다. HappyChristmas!`);
      history.push(`/`);
    } catch (err) {
      alert(err + "에러");
      history.push(`/`);
    }
  };

  return (
    <>
      <h1 className="">
        Write a Christmas Letter to <span className="font-size-md">Sian</span>
        &nbsp;
        <FontAwesomeIcon icon={faGift} />
      </h1>
      <hr />
      <div className="mb-3">
        <label className="form-label">작성자</label>
        <input
          className="form-control"
          name="sender"
          onChange={(event) => {
            setSender(event.target.value);
          }}
        />
      </div>
      <div className="mb-3">
        <label className="form-label">Christmas Message</label>
        <textarea
          className="form-control"
          rows={10}
          name="message"
          onChange={(event) => {
            setMessage(event.target.value);
          }}
        />
      </div>

      <button
        className="btn btn-primary"
        onClick={handleMessageSubmit}
        name="create-Btn"
        disabled={!sender || !message}
      >
        작성하기
      </button>

      <Link
        className="btn btn-danger ms-2"
        to="/"
        id="home-Btn"
      >
        돌아가기
      </Link>
    </>
  );
};

export default CreateMessage;

등록 페이지에서는 작성자와 내용을 입력하고 작성하기 버튼을 클릭하면 onSubmit 메서드가 실행됨

onSubmit 메서드는 입력된 작성자와 내용을 등록시켜주는데, 등록이 완료되면 Alert 이 뜨고 메인페이지로 이동됨

작성자와 내용 둘중에 하나라도 입력이 되지않으면 버튼은 비활성화