포스코X코딩온 웹 풀스택 과정을 수강하며 확실히 듣기 전보다 코딩능력이 증가했지만, 웹개발 공부가 거의 처음인 나에게 교육 진도가 정말 빨랐다. 그래서 지금까지 진행하며 배운 node.js와 mysql 그리고 리액트까지 살짝 다룰 수만 있을 정도이고, 어떤 작업이나 원하는 기능을 구현하고 싶을 때, 바로바로 구현하는 것에 어려움이 있다. 그래서 고민해본 결과 현재 진도를 나가고 있는 스프링부트를 배우는 것은 보류하고 node.js와 mysql 그리고 리액트를 원하는대로 능숙하게 다룰 수 있도록 강의와 개인 프로젝트를 진행하며 개인 실력을 기르기로 결심했다. 지금까지 정리하지 못한 블로그도 하루 1블로깅을 하며, 다시 정리하고자 한다. 일단 지금은 이전에 진행하던 리액트 개인 프로젝트를 완성하며, React와 Node.js를 능숙하게 다룰 수 있도록 노력할 예정이다. 블로그도 새로 팔까 고민중이다. 

 

[앞으로 하루동안 해야하는 것들]

default : 3시간 이상 집중(공부시간)

아침 운동,

1일 1블로깅

1일 1커밋

2일 1 백준/프로그래머스 문제풀기(번갈아가면서)

리액트 개인 프로젝트 (마무리 후 >> 포스코X코딩온 kdt에서 진행한 두 번째 프로젝트 완성 후 배포하여 마지막 프로젝트를 할 때 사용할 수 있도록 개발하기 >> 포트폴리오 작성)


 

[11월 6일 해야하는 것]

개인 프로젝트 로그인/회원가입 백엔드 구현

Node.js 강의 했던 부분 처음부터 실습 위주로 복습

1일 1블로깅

1일 1커밋

