서버와 클라이언트 폴더를 분리하고 그거에 맞춰서 만들 예정이다.

 

클라이언트 폴더는 myblog

서버 폴더는 myblog_server

이다

 

클라이언트 폴더에는 React + TypeScript를 설치하여 셋팅해준다

yarn create react-app myblog --template typescript

 

서버 폴더에는 express + mysql + sequelize 를 이용하여 데이터와 연결해 줄 것이다.

yarn add express mysql2 sequelize sequelize_cli

 

서버 폴더에서 먼저 .gitignore 파일을 만들어 node_modules 파일이 올라가지 못하게 만들어두고,

npx sequelize init을 입력하여 시퀄라이즈 사용에 필요한 폴더와 파일들을 만들어준다.

 

config,json에서 데이터베이스와 연결해주고,

//config.json
{
  "development": {
    "username": "toss1",
    "password": "12345",
    "database": "myblog",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "test": {
    "username": "root",
    "password": null,
    "database": "database_test",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": null,
    "database": "database_production",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}

 

 

/models/index.js에서 필요한 부분만 남겨 만들어준다. 그리고 모델을 만들 파일을 연결해 줘야한다.

'use strict';

const Sequelize = require('sequelize');
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};
const sequelize = new Sequelize(config.database, config.username, config.password, config); 


db.User = require(./user)(sequelize);	

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

 

 

그 후에 일전에 만들어준 ERD에 맞춰서 models 폴더에 원하는이름.js를 만들어  테이블을 정의해준다.

const {DataTypes} = require("sequelize");
const Model = (sequelize) => {
    const User = sequelize.define(
        "User",
        {
            user_id: {
                type: DataTypes.INTEGER,
                allowNull: false,
                primaryKey : true,
                autoIncrement: true,
            },
            nick_name: {
                type: DataTypes.STRING(255),
                allowNull: false,
            },
            email: {
                type: DataTypes.STRING(255),
                allowNull: false,
            },
            password: {
                type: DataTypes.STRING(255),
                allowNull: false,
            },
            name: {
                type: DataTypes.STRING(255),
                allowNull: false,
            }
        },
        {
            tableName : "User",
            timestamps: false,
        }
    );
    
    const Bloging = sequelize.define(
        "Bloging",
        {
            blog_id : {
                type: DataTypes.INTEGER,
                allowNull: false,
                primaryKey : true,
                autoIncrement: true,
            },
            title : {
                type: DataTypes.STRING(255),
                allowNull:false,
            },
            contents : {
                type: DataTypes.TEXT,
                allowNull:false,
            }
        },
        {
            tableName: "Bloging",
        }
    );

    const Img = sequelize.define(
        "Img",
        {
            image_id: {
                type: DataTypes.INTEGER,
                allowNull: false,
                primaryKey : true,
                autoIncrement: true,
            },
            blog_id: {
                type: DataTypes.INTEGER,
                allowNull: false,
            },
            image_url: {
                type: DataTypes.STRING(255),
                allowNull: false,
            }
        },
        {
            tableName: "Img",
            timestamps: false,
        }
    );
    
    const MyBloging = sequelize.define(
        "MyBloging",
        {
            user_id : {
                type: DataTypes.INTEGER,
                allowNull: false,
            },
            blog_id : {
                type: DataTypes.INTEGER,
                allowNull: false,
            }
        },
        {
            tableName: "MyBloging",
            timestamps: false,
        }
    );

    //관계 정의
    MyBloging.belongsTo(User, { foreginKey : "user_id"});
    MyBloging.belongsTo(Bloging, { foreginKey : "blog_id"});

    return {
        User,
        Bloging,
        Img,
        MyBloging,
    }
}

module.exports = Model;

 

 

이렇게 한 후에 서버를 열어줄 app.js를 만들어주고 서버를 열어주자

 

//app.js

const express = require("express");
const db = require("./models");
const path = require("path");
const jwt = require("jsonwebtoken"); //jwt 암호화에 필요

//s3에 필요
// const aws = require("aws-sdk"); // aws-sdk 모듈 추가
// const multer = require("multer"); // multer 모듈 추가
// const multerS3 = require("multer-s3");

const PORT = 8000;
const app = express();

//body-parser
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

// 정적 파일을 제공할 디렉토리를 설정
app.use(express.static(path.join(__dirname, "public")));

//router
const useRouter = require('./route/router');
app.use('/', useRouter);

//404
app.use('*', (req, res)=>{
    res.render('404');
});

//서버 오픈
db.sequelize.sync({force : true}).then(()=>{
    app.listen(PORT, () => {
        console.log(`http://localhost:${PORT}`);
    });
});

 

 

만들던 과정 중에 생각이 든건 s3를 백에서 사용하는데 지금 의미가 없었다...하핳 사전 과제는 프론트인데 백에서 s3를 사용할리가 없기 때문이다. 그래서 일단 데이터 호출한 부분까지만 정리해 올리고 목업 데이터를 이용한 기능 구현에 초점을 맞춰서 프론트 단에서만 개발을 할 예정이다.

 

2시간이 좀 아깝지만 남은 시간 빡세게 해보자ㅠㅠ

 

/route/router

const express = require('express');
const router = express.Router();
const controller = require('../controller/Cblog');

router.get('/', controller.get_blog);

module.exports = router;

 

 

/controller/Cblog

const db = require('../models');
const models = db.User;

const get_blog = async(req, res) => {

    const blogContents = await models.User.findAll({});

    console.log("받아온 블로그 데이터", blogContents);
}

module.exports = {
    get_blog,
}

 

 

이렇게 하면 로컬 데이터베이스에서 데이터 가져오는 게 가능하다.

 

이 글은 여기서 마무리하고 목업 데이터를 이용한 프론트엔드 블로그 만들기를 해봐야겠다

emotion을 적용하기 위해 tsconfig.json에

"jsxImportSource": "@emotion/react"

를 입력하자 마자 오류가 생겼다. 

오류 내용 : 

Cannot find module '@emotion/react/jsx-runtime' or its corresponding type declarations

알고 보니까 이건 VS code의 버전 문제였다.

>TypeScript의 버전을 WorkSpace 버전으로 바꾸면 바로 에러가 사라진다.

 

다음으로 CSS  Props 방식으로  사용해보자

 

그리고 그냥 쓰지 말고 최상단에

/** @jsxImportSource @emotion/react */

 

해당 코드를 적어줘야하는데 이는 JSX pragma라고 하는데 Babel 트랜스파일러한테 JSX 코드를 변환할 때, React의 jsx()함수가 아니라 Emotion의 jsx()함수를 대신 사용하라고 알려주기 위해 사용한다.

이를 입력하면 css Props 형식으로 만든 스타일이 적용된다. 

 

이렇게 해서 

// 문자형
const styled = css`
	스타일 값
`

// 객체형
const styled = {css({
	스타일 값
})}

 

해당 방식으로 정의한 후 엘리멘트에 Props 형식으로 보내주면 된다.

 

시간이 부족한 관계로 각 div 크기 고정만하고 마무리했다.

 

 

주식 탭, 혜택 탭 >> 웹으로 구현

 

웹 서비스에서 가장 다루기 어려운 부분은 무엇인가?

>> 비동기 프로그래밍

- 순서가 보장되지 않는 상황에서

- 좋은 사용자 경험을 위해 필수 

 

자바 스크립트에서는

- Callback

- Promise

- RxJS

를 이용해 비동기적인 상황을 다루고 있다.

 

좋은 코드의 원칙을 알아보자

 

(1)

x.foo.bar.baz에 안전하게 접근하기 위한 코드

>> 함수가 하는 일에 비해 코드가 너무 복잡하다.

>>> 각 프로퍼티에 접근하는 핵심 기능이 코드로 잘 드러나지 않는다.

 

Optional Chaining 문법을 활용한 동일한 함수 ▼

- 코드가 간결하다

- 성공한 경우를 생각하는 x.foo.bar.baz와 문법적 차이가 크지 않다.

- 함수의 역할을 한눈에 파악할 수 있다.

 

(2)

자바스크립트에 Propmise가 없던 시절 비동기 처리를 하기 위한 코드

비동기 처리를 위해 callback 사용

- 성공하는 경우와 실패하는 경우가 나뉘어 있지 않다.

- 코드를 작성하는 입장에서 매번 에러 유무를 확인해야한다.

 

async/await을 사용한 같은 함수 ▼

- 성공하는 경우만 다룸

- 실패하는 경우에 대한 처리는 외부에 위임 가능

 

좋은 코드의 특징

성공하는 경우와 실패 경우를 분리해 처리할 수 있다.

비즈니스 로직을 한눈에 파악할 수 있다.

 

 

 좋지 않는 코드

 

실패하는 경우에 대한 처리를 외부에서 위임하게 만들어보자

 

 

복잡한 상황

 

2개의 비동기 리소스를 가져올 떄의 상태

 

 

성공하는 경우에 집중하여 코드의 복잡도를 낮춘다 

일반적으로 작성하는 동기 로직과 큰 차이가 없다.

 

리액트가 비동기 처리가 어려웠던 이유

 

이에 대한 해결책으로 리액트 팀이 제시한 것이 바로

 

목표로 하는 코드 

- 컴포넌트는 성공한 상태만 다룬다

- 로딩 상태와 에러 상태가 분리되어 외부에서 다룬다.

- 동기와 거의 같게 사용할 수 있게 된다.

 

React Suspence는 위와 같은 useAsycnValue 같은 hook을 쉽게 만들 수 있도록 Low-Level API를 제공한다.

 

로딩 상태와 에러 처리를 컴포넌트가 사용되는 곳에서 쓴다.

 

비동기 호출을 하는 함수나 컴포넌트를 가운데 두고, 실패하는 부분을 처리하는 부분을 감싸게 만든다. 

 

fallback으 무엇인가?

 

사용하는 방법

 

runPureTask의를 통해 비동기 함수를 동기적으로 작성할 수 있다.

 

 

 

 

Recoil과 Suspense 사용해보기

 

 

Hooks를 사용하면서 얻은 이점

 

Suspense의 이점

 

 

 

를 이용한 사용자 경험 향상

'기초부터 다시 > SLASH' 카테고리의 다른 글

SLASH 21 - 실무에서 바로 쓰는 Frontend Clean Code  (1) 2024.01.29
SLASH 21 - TDS로 UI 쌓기  (1) 2024.01.28

리액트 자습서를 참고해 만든다 

 

자습서: React 시작하기 – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org

 

리액트에 대한 기초 설명을 알려주면 진행된다. 리액트는 우리가 아는 것과 같이 최상위 컴포넌트에서 하위 컴포넌트로 Props를 전달할 수 있다. 

 

사용할 리액트 컴포넌트 

1. Square : button 렌더링
2. Board : 9개의 사각형 렌더링

3. Game

 

클릭 시 박스의 값이 바뀌는 상태를 만들어주고 플레이어 X와 Y를 번갈아가며 띄워주면 끝나는 기능이다. 기능을 구현한 후에 자습서를 보며 잘 되어 있는지 확인 후 emotion을 이용해 스타일을 꾸미고 마무리할 예정이다.

 

먼저 자식 컴포넌트부터 구현해보자

가장 하위 자식 컴포넌트인 Square은 부모 컴포넌트에서 주는 값을 받아 채워 넣어주기만 하면 된다.

 

일단 플레이어가 서로 바뀌며 판을 놓는 기능을 완성했다. 클래스형으로 짜여진 코드를 함수형으로 바뀌면서 시간이 좀 오래 걸렸고, 컴포넌트 사이의 이펙트를 잘 못 이해해서 조금 오래 걸렸다ㅠㅠ 틱택토를 빨리 완성하고 리액트 블로그를 만들어보며 토스 사전 과제를 준비해 볼 예정이다. 아직 기본기가 많이 부족한 것을 느꼈다.

 

문제 1. 특정 버튼을 클릭하고 다른 버튼을 클리할 때 플레이어가 변경되어야 하는데 변경되지 않았고 특정 버튼을 두 번 눌러야 플레이어가 변경됐다.

 

처음엔 보드 컴포넌트에 들어오는 판의 위치값인 i가 스퀘어 컴포넌트로 전달되지 않아 생기는 오류라고 생각해서 두 컴포넌트 사이에 i를 연결할려고 노력했다. 스퀘어에 지정된

onClick : () => void

이 타입에서 void가 아니라 리턴값을 가진 함수 타입으로 바꿀려고 했는데 해결책을 못찾았다. (사실 없는거였다. 하위 컴포넌트에서는 클릭되었다는 신호만 올려주면 되는거였다.) 결론적으로 게임 컴포넌트에서 받은 판의 위치값인 i는 보드 컴포넌트의 핸들클릭 함수에 파라미터 값으로 들어가게 하면 되는 문제라 핸들클릭에 i를 적어 number로 ts를 지정해주고, porps인 i를 보드 컴포넌트의 return에서 받아서 핸들클릭 함수로 보내주면 되는거였다.

 

그리고 특정 버튼을 두 번 눌러야 바뀌는 이유는 최상위 컴포넌트인 게임 컴포넌트에서 플레이어의 값을 확인하고 보내줘야하는데 게임 컴포넌트에서 플레이어값을 정한게 아니라 보드 컴포넌트에서 정했기 때문에 특정 버튼을 누른 후 다른 버튼을 눌러도 바뀌지 않던 것이었다. 그래서 플레이어 값 스테이트를 게임 컴포넌트로 올리며 문제를 해결했다.

 

하하 너무 주절주절 적은것 같지만 일단 생각나는 데로 쭉 적고 있다. 추후에 정리해서 다시 올릴 예정이다.

 

코드에 기록한 내용 ▼

    // 핸들클릭 함수의 i를 스퀘어 컴포넌트에서 어떻게 받는가 고민하고 있었는데 생각해보니
    //여기서 i는 그저 Props로 받은 i를 핸들클릭 함수로 보내기 위한 파라미터여서
    //하위 컴포넌트인 스퀘어에 있는 onClick 함수하고는 상관이 없는 거였다.
    //그냥 클릭된거를 전달해주면 보드 컴포넌트에서 알아서 해주니까 i를 넘길 생각은 안해도됨

    //이제 문제는 두 번 클릭해야 플레이어의 값이 바뀌는건데 아마 i를 파라미터로 전달해주면서 해결이 되지 않을까 싶다.
    //왜냐하면 i를 파라미터로 받기 전에는 비동기적으로 클릭할때만 i를 받아왔는데,
    //파라미터로 전달해줌으로써 0-8을 파라미터로 받는 모든 함수에서 작동한다는 거니까 가능하지 않을려나..?
    // 안되네...
    //드디어 됐다 제일 최산단 컴포넌트인 Game 컴포넌트에서 playerCheck state를 만들어주고 props로 넘기니까 된다.

 

조금 부끄럽다..하핳

 

문제 2. 컴포넌트 에러 

    //오류 1
    // Board.tsx:18 Warning: Cannot update a component (`Game`) while rendering a different component (`Board`).
    //컴포넌트를 렌더링 하면서 다른 컴포넌트가 또 렌더링되어 생기는 문제다
    //오류 코드
    //     const handleClick = (i:number) => {
    //        setSquares((prevSquare)=>{
    //            const copySquare = [...prevSquare];
    //            copySquare[i] = playerCheck ? "O" : "X";
    //            setPlayerCheck(!playerCheck);
    //            return copySquare;
    //        });
    //    }
    //위와 같이 짜버리면 Board 컴포넌트를 렌더링하는 도중에 playCheck가 바뀌며 Game 컴포넌트도 렌더링된다
    //그래서 playCheck를 바꿔주는 걸 squares가 바뀐 후에 실행하며 문제를 해결했다.

이것도 당연한건데 아휴..ㅠㅠ 이제 제대로 알았으니까 된거라고 생각한다.

 

계속 똑같은 개념에서 실수하고 있었다. 

승자를 결정하는 기능을 만드는 과정에서

보드의 배열을 board.tsx에 만들었는데 이건 9개의 다른 배열을 만든거라 당연히 한줄의 값이 같은 배열을 발견할 수가 없기 때문에 승자를 찾을 수 없던 것이었다.

 

배열을 최상위에 만들어야 적용이 되는 걸 계속 생각하지 않고 있어서 거의 2시간을 날렸다 하...ㅠㅠ 빨리 마무리하고 토스 사전 과제용 블로그를 만들어야겠다.

 

이제 완성했다

 

Game.tsx

import { useEffect, useState } from "react";
import Board from "./Board";

export default function Game(){

    const [playerCheck, setPlayerCheck] = useState<boolean>(true);  
    const [gameState, setGameState] = useState<string>("");
    const [squares, setSquares] = useState<string[]>(Array(9).fill(null));
    const [history, setHistory] = useState<string[][]>([Array(9).fill(null)]);

    const forMapArr1:number[] = [0, 1, 2];
    const forMapArr2:number[] = [3, 4, 5];
    const forMapArr3:number[] = [6, 7, 8];

    // const winner = winnerChecker(squares);
    // console.log("승자", winner);
    // if(winner){
    //     setGameState(`Winner : ${winner}`);
    // } else {
    //     setGameState(`Next Player : ${playerCheck ? "O" : "X"}`);
    // }

    // 위의 사례도 이전에 핸드클릭 함수 안에서 위너체크 함수를 실행시키고 winner 유즈스테이트에 넣었기 때문에 
    // 여기서 setGameState로 gameState에 결과를 넣는 순간 setwinner 유즈 스테이트가 동시에 실행되면서
    // 무한 루프를 돈것이다.
    // 드디어 해결

    const jumpTo = (index:number) => {
        setSquares(history[index]);
        if(index === 0) {
            setHistory([Array(9).fill(null)]);
        }
    }
    

    return (<div>
        <div>
            {gameState}
        </div>
        <div>
            <div>
                {forMapArr1.map((forMapArr1)=>{
                    return(<Board
                        i={forMapArr1} 
                        playerCheck={playerCheck} 
                        setPlayerCheck={setPlayerCheck} 
                        setGameState={setGameState} 
                        squares={squares} 
                        setSquares={setSquares}
                        history={history}
                        setHistory={setHistory}
                    />)
                })}
            </div>
            <div>
                {forMapArr2.map((forMapArr2)=>{
                    return(<Board
                        i={forMapArr2} 
                        playerCheck={playerCheck} 
                        setPlayerCheck={setPlayerCheck} 
                        setGameState={setGameState} 
                        squares={squares} 
                        setSquares={setSquares}
                        history={history}
                        setHistory={setHistory}
                    />)
                })}
            </div>
            <div>
                {forMapArr3.map((forMapArr3)=>{
                    return(<Board
                        i={forMapArr3} 
                        playerCheck={playerCheck} 
                        setPlayerCheck={setPlayerCheck} 
                        setGameState={setGameState} 
                        squares={squares} 
                        setSquares={setSquares}
                        history={history}
                        setHistory={setHistory}
                    />)
                })}
            </div>
        </div>
        <div>
            {history.map((history, index)=>{
                const desc = index ? 
                    `Go to Move #${index}` :
                    "Go to Start";
                return(<div key={index}>
                    <button onClick={()=>jumpTo(index)}>{desc}</button>
                </div>)
            })}
        </div>    
    </div>)
}

 

 

Board.tsx

import { useEffect, useState } from "react";
import Square from "./Square";
import winnerChecker from "../utills/WinnerChaecker";

interface Props_board {
    i : number;
    playerCheck : boolean;
    setPlayerCheck : React.Dispatch<React.SetStateAction<boolean>>;
    setGameState : React.Dispatch<React.SetStateAction<string>>;
    squares : string[];
    setSquares : React.Dispatch<React.SetStateAction<string[]>>;
    history : string[][];
    setHistory : React.Dispatch<React.SetStateAction<string[][]>>;
}

export default function Board({i, playerCheck, setPlayerCheck, setGameState, squares, setSquares, history, setHistory}:Props_board) {

    const [whoWinner, setWhoWinner] = useState<string | null>("");    

    const handleClick = (i:number) => {
        
        if(whoWinner || squares[i]){
            console.log("게임 끝");
            return
        }    
        setSquares((prevSquare)=>{
            const copySquare = [...prevSquare];
            copySquare[i] = playerCheck ? "O" : "X";
            return copySquare;
        });

        setHistory((prev) => [...prev, [...squares]]);
        setPlayerCheck(!playerCheck);
    }
    
    useEffect(()=>{
        // console.log("현재 배열", squares);
        
        const winner = winnerChecker(squares);
        setWhoWinner(winner);

        if(winner) {
            setGameState(`Winner : ${winner}`);
        } else {
            setGameState(`Next Player : ${playerCheck ? "O" : "X"}`)
        }
        
    },[squares ,playerCheck]);

    

    return(<>
        <Square 
        value={squares[i]}
        onClick={()=>handleClick(i)}
        />
    </>)
    // 여기서 플레이어값을 지정해버리면 하나의 스퀘어에서만 작동한다
    // 그래서 다른 값을 눌렀을 때 그 플레이어 값은 완전 다른 개별적인 값이기에 유기적이지 못한 상태인것
    // 굳

    // 계속 승자가 나왔는데 안나와서 답답했는데 위의 실수랑 똑같이 했었다. 각 보드에서 9개짜리 보드를 만들어도
    // 그 보드는 9개의 각 스퀘어에서 생성될뿐 서로 유기적으로 연결되어 있지 않다.
    // 그래서 계속 비교를 해도 한줄을 채운 보드의 모음은 확인할 수 없으니까 승자가 안나왔다.
    // 제발 한 번 더 생각하고 만들자
    // 내가 만든 이 보드 컴포넌트는 하나의 스퀘어를 만들기 위한 놈이라서
    // 여기서 보드 배열을 만들어도 전체적으로 연결되지 못한다. 

}

 

 

Square.tsx

import { useState } from "react";

interface Props_square {
    value : string | null;
    onClick : () => void;
}

export default function Square({value, onClick}:Props_square){

    return(<>
        <button onClick={()=>onClick()}>
            {value}
        </button>
    </>)
}

 

여기에 emotion을 이용해 꾸민 다음에 완성을 할 예정이다.

+ Recent posts