(코테 준비는 내일부터

 


[리액트 개인 프로젝트 로그인/회원가입 기능 구현]

로그인과 회원가입 구현은 bcrypt 암호화를 사용하여 구현했다.

 

먼저 bycrypt를 설치해준다.

npm i bcrypt

 

 컨트롤러에 적용

// /Controller/Cstory.js

const bcrypt = require("bcrypt");

 

이렇게 위에서 불러주고, 회원가입할때 암호화하여 패스워드를 보내주고 로그인할때 비교하여 비교한 값이 true라면 로그인 성공, false라면 로그인 실패를 해주면된다. 

 

먼저 회원가입 기능을 구현해보자 패스워드 암호화 함수를 따로 만들어준다.

// /Controller/Cstory.js

const bcrypt = require("bcrypt");
//나머지 부분들은 생략한다.
 
 //Auth 기능
 
 //비밀번호 암호화
const bcryptP = (pw) => {
    return bcrypt.hashSync(pw, 10);
}

pw는 암호화하고자 하는 데이터이고, 10은 salt값이다. 여기서 salt란 입력한 값에 붙이는 특정 값으로 보안을 높여준다. 여기서 주의할 점은 bcrypt는 단방향 암호화이기 때문에 복호화가 불가능하다. 복호화가 가능한 암호를 만들고 싶다면, crypto의 createCipheriv를 사용하면 된다.

 

그 다음에 회원가입을 할 때, 암호화를 하여 데이터를 넣어준다.

// /Controller/Cstory.js

const bcrypt = require("bcrypt");
//나머지 부분들은 생략한다.
 
 //Auth 기능
 
 //비밀번호 암호화
const bcryptP = (pw) => {
    return bcrypt.hashSync(pw, 10);
}

// 회원가입
const signup = async(req, res) => {

    try {
        const {email, nickname, pw} = req.body;
        const hashPw = bcryptP(pw);//위에서 만들어준 암호화 사용
        const signUp = await models.user.create({
            email: email,
            nick_name: nickname,
            pw: hashPw,
        });
        if(signUp){
            res.json({result:true});
        }    
    } catch (error) {
        console.log(error);
    }

}

 

이후에 로그인을 할때 비교해줄 함수를 만들고 이 함수를 적용하여 로그인 기능을 구현하면된다.

 

간편하게 보기위해 이전에 만들었던 회원가입 부분은 생략하도록 하겠다.

// /Controller/Cstory.js

const bcrypt = require("bcrypt");
//나머지 부분들은 생략한다.
 
 //Auth 기능
 
 //비밀번호 암호화
const bcryptP = (pw) => {
    return bcrypt.hashSync(pw, 10);
}

//비밀번호 비교
const conpareBcryptP = async (pw, dbpw) => {
    return await bcrypt.compare(pw, dbpw);
}

여기서 비교한 값을 비동기로 처리한 이유를 설명하자면, 암호를 비교한 후 일치한다면 true를 리턴해주고, 일치하지 않는다면 false를 리턴해준다. 근데 이 값을 받기 위해선 비동기로 처리해야하기 때문에 async await을 사용해주었다.

 

로그인할려고 하는 사용자가 보낸 데이터(이메일과 패스워드)를 구조분해할당으로 받고, 시퀄라이즈의 쿼리문으로 해당 이메일을 가진 튜플을 가져온다. 튜플이 존재하지 않을 경우 "해당 유저가 존재하지 않습니다"라는 msg랑 false값을 보내준다.

// /Controller/Cstory.js

const bcrypt = require("bcrypt");
//나머지 부분들은 생략한다.
 
 //Auth 기능
 
 //비밀번호 암호화
const bcryptP = (pw) => {
    return bcrypt.hashSync(pw, 10);
}

//비밀번호 비교
const conpareBcryptP = async (pw, dbpw) => {
    return await bcrypt.compare(pw, dbpw);
}


//로그인 데이터 보냄
const login = async(req, res) => {

    try {
        const {email, pw} = req.body;
        console.log("로그인 데이터", email, pw);
        const result = await models.user.findOne({
            where: {email},
        });
        if(!result){
            res.json({
                result : false,
                msg : "해당 유저는 존재하지 않습니다.",
            });
        } 
        
        if (result) {
			//튜플이 존재할 때
        }
    } catch (error) {
        console.log(error);
    }

 

이제 튜플 즉, 유저가 존재할 때 비밀번호가 일치에 따라 일치한다면 true값을 일치하지 않는다면 false값을 보내준다. 

// /Controller/Cstory.js

const bcrypt = require("bcrypt");
//나머지 부분들은 생략한다.
 
 //Auth 기능
 
 //비밀번호 암호화
const bcryptP = (pw) => {
    return bcrypt.hashSync(pw, 10);
}

//비밀번호 비교
const conpareBcryptP = async (pw, dbpw) => {
    return await bcrypt.compare(pw, dbpw);
}

//로그인 데이터 보냄
const login = async(req, res) => {

    try {
        const {email, pw} = req.body;
        console.log("로그인 데이터", email, pw);
        const result = await models.user.findOne({
            where: {email},
        });
        if(!result){
            res.json({
                result : false,//프론트에서 로그인했을때 받은 값이 false라면 msg를 p태그로 띄워주기
                msg : "해당 유저는 존재하지 않습니다.",
            });
        } 
        
        if (result) {
            const compare = await conpareBcryptP(pw, result.pw);
            console.log("비교 후 리턴 값", compare);//비교한 후의 리턴값을 받을려면 비동기로 받아야한다.
            if(compare){//compare이 true일 때,
                res.json({
                    result : true,
                    msg: "로그인 성공!"
                });
            } else{//compare이 false일 때,
                res.json({
                    result : false,
                    msg : "비밀번호가 일치하지 않습니다."
                })
            }

        }
    } catch (error) {
        console.log(error);
    }

 

이를 모두 합친 코드는 다음과 같다.

// /Controller/Cstory.js

const bcrypt = require("bcrypt");
//나머지 부분들은 생략한다.
 
 //Auth 기능
 
 //비밀번호 암호화
const bcryptP = (pw) => {
    return bcrypt.hashSync(pw, 10);
}

//비밀번호 비교
const conpareBcryptP = async (pw, dbpw) => {
    return await bcrypt.compare(pw, dbpw);
}

// 회원가입
const signup = async(req, res) => {

    try {
        const {email, nickname, pw} = req.body;
        const hashPw = bcryptP(pw);//위에서 만들어준 암호화 사용
        const signUp = await models.user.create({
            email: email,
            nick_name: nickname,
            pw: hashPw,
        });
        if(signUp){
            res.json({result:true});
        }    
    } catch (error) {
        console.log(error);
    }

}

//로그인 데이터 보냄
const login = async(req, res) => {

    try {
        const {email, pw} = req.body;
        console.log("로그인 데이터", email, pw);
        const result = await models.user.findOne({
            where: {email},
        });
        if(!result){
            res.json({
                result : false,
                msg : "해당 유저는 존재하지 않습니다.",
            });
        } 
        
        if (result) {
            const compare = await conpareBcryptP(pw, result.pw);
            console.log("비교 후 리턴 값", compare);//비교한 후의 리턴값을 받을려면 비동기로 받아야한다.
            if(compare){
                res.json({
                    result : true,
                    msg: "로그인 성공!"
                });
            } else{
                res.json({
                    result : false,
                    msg : "비밀번호가 일치하지 않습니다."
                })
            }

        }
    } catch (error) {
        console.log(error);
    }

 


구현을 하면서 겪었던 에러와 알게된 점

 

- 에러 : data must be a string and salt must either be a salt string or a number of rounds

bcrypt가 받는 데이터는 반드시 string  값이거나 salt값으로 받아야 한다.

근데 나는 포스트맨으로 비밀번호를 int값으로 보냈기 때문에 에러가 발생했었다.

 

- try catch문을 사용하는 이유 :

try catch문을 사용하지 않은 상태에서 에러가 발생하면 서버가 꺼진다. 이를 방지하기 위해 try catch문으로 에러를 받는 것이다. 또한 에러가 발생했을때 어떤 작업을 할지도 정할 수 있기 때문에 매우 유용하다.


앞으로 구현해야하는 작업들

[백엔드 : Node.js]

[구현해야하는 기능]

  • 메인페이지 정보 보내기 11/6 (완)
  • 회원가입 : 데이터 베이스로 정보 보내기 11/6  (완)  
  • 로그인 : 비밀번호 암호화 하기 JWT로 구현 11/6 (완)
  • 이야기 상세 페이지에서 수정과 삭제가 가능
  • 마이페이지 들어가 내가 적은 이야기 확인가능 
  • 이야기 업로드 가능

 

  • 이야기 업로드 기능 11/7 - 8
  • 사진 업로드 서버로 올렸다가 다시 받으면서 주소 간소화 성공하기(코프런 참고) 
  • 에디터 가져와서 벨로그처럼 기능할 수 있도록 하기(코프런 참고)
  • 챗지피티 api 가져와 맞춤법 검사 해주기
  • 마이페이지 : 사용자 아이디가 있는 이야기와 세계 가지고 오기

[프론트엔드 : 리액트] : 백엔드 마무리 후 작업

  • 메인페이지
  • 회원가입 페이지
  • 로그인 모달
  • 사용자의 작업이 가능하도록 토큰 부여(로그인 했을때 백엔드에서 true값과 로그인 성공 메세지를 전달 받으면 토큰 받기 토큰이 있을경우 이용할 수 있는 기능 보여주기)
  •  
  • 마이페이지
  • 스토리 상세페이지
  • 스토리 업로드 페이지(챗지피티 적용(백엔드에서))

2차 프로젝트를 시작하면서(물론 그전부터) 블로깅을 거의 안하게된거 같다ㅠㅠ 이제 포트폴리오도 준비하고 기술면접, 코딩테스트, 자소서 등 준비할게 많기 때문에 차근차근 최근것부터 예전 것을 정리해보며, 준비하고자 한다. 프론트엔드 개발자로써 제일 중요하다고 할 수 있는 리액트를 하나하나 파헤쳐보자

 


SPA

리액트 라우터를 먼저 알기 전에 리액트에서 라우터를 이용함으로써 개발하려고 하는 SPA에 대해 알고가야한다.

SPA란 Single Page Application의 약어로 단일 웹페이지로 돌아하는 어플리케이션을 말한다. 브라우저에서 자바스크립트를 이용해 단일 췝페이지 상의 HTML 요소를 동적으로 생성 및 조작할 수 있다.

다만 검색 엔진 최적화에는 적합하지 않은 방법이다. 이는 React, Svelte, Vue.js와 같은 라이브러리로 개발할 수 있다.

 

라우팅

그럼 라우팅이란 무엇일까? 라우팅이란 간단하 말하자면 경로를 지정하는 행위를 말한다. 사용자가 요청한 url에 맞는 페이지를 보여주는 것이고, 리액트에서 제일 많이 사용되는 것이 React Router이다.

 

React Router

결론적으로 리액트 라우터란 개발자가 주소별로 다른 컴포넌트를 보여주기 위해 사용하는 라이브러리라고 할 수 있다.  설치 명령어는 다음과 같다

npm i react-router-dom@6 #버전 6 지정 설치

주요 컴포넌트는  

- BrouserRouter

- Routes

- Route

- Link

- createBrowserRouter

위와 같이 있다. 하나씩 알아보자

 

BrouserRouter

▷ HTML5를 지원하는 브라우저의 주소를 감지한다.

▷  Router의 역할

▷ 새로고침 없이 새로운 컴포넌트를 랜더링 해준다.

▷ url마다 컴포넌트가 바뀔 떄, 바뀌는 부분의 최상단에 위치해야한다.

import {BrowserRouter} from 'react-router-dom';

설치한 리액트 라우터 돔에서 가져와서 사용하면 된다.

 

- Routes, Route

▷ 경로가 일치하는 컴포넌트를 렌더링해주도록 경로를 매칭하는 역할을 해준다.

아래와 같이 경로를 매칭한다.

<Routes>
	<Route path="/" element={<RouteMain />}><Route>
    <Route path="/product/:id" element={<RouteProduct />}><Route>
    <Route path="*" element={<RouteNotFound />}><Route>
</Routes>

path는 경로이고 element는 연결할 컴포넌트이다.

하지만 위처럼 사용하는 방식은 옛날에 사용한 방식이며, 최근에는 createBrowserRouter를 사용한다.

 

- createBrowserRouter

import {createBrowserRouter} from 'react-router-dom';

를 불러와 배열 안의 객체를 중첩으로 넣어서 사용한다.

- Link

경로를 변경한다. 기존에 사용하던 a태그는 새로고침으로 렌더링을 수행했지만 <Link> 컴포넌트는 페이지 전환을 방지한다.

 

- Outlet

중첩 라우트 렌더링: Outlet은 중첩 라우트의 부모 라우트 컴포넌트 내에 서 사용되어, 해당 부모 라우트의 URL과 일치하는 자식 라우트 컴포넌트 를 렌더링해준다

context를 이용하여 자식 라우트에 데이터 전송이 가능하며 자식 라우트에서는 useOutletContext()를 사용한다.


URL 파라미터

- useParams()

경로에 : 를 사용해 설정되는 것을 가져올 수 있다.

URL 쿼리스트링

- useSearchParams()

원하는 쿼리스트링을 보낼 수 있다.

  - useNavigate()

 

 

 

리액트 시작

npx create-react-app 폴더이름

타입 스크립트 시작

npx create-react-app 폴더이름 --template typescript

JSX & 문법

JSX [형태]

1. 최상위 element는 형제 요소가 없는 고유한 요소여야야한다

ex) 가능한 형태

function App() {
  return (
    <div>
      <h1>가능</h1>
    </div>
  );
}

export default App;

불가능한 형태

function App() {
  return (
    <div>
     
    </div>
    <h1>불가능</h1>
  );
}

export default App;

최상위에 아무것도 넣고 싶지 않다면 빈태그로 넣을 수 있다.

 

2. 종료 태그는 무조건 존재해야한다. ex) < h1></h1>이거나  <input />

 

 

JSX [변수 삽입]

위에서 선언한 변수를 html에 {}로 넣을 수 있다.

 

JSX [아이디와 클래스]

id 적용이 가능하며, class는 className으로 입력해야한다.

 

JSX [스타일 적용]

style 적용은 중괄호 안의 중괄호, 즉 객체 형태로 작성하여 스타일을 넣어준다.

ex) { {color : 'red', fontSize : '40px'} } // 카멜케이스로 넣어야한다.

 

문법 [map()과 filter()]

JSX에서 변수 삽입할 때 사용하는 중괄호 안에는 if문이나 for문은 들어가지 못한다. 그래서 배열을 편하게 html에 보여주고 싶을때, map 함수와 filter 함수를 이용해 편하게 보여줄 수 있다.

 

- map() 함수

예를 들어서 나열을 하고 싶은 arr이라는 배열이 있을때, for문을 사용하지 않고 map을 이용한다면 편하게 정렬할 수 있다.

let arr = [1, 2, 3, a, b, c];

arr.map((val, index, arr) => {

        retrun 보여주고 싶은 값

});

여기서 각 콜백 함수에 대해 설명하자면

val : 현재 돌고 있는 값

index : 돌고 있는 값의 인덱스값

arr : 현재 돌고 있는 배열 전체

 

그리고 보통 리액트에서 사용할때는 객체가 배열에 들어가 있는 경우가 많기 때문에 단일로 써서 각 객체에서 원하는 값을 불러낸다. 여기서 중요한 점은 key값을 설정해줘야 오류가 나지 않는다. 아래와 같은 객체를 가진 배열이 있다고 가정하자. 그럼 map은 다음과 같이 사용할 수 있다.

//리액트의 한 컴포넌트라고 가정하자

    export default function 컴포넌트이름 () {
        //여기에 옆에 있는 객체를 가진 배열도 존재한다고 가정한다.

        return (<>
            <ul>
                {list.map((listData) => {
                    return <li key={listData.id}> {listData.alpha} </li>
                })}
            </ul>
        </>)
    }

따로 아이디가 존재하지 않다면 인덱스를 가지고 오는 콜백함수를 키값으로 넣을 수 있다. 

 

- filter() 함수

 보통 map 함수를 이용해 배열을 나열하기 전에 어떠한 조건을 기준으로 배열을 가공할 때 사용한다. 즉, filter 함수로 리턴된 배열을 map함수로 나열한다고 생각하면 편하다. 왜냐하면 filter 함수는 map 함수처럼 html 자체를 반환해주지는 않고, 그냥 조건에 맞는 배열 자체를 보내주기 때문이다. 사용법은 다음과 같다.

//리턴한 배열을 받기 위해서 변수를 선언하어 사용하는 경우가 많다.
arr.filter((val, index, arr)=> {
	return 조건
})

 

 

문법 [sort함수]

sort 함수를 통해 문자나 숫자를 정렬할 수 있다.

 

문법 [구조분해할당] 

지금까지는 변수를 선언할때 보통

let num = [1, 2, 3, 4];

이런 식으로 했었는데, 선언을 할때부터 각 값을 지정할 수 있다.

let [a, b, c, d] = [1, 2, 3, 4];

이를 활용하여서 어떠한 객체 파일을 보냈을때 받은 데이터에서 원하는 값만 사용하고 싶은 경우 구조분해 할당을 통해 원하는 값만 뽑아서 사용할 수 있다.

ex)

보낸 객체

const data = {

    name: result.name,

    pw: result.pw,

}

 

받은 곳에서 이름만 사용하고 싶음

const data = req.body.data //(경우에 따라 params가 올 수도 있다.)

let { name } = data;


Component

MVC 모델의 view를 독립적으로 구성해 재사용할 수 있고, 새로운 컴포넌트도 만들 수 있다. 간단히 말하자면 하나로 이어진 html 파일은 구분하기와 읽기가 어렵기 때문에 개별적으로 컴포넌트를 만들어준 후에 App.js에서 import해서 가져올 수 있다. 이 과정에서 props를 넘겨주면 컴포넌트끼리 데이터를 간편하게 주고 받을 수 있다.

 

컴포넌트는 원래 함수형 컴포넌트롸 클래스형 컴포넌트가 있다. 예전에는 state와 라이프 사이클을 쓸 수 있는 클래스형

컴포넌트를 많이 사용했지만 현재는 Hook으로 state와 라이프사이클 등 다양한 부분들을 함수형 컴포넌트에서도 사용할 수 있게 되면서 함수형 컴포넌트를 많이 사용하게 됐다.  각각의 컴포넌트 구조를 알아보자

 

- 클래스형 컴포넌트

클래스형 컴포넌트는 리액트에서 컴포넌트를 불러오고 상속을 받아서 사용을 한다. 그래서 랜더 안에 리턴을 넣어서 사용한다.

import {Component} from "react";

class 컴포넌트이름 extends Component {
	render() {
    	//삼항연산자 한 번 써보자
        const classes = 'dd';
        
        return (
        	<>
            	<div>{classes === 'dd' ? "dd씨 반가워요" : "넌 뭐야"}
                {/* 참이라면 앞의 것을 출력해주고 거짓이라면 뒤의 것을 출력해준다. */}
                <div> 반가워!! </div>
            </>
        )
    }
    
    export default 컴포넌트이름;

}

- 함수형 컴포넌트

함수형 컴포넌트는 위에서 따로 불러올 필요 없이 바로 함수로 만들어서 사용할 수 있다. 그리고 함수 특성을 이용해 작성 동시에 앞에 export default를 적어서 바로 export 해 줄 수 있다.

export default function 컴포넌트이름() {
	
    return (<>
    	<div>함수형 컴포넌트</div>
    </>)
}

 클래스형 컴포넌트와 비교하면 정말 간단한 것을 알 수 있다. 그리고 export할때 디폴트를 쓰는 것과 았르 때 import 하는 방법이 다른데 다음과 같다.

이것도 기억하면 좋을 것 같아서 가져왔다.


Props (+ JS와 Ts 사용법 차이)

부모 컴포넌트에서 자식 컴포넌트로 정보를 보낼 때 사용한다. 보통 상위 컴포넌트에서 다른 컴포넌트를 불러올 때 아래와 같이 동시에 쓴다.

- JS 사용법

import Child from './Child';
import {useState} from 'react';


export default Parents() {
	const[name, setName] = useState('내이름');
    
    return (<>
    	<Child 보내는이름=문자열 혹은 {위에서 정의한 변수나 state}></Child>
    </>)
}

위와같이 작성하면 자식 컴포넌트에서 props로 받아서 쓸 수 있다.

 
 export default Child(props) {
 	
    return (<>
    	<div>{props}</div>//배열일 경우 {props[원하는인덱스값]}
        <div>{props.children}</div>// 위에서 보낸 값 말고 <>이사이의 값</>을 가져온다.
    </>)
 }
 
 //구조분해할당 방식
 
  export default Child({name, children}) {
 	
    return (<>
    	<div>{name}</div>
        <div>{children}</div>
    </>)
 }

- Ts 사용법

근데 이를 TypeScript에서 쓰려면 부모 컴포넌트와 자식 컴포넌트에서 타입을 모두 지정해줘야 오류가 나지 않고 잘 전달된다.

import Child from './Child';
import {useState} from 'react';


export default Parents() {
	const[name, setName] = useState<string>('내이름');//타입 지정해줘야함
    conconst[nameArr, setArr] = useState<string[]>(['내이름', '너이름', '우리이름']);
    //배열일 경우 타입지정
    
    return (<>
    	<Child name={name} setName={setName} name={nameArr} setName={setArr} >넌 할 수 있어</Child>
        //자식 컴포넌트에서 어떤 타입으로 받아야할지 모르겠다면, 
        //여기서 해당 변수위에 마우스를 올리면 어떠 타입인지 알 수 있다. 
    </>)
}

위와 같이 보내주었다면 개별로 타입을 지정해주는 것보다 interface로 만들어 지정해주는 것이 편하다.

import { Dispatch, SetStateAction } from 'react'

interface PROPS {
	name : string,
    nameArr : string[],
    setName : Dispatch<SetStateAction<string>>,  
    setArr : Dispatch<SetStateAction<string[]>>,
    //원래 ()=>{} 이 방식으로 보내도 됐었는데 계속 오류가 나서 찾아보니까 저렇게 하는거였다.
    children : React.ReactNode,
}

export default Child({name, nameArr, setName, setArr, chidren}: PROPS){
	
    const change = () => {
    	setName('너이름');
    }
    
	return (<>
    	<p>{name}</p> //이름
        <p>{nameArr.map((val. valindex) => {
        	return <p key={valindex}> {val} </p>
        })}</p> // 배열 출력
        <p> {children} </p>
        <button onClick={change}>이름변경</button>
    </>)
}

이렇게 사용할 수 있다.


 

 

 

 


props [객체를 가진 배열에 id를 추가한 후 id에 맞는 값을 가져와 모달창에 출력하는 방법](+Ts)

처음에는 객체를 가지지 않고 그냥 문자열을 가진 배열에서 인덱스를 뽑아서 보낼려고 했는데, 이유는 모르겠지만 계속 되지 않아서 객체를 가진 배열로 만들어 id와 내용을 넣어서 제목에 onClick 이벤트를 넣고 클릭을 했을때, 해당 id를 받아와서 자식 컴포넌트로 보내주는 방식을 사용했다. 이 과정에서 확실히 알게 된것은 타입 지정을 미리 해줘야한다는 것이다. 

 

배열 안에 있는 객체임으로 객체에 들어가는 변수들에 대한 타입 지정을 interface로 해준 뒤 그 인터페이스이름[] 이렇게 타입을 넣으면 된다. 실제 코드를 쳐보도록 하겠다.

 

App.js

//실제 내가 실습 중인 코드는 다른 내용들이 다 포함되어있어서
//위에서 말한 기능만 보여주는 코드를 짜보도록 하겠다.

import { useState } from 'react';
import Modal from './Modal';

//2. 만든 객체를 보고 interface를 만들어주자
interface Title {
	id: number;
    title: string;
    text: string;
}

export default Parents() {
	//1. 객체를 먼저 만들어주고 그에 맞는 interface를 만들어주자
    //3. 만든 interface를 useState에 적용해준다. Title[]
    const [title, setTitle] = useState<Title[]>([
        {
            id: 1,
            title: '제목1'
            text: '내용1'
        },
        {
            id: 2,
            title: '제목2'
            text: '내용2'
        },
        {
            id: 3,
            title: '제목3'
            text: '내용3'
        },
    ]);
    
    //4. 모달 창은 삼항연산자나 단축평가를 이용해 앞의 값이 true라면 창이 열리도록 할것이다.
    // 그리고 버튼을 눌렀을때 id 값도 같이 들어와야한다.
    // 모달을 열고 닫을 boolean을 담은 useState와 클릭이벤트가 일어났을때 값을 변경할 함수를 만들자
    // 또한 id의 값을 가져갈 useState도 만들어주자
    
    const[modal, setModal] = useState<boolean>(false);//처음엔 닫혀있음으로 false값이다.
    const[id, setId] = useState<number>(0);//값을 안써주면 오류가 난다. 0을 기본값으로 쓰자
    
    //5. 버튼을 눌렀을때 id값을 가져오는 부분을 만들고 id를 파라미터로 받아서 사용해주자
    const modalBtn = (id) => {
    	if(modal === false){
        	setModal(true);
            setId(id);
        } else {
        	setModal(false);
        }
    };
    
    return (<>
    	{/* 5. 화살표함수 방식으로 바꿔서 val.id를 하면 id를 받을 수 있다.*/}
        {title.map((val)=>{
        	return <p key={val.id} onClick={() => modalBtn(val.id)}> {val} </p>
        })}
        
        {/* 6. 가져온 모달창에 props를 보내주는 것과 
        modal state에 맞춰 열고 닫히는 코드를 짜보자 */}
    	
        //삼항연산자
        {modal ? <Modal titleObj={title} id={id}></Modal> : null}
        //단축평가
        {modal && <Modal titleObj={title} id={id}></Modal>}
        
    </>)	
    
}

위에서 업급했듯 저렇게 props를 받기 위해선 자식 컴포넌트에서 type을 다 설정해줘야한다.

 

Modal.js

//1. 부모에서 사용한 interface와 props에 적용할 interface를 만들어주자
interface Title {
	id: number;
    title: string;
    text: string;
}

interface PROPS {
	title: Title[];
    id: number; 
}

export default Modal({title, id}: PROPS){
	
    //2. 값을 받아와서 타입 설정을 해준 뒤에 인덱스 값이므로 id-1로 불러와서 넣어준다. 
    retrun (<>
    	<h4>제목 : {title[id-1].title}</h4>
        <p>날짜 : 10월 16일 발행(까먹고 추가 못함)</p>
        <p>내용 : {title[id-1].text} </p>
    </>)
}

이렇게 만들어주면 각 제목을 누를때만다 제목에 대한 내용이 나오는 모달창을 구현할 수 있다.


 

 

 


Hook

훅은 클래스형 컴포넌트에서만 가능했던 상태관리(state)와 생애주기(라이프 사이클)을 함수형 컴포넌트에서도 할 수 있도록 하는 기능이다.

훅의 중요한점은 컴포넌트 안에서만 호출이 가능하다는 것이다. 대표적인 것들을 설명하자면

- useState() : 상태관리를 해주는 가장 기본적인 훅이다.

- useEffect() : 클래스형에서 사용하는 라이프 사이클을 사용할 수 있도록 해준다.

- useContext() : 리액트에서 사용하고자 하는 데이터를 전역적으로 처리할 수 있다. 컴포넌트가 많은 큰 프로젝트에서 props를 사용하면 매우 번거로운데, 이에 대한 해결책 중 하나이다. 하지만 useContext보다는 리덕스를 더 많이 사용하는 추세이다.

- useReducer() : 복잡한 컴포넌트의 상태로직을 리듀서 함수로 관리해준다.

- useMemo() : 메모이제이션을 통해 함수의 리턴 값을 재사용할 수 있게 해주는 훅

- useCallback() : 함수를 메모이제이션하여 불필요한 렌더링을 줄이게 해주는 훅

- useRef() : 참조(reference)를 생성하고 관리할 수 있는 훅 (DOM 접근, 변수 보존 등)

 

 

Hook [useState]

- 기본

위에서 import로 선언을 해주고,

let/const [실제 값, 값변경함수] = useState(실제값 초기화);

위와 같은 방식으로 사용한다. 그리고 타입 스크립트로 지정해줄려면 

let/const [실제 값, 값변경함수] = useState<string, number 등등>(실제값 초기화);

let/const [실제 값, 값변경함수] = useState<string, number[]/ string, number{}>(실제값 초기화 >> 배열이나 객체);</string,>

 

 

이 state를 사용할 경우 state가 갱신되거나 변경될때마다 다시 랜더링 해주기 때문에 유용하게 사용할 수 있다.

즉 계속해서 변화하거나 추가되는 것에 state를 사용하면 된다.

원래는 클래스형 컴포넌트에서만 사용할 수 있었는데 useState를 통해 함수형 컴포넌트에서도 사용할 수 있게 됐다.

 

주의할 점은 무조건 값을 변경할 때는 실제 값인 state를 건들면 안되며, setState를 통해 변경해야한다.

 

- 배열을 변경할 때,

배열을 변경할 때는 직접 배열이 있는 state 가져올 수 없기 때문에. spread 연산자(...)를 사용한다.

만약에 state가 배열로 설정되어 있고, 해당 state에 새로운 값을 추가하고 싶다면 다음과 같이 작업하면 된다.

 

ex) 

- 배열 추가

const [arr, setArr] = useState<string[]>(['바보', '강아지', '야옹']);

추가

setArr([...arr], '추가할 값'); 

 

- 배열 삭제

setArr(arr.filter(e => e.id !== 삭제하고자 선택한 값의 아이디);

배열의 객체 속에 아이디가 있다고 가정한 후 위와 같이 코드를 적으면 선택된 아이디와 다른 아이디만 반환하기 때문에 

선택된 값이 삭제된 배열을 얻을 수 있다. 

 

- 배열 교체

Array.prototype.push.apply(기존 배열, 새로운 배열);

 

 

Hook [useEffect]

라이프 사이클을 리액트에서 사용할수 있도록 해주는 훅이다. 라이프 사이클은 총 3가지로 나뉘는데 

1. Mount : 처음 생성될때 === 컴포넌트를 처음 불러올때 

2. Update : state나 props가 변경되어 재랜더링 될때

3. Unmount : 컴포넌트가 사라질때

 

이렇게 3가지의 경우를 useEffect를 사용해 각 경우에 어떤 작업을 실행할 지 정해줄 수 있다. 

클래스형에서는 •  componentDidMount • componentDidUpdate • componentWillUnmount

위의 세 가지 메서드를 자주 사용하고 공식 문서에도 권장하고 있다.

 

클래스형에서 사용하는 예시를 간단히 보여주고 useEffect의 사용법을 배워보자

state에 있는 숫자를 더해주고 빼주는 기능을 하는 코드이다. 클래스형에서 state와 함수를 사용하기 위해선 this를 써야하고, props와 state는 생성자에 넣어줘야한다. 그리고 위에서 사용하는 메소드들은 그냥 적어서 바로 사용할 수 있다.

 

Hook [useEffect()]

- useEffect( () => {} ); //컴포넌트가 랜더링 될때마다 실행된다.

- useEffect( () => {}, [] ); // mount가 될때만 즉 최초로 컴포넌트를 불러올때만 실행한다.  

- useEffect( () => {}, [특정값] ); // 특정값이 변경될때만 실행한다.

 

만약에 컴포넌트가 사라지거나 특정값이 사라질때 어떤 작업을 수행하고 싶다면, 중괄호 안에 return () => {}; 를 이용해 해당 함수 안에 넣으면 수행하게 할 수 있다.

 

간단히 요약하자면 useEffect(랜더링 이후 수행할 함수, 어떤 상황에 할 것인지 나타냄(  or [] or [특정값]) )

 

 

Hook [useRef()]

훅 useRef는 html의 요소를 가지고 오기 위해 사용한다. 근데 이를 ts에서 사용하기가 진짜 빡셌다. 특정 요소에서 원하는 값을 가지고 올려고 할때 형식이 맞지 않아서 가지고 올 수 없기 때문에 요소에 적는 ref에도 형식 지정을 해줘야한다..ㅠㅠ

 

기본적인 사용법은 useRef로 myref하나 만들어주고 이를 원하는 요소에 ref={}로 넣어준뒤 myref.current로 가져올 수 있는 다. ts에서도 똑같이 해주면 되는데 형식 지정을 해줘야한다. 

 

프로젝트를 하면서 겪은 것을 올리도록 하겠다.

에러 

'MutableRefObject<HTMLTextAreaElement | undefined>' 형식은 'LegacyRef<HTMLTextAreaElement> | undefined' 형식에 할당할 수 없습니다. 'MutableRefObject<HTMLTextAreaElement | undefined>' 형식은 'RefObject<HTMLTextAreaElement>' 형식에 할당할 수 없습니다. 'current' 속성의 형식이 호환되지 않습니다. 'HTMLTextAreaElement | undefined' 형식은 'HTMLTextAreaElement | null' 형식에 할당할 수 없습니다. 'undefined' 형식은 'HTMLTextAreaElement | null' 형식에 할당할 수 없습니다.ts(2322) index.d.ts(159, 9): 필요한 형식은 여기에서 'DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>' 형식에 선언된 'ref' 속성에서 가져옵니다.

 

해결법

이 에러는 TypeScript에서 useRef의 반환 값과 textarea 요소의 ref 속성의 타입이 일치하지 않아 발생합니다. TypeScript에서는 기본적으로 useRef를 사용하면 current 속성이 HTMLTextAreaElement | null 형식을 가집니다. 반면 textarea 요소의 ref 속성은 HTMLTextAreaElement | undefined일 수 있습니다.

이 문제를 해결하려면 useRef와 textarea 요소의 타입이 일치하도록 타입을 지정해야 합니다. HTMLTextAreaElement | null 형식을 HTMLTextAreaElement | undefined 형식으로 변경하거나 그 반대로 변경할 수 있습니다. 아래는 두 가지 방법 중 하나를 선택하여 문제를 해결하는 방법입니다:

1. useRef를 HTMLTextAreaElement | undefined 형식으로 변경:

const storyTextAreaRef = useRef<HTMLTextAreaElement | undefined>(undefined);

2. textarea 요소의 ref를 HTMLTextAreaElement | null로 변경:

<textarea ref={storyTextAreaRef as React.LegacyRef<HTMLTextAreaElement> | undefined} id='story'>
  {title[id-1].story}
</textarea>

 

이렇게 하면 textarea 요소의 ref 속성과 useRef의 반환 값의 타입이 일치하게 됩니다.

어느 방법을 선택하느냐는 개발 환경 및 팀의 코딩 스타일에 따라 다를 수 있습니다. 각 방법은 문제를 해결하는 데 사용될 수 있습니다.

 

위와같이 하면 가져올 수 있다. 근데 정말 저렇게 하는 것이 맍는지 의문이들긴한다.

 


 

 

 

 

 


리액트 라우터 (React Router)

사용자가 요청한 주소에 따라 원하는 페이지를 보여주기 위해 사용한다.

npm i react-router-dom@6

버전 6으로 지정해서 설치해준다. 

 

 BrowserRouter와 createBrowserRouter를 import 해서 사용할 수 있고, 최근에 사용하는 것은 유지보수하기 수월한  createBrowserRouter라고 한다. 

 

그러므로 BrowserRouter는 간단히 사용법만 알아보고 현재 진행중인 프로젝트는 createBrowserRouter를 이용할 것이다.

 

두 작업 모두 먼저 해야하는 것은 Router파일을 만들어줘야한다.

 

- BrowserRouter

먼저 위에서 import를 해준 뒤에 라우터 컴포넌트를 만든 후 그 안에 < BrowserRouter > </ BrowserRouter >를 넣어주고, Routes와 Route를 이용해 각 컴포넌트의 경로를 지정해주면된다.

import { BrowserRouter, Routes, Route } from 'react-router-dom';

import Header from './12Router/Header';
import Home from './12Router/Home';
import About from './12Router/About';
import NotFound from './12Router/404';
import User from './12Router/User';
import Redirect from './12Router/Redirect';
import UserDetail from './12Router/UserDetail';
import Error from './12Router/Error';
import Comment from './12Router/Comment';

export default function Router() {
    return (
        <>
            <BrowserRouter>
                <Header />
                <Routes>
                    <Route path="/" element={<Home />} />
                    <Route path="/about" element={<About />} />
                    <Route path="/user" element={<User />} />
                    <Route path="/user/:id" element={<UserDetail />} />
                    <Route path="/redirect" element={<Redirect />} />
                    <Route path="*" element={<NotFound />} />
                </Routes>
            </BrowserRouter>
        </>
    );
}

 path는 지정할 경로이며 elment는 보여줄 컴포넌트를 지정하는 것이다.

 

- createBrowserRouter

 크리에이트브라우저라우터는 이거 자체만 import해서 가져오면 사용가능하다.

import { createBrowserRouter } from 'react-router-dom';

import App from './App';
import NotFound from './12Router/404';
import Header from './12Router/Header';
import Student from './12Router/Student';
import Codingon from './12Router/Condingon';
import Search from './12Router/Sech';

const Router = createBrowserRouter([
	{
    	path: '/',
        element: <Header />,
        // 여기서 children : [{},{}...]를 이용해서 api/children에 지정된 경로
        // 이렇게 사용할 수 있다.
    },
    {
    	path: '/student',
        element: <App />,
        children: [
        	{
                path: 'kdt',
                element: <Student />,
            },
            {
                path: 'codingon',
                element: < Codingon/>,
            },
            {
                path: 'new',
                element: <Search />,
            },
        ],
    },
]);

export default Router

이렇게 지정한 뒤에 api에 따라 자식 컴포넌트로 원하는 컴포넌트를 불러올 수 있는 Outlet을 App.js에 적용하고, 인덱스에는 다음과 같이 적용하면된다.

App.js

import { Outlet } from 'react-router-dom';
import Header from './12Router/Header';
import Router from './Router';

function App() {
    return (
        <>
            {/* 최근 버전 */}
            <Header />{/* 계속 페이지에 고정하고 싶은 컴포넌트가 있다면 헤더처럼 여기서 적어주면된다. */}
            <Outlet />
            
            {/* 옛 버전은 라우터를 불러왔다 */}
            {/* <Router /> */}
            
        </>
    );
}

export default App;

 

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import Router from './Router';

import App from './App';
import { RouterProvider } from 'react-router-dom';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    {/* 옛 버전 */}
    {/* <App />  */}
    
    {/* 최근버전 */}
    <RouterProvider router={Router} />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals

위와 같이 라우터프로바이더를 불러오고 거기에서 라우터를 props로 보내주면된다.

 

Ts에서도 똑같이 해주면된다. 그리고 리액트 라우터를 하면서 유용하게 사용할 수 있는 것들을 설명하며 라우터 부분을 마치도록 하겠다.

이는 주소로 아이디를 보내서 아이디에 맞는 페이지를 보여주고 싶을 때 사용할 수 있다. api/:id로 해서 그 id값을 가지고 올때 useParams로 받아서 가져올 수 있다.

 

주소에 있는 ? 뒤의 값을 바꿀 수 있다.

 

 

import { useNavigate } from 'react-router-dom';

import 로 가져와서 사용할 수 있다. 이후에 위처럼 navigate('이동할 주소'); 이 방식으로 원하는 페이지로 이동할 수 있다.

 

useNavigate()와 useLocation()

네비게이트로 파일을 보내주면 uselocation으로 받아서 쓸 수 있다.

먼저 useNavigate로 원하는 데이터를 보내준다.

const navigate = useNavigate();

const gotoMain = () => {
  navigate("/main", {
    state: {
      userId: user.uid
    }
  });
};
// 두 번째 인자에 데이터를 써서 보내준다

 

그 다음 useLocation을 import 해준다.

import { useLocation } from 'react-router-dom';

그다음에 uselocation을 불러오면서 navigate로 넘어온 데이터를 받을 수 있다.

const location = useLocation();


const [userId, setUserId] = useState(
  location.state?.userId
);

위와 같이 location에 state의 정보를 담을 수 있다.


 

 

 

 

 


이벤트 리스너

 

onClick = {} : 중괄호 안에 함수를 적으면 클릭할 때, 실행된다.

원래는 함수를 적고 바인드를 해줘야하지만 화살표 함수를 쓰면 바로 사용할 수 있다 그래서 중괄호 안에 들어가는 함수는 아래와 같이 작성해주면 된다.

const 함수이름 = () => {
	원하는 기능
}

 

onChange = {}: onChange가 있는 요소의 값이 변할때 작동한다. 보통 값이 변할 때마다 그 값을 보내주는 방식으로 사용을 많이하는데 다음과 같이 사용하면된다.

onChange = {(e) => { e.target.value }
//변한 값을 받을 수 있다.

이를 함수에서 받고 싶다면 다음과 같이 사용하면 된다.

export default function Component () {
	
    const 함수이름 = (e) => {
    	consle.log('변한 값', e.target.value)
    }
    
    return (<>
    	<input onChange={함수이름}> </input>
    </>)
}

이렇게 사용하면 변한 값을 함수에서 받아서 사용할 수 있다.


 

 

 

 


부가기능

에디터를 만들어주는 Quill 사용해보기

먼저 npm으로 깔아준다. 

npm i react-quill
 

React-Quill 사용하기(1)

ReactQuill 사용하기

velog.io

 

생각해보니까 벨로그처럼 만들어야해서 퀼말고 토스트 ui를 가져다 쓰기로 했다.

 

[React] Toast UI Editor 적용기

Next.js 프로젝트에 Toast UI Editor를 적용해보자

velog.io

잠들어 버려서 프로젝트를 완성할 시간이 부족했다..ㅠ 빠르게 할려고 참고한 블로그를 올린다.

 

근데 치명적인 단점이 토스트 ui가 리액트 18버전에서는 적용이 안된다는 것이었다. 그래서 마크다운은 포기하고 Quill을 다시 이용하기로 했다.

 

지금  Ts를 같이 사용중이기 때문에 npm으로 설치할때 아래와 같이 설치해줘야한다

npm install @types/react-quill

 

 


에러관리

리액트 정리를 하면서 블로그 프로젝트를 만들고 있다. 이 과정에서 생기는 에러들을 정리하는 곳이다.

 

 

에러 1.

'App'은(는) 값을 참조하지만, 여기서는 형식으로 사용되고 있습니다. 'typeof App'을(를) 사용하시겠습니까?

이는 ts 오류인데 코드를 적고 있는 파일의 형식을 tsx로 바꾸어주면 해결된다.

 

에러 2.

Error: input is a void element tag and must neither have `children` nor use `dangerouslySetInnerHTML`.

input을 p태그 안에 넣어서 생긴오류

 

프론트엔드를 만들다가 하나 간과한 것이 데이터베이스에서 데이터를 가져오려면 연결을 먼저 성공시켜야 한다는 것이었다. AXIOS를 통해 데이터를 불러와도 연결이 되어 있지 않아서 불러올 수가 없으니 내가 잘 만들고 있는 것인지 화면은 잘 구성되어 있는 것인지 알 수 있는 방법이 없었다. 그래서 프론트엔드를 완성하기 전에 백엔드와 프론트엔드를 연결하고, 다시 프론트엔드를 완성하고자 한다.

 

미리 만들어진거에 적용하는게 생각보다 어려워서 일단 리액트와 node.js를 새로운 폴더를 만들어서 거기서 연결한 후에 지금까지 적은 코드들을 적용할 예정이다.

 

같이 교육을 듣는 친구가 프록시 없이 리덕스 자체로 연결을 했다고 해서 그 방법을 찾을려고 했는데 안찾아지고 잘 모르겠어서 프록시를 이용해서 서버를 통합해보고자한다.

 

https://whiteknight3672.tistory.com/264

 

2. (홈IoT DIY) Express + React 연동하기

이전 포스팅에서 Express와 React를 설치하고 실행까지 해봤습니다. 저번 포스팅에서 해봤듯, React 앱은 localhost:3000에서, Express 서버는 localhost:5000에서 접근할 수 있습니다. 그런데 어떻게 해야 Express

whiteknight3672.tistory.com

이 블로그를 참고하며 연결했다.

 

그리고 이전에 했던 방식을 살펴보면 서버의 index.js(app.js의 역할)에서 리액트에서 빌드한 html 파일을 연결한 것이었는데, 사실 값을 받아오지 못하면 뜨는 로딩중...이 잘뜨고 있다. 그런데 값을 받아오지 못하는 이유는 포트번호 때문이 아닐지 생각이 들었다.

 

일단 지금까지 한 것을 살펴보도록 하겠다.

프론트엔드 (client)

App.js

import Todolist from "./todolist";

function App() {
  return (
    <>
      <Todolist />
    </>
  );
}

export default App;

index.js

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
);

todolist.js

import axios from 'axios';
import { useEffect } from 'react';
import { useState } from 'react';

export default function Todolist () {
    const [todos, setTodos] = useState([]);
    const [loading, setLo] = useState(true);// 데이터를 제대로 받아오면 false 아니면 true
    
    useEffect(() => {
        const tododata = async () => {
            const res = await axios({
                method: "GET",
                url: "/todos"
            });
            console.log(res.data.data);
            setTodos(res.data.data);
            setLo(false);
        }
        tododata();
    }, []);

    const Add_todolist = async () => {
        const update_data = document.querySelector('.todo_text').value

        
        const res = await axios({
            method: "POST",
            url: "/todos",
            data: {
                title: update_data,
                done: 0, 
            },
        });
        
        todos.push({
            id: res.id,
            title: res.title,
            done: res.done,
        });
    }
    
    return (
        <>
            <div>
                {
                    loading ? (
                        <h1>로딩중...</h1>
                    ) : (
                       <>
                        <input className='todo_text' type='text' placeholder='추가할 todolist 작성'></input><button onClick={Add_todolist}>추가</button>
                        <ul>
                            {todos.map((data) => {
                                return <li key={data.id}>{data.title}</li>;
                            })}
                       </ul> 
                       </>
                    )
                }
            </div>
        </>
    );
}

 

백엔드(server) : 컨트롤러는 넘어가고 연결되는 코드만 보여주도록하겠다.

router.js

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

//리액트 연결
router.get('/', (req, res) => {
    res.sendFile(path.join(__dirname, 'client/build/index.html'));
});

router.get('*', (req, res) => {
    res.sendFile(path.join(__dirname, 'client/build/index.html'));
});

router.get('/todos', controller.get_todo);
router.post('/todos', controller.post_todo);
router.delete('/todo/:todoId', controller.del_todo);
router.patch('/todo/:todoId', controller.patch_todo);

module.exports = router;

index.js

const express = require('express');
const app = express();
const PORT = 8080;
const db = require('./models');
const path = require('path');

// app.set('view engine', 'ejs'); //ejs를 사용할 때 설정

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

app.use(express.static(path.join(__dirname, 'client/build')));

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

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

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

 

이 상태에서 리액트는 기본적으로 localhost:3000에서 접근할 수 있도 express는 내가 설정한 localhost:8080에서 접근할 수 있다. 이 접근을 같은 곳에서 하게 해주면 되는 것 같다. 

 

이는 node에서 cors를 설정해주고, React에서 proxy를 설정해주면된다. 위에서는 앞서 말했듯이 리액트에서 빌드를 한 후에 백엔드에서 app.use로 경로를 설정해주고, 라우터에서 빌드 파일과 연결을 해준 상태다. 이 다음 단계는 Concurrently와 http-proxy-middleware를 설치한 후 적용해야한다.

먼저 Concurrently를 설치해주자. 

npm i concurrently

두 개를 모두 포함하고 있는 파일에 설치해준다. 그리고 client 폴더로 이동해서 

npm i http-proxy-middleware

를 설치 해준다.

concurrently는 한 개의 터미널에서 두 개의 서버 작동이 가능하도록 해주는 역할을 한다. package.json의 script 섹션을 다음과 같이 추가해주면 npm run dev를 통해 두 서버를 동시에 실행시킬 수 있다.

  "scripts": {
    "test": "node app",
    "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
    "dev:server": "cd server && node index.js",
    "dev:client": "cd client && npm start"
  },

 

리액트에서 setup.js를 만들어주고

const { createProxyMiddleware } = require("http-proxy-middleware");

module.exports = function (app) {
  app.use(
    createProxyMiddleware("/api/", {
      target: "http://localhost:8080",
      changeOrigin: true,
    })
  );
};

이렇게 만들어주면 이제 localhost:3000/api 이하의 경로로 시작되는 요청이 http://localhost:5000/api 에서 처리되기 때문에 axios를 이용할 수 있다.

 

근데 계속 axios를 받아오지 못한다ㅠㅠ 이유를 도저히 모르겠다 리더님한테 가서 물어봐야겠다.

 

 


Node.js와 React 연결 성공!


드디어 해결했다 Concurrently와 http-proxy-middleware를 이용하는 것은 다 수동으로 할 수 있는 동작이었다.

 

터미널을 두개를 열어서 서버와 리액트에서 각각 실행해주면 되고, 프록시 없이 리액트에서 http://localhost8080/todos로 연결을하면 된다. 그래서 데이터베이스의 정보를 가져오는 것은 성공했다.

서버 터미널

클라이언트 터미널과 주소

이런 방식으로 하면 (위를 보면 리액트 앱인  client에서 주소를 호출할때는 node.js에서 연결된 localhost:8080으로 url을 지정해준 것을 확인할 수 있다.)

데이터가 넘어온것을 확인할 수 있다. 이제 다시 작업을 해보자

 

여기서 문제점은 받아노 데이터가 객체이기 때문에 map함수를 사용해서 화면에 띄우는 것은 어렵다는 것이다. 그래서 계속 고민을 하다가

 

[JavaScript]객체를 배열로 변환하는 방법

이번 포스팅에서는 JavaScript에서 객체를 배열로 변환하는 방법들을 소개합니다. 고전적인 방법 JavaScript에서 객체를 배열로 변환하는 고전적인 방법은 객체의 속성을 반복적으로 접근하여 push()

developer-talk.tistory.com

여기서 Object.valuse함수를 통해 객체를 배열로 바꿀 수 있다는 것을 알게 되었고, 바로 적용하였다. 

연결 하는 법을 알았고 이를 기반으로 투두리스트 추가와 리스트 불러오기 기능은 완성했다. 다음 포스팅에서 리스트 불러오기와 추가한 부분까지 정리를 한 후에 수정과 삭제 기능을 구현하고, css까지 완성할 예정이다.

+ Recent posts