본문 바로가기
목차
React

[React]react hook & SpringBoot [2/2]

by 지각생 2022. 8. 7.
728x90

19강 - Springboot 서버 만들기(1/3)

사용할 기능들

  • RestController
  • JPA
  • H2
  • Junit

 

프로젝트 생성

  • [ File ] - [ New ] - [ Other ]

 

  • [ "spring" ] - [ spring starter project ] - [ next ]

 

  • Name : book
  • package :  com.cos.book
  • [ Next ]

  • Spring boot DevTools
  • Lombok
  • Spring Data JPA
  • H2 Database
  • Spring Web
  • [ finish ]

  • [ 패키지 우클릭 ] - [ new ] - [ Package ]
  • Name : com.cos.book.web

 

  • [ web 패키지 우클릭 ] - [ new ] - [Class ]

Name : BookController

[ finish ]

 

서버 실행 테스트

BookController.java 작성

package com.example.book.web;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BookController {

	@GetMapping("/")
	public ResponseEntity<?> findAll(){
		return new ResponseEntity<String>("ok",HttpStatus.OK);
	}
}

 

서버실행

  • [ Run As ] - [ Spring boot App ]

 

launch 오류시 참고 링크

https://late90.tistory.com/475

 

[STS4] The selection cannot be launched and there are no recent launches

해결 방법: 메뉴 창에서 1. Window - > Preferecences 클릭 2. Run/Debug -> Launching 란을 보면 Launch Operation 항목이 있다. Launch the selected resource or active editor. if not launchable: 항목의 라..

late90.tistory.com

Ansi Console 오류

이렇게 창이 뜨긴 하는데 서버는 정상작동된다. 나중에 알아보고 필요하면 수정해줘야겠다.

 

결과 화면

브라우저에 localhost:8080 직접 입력해 주자.

 

HTTP응답 상태 코드

  • 100번 대 : 기다려
  • 200번 대 : 잘했어
  • 300번 대 : 다른 주소로 돌려줄게
  • 400번 대 : 클라이언트가 요청 잘못했어.
  • 500번 대 : 서버 잘못 만들었어.

 

H2 사용해보기

서버 실행해보면 콘솔에 h2 URL정보가 있다.

 

  • localhost:8080/h2-console
  • JDBC URL : STS4에서 서버 실행 후 콘솔 창에서 URL 정보 드래그 복붙
  • User Name : sa           (dafault 값) 
  • Password :                   (dafault 값)
  • [ Test Connection ]

application.yml 설정

캡처버젼

server:
  servlet:
    encoding:
      charset: utf-8
      enabled: true

spring:
  datasource:
      url: jdbc:h2:mem:testdb
      driver-class-name: org.h2.Driver
      username: sa
      password:
      
  jpa:
    hibernate:
      ddl-auto: create #create, update, none
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

강의 영상과 다른건지 자동생성 안되는 문장도 있었지만 강제로 기입하니 정상 작동되는 걸 확인했다.

 

model 만들기

[ 패키지 우클릭] - [ name : com.cos.book.domain ] - [ finish ]

[ 도메인 우클릭 ] - [ new ] - [ Class ]

[ name : book ] - [ finish ]

 

캡처버젼

package com.example.book.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity // 서버 실행시에 Object Relation Mapping이 됨.서버 실행시에 테이블이 h2dㅔ 생성됨.
public class book {
	@Id // PK를 해당 변수로 하겠다는 뜻.
	@GeneratedValue(strategy = GenerationType.IDENTITY) // 해당 데이터베이스 번호증가 전략을 따라가겠다.
	private Long id;
	
	private String title;
	private String auther;

}

 

Repository 만들기

[ 도메인 패키지 우클릭 ] - [ Interface ] - [ Name : BookRepository ] - [ finish ]

 

캡처버젼

package com.example.book.domain;


// @Repository 적어야 스프링 IoC에 빈으로 등록이 되는데
// JpaRepository를 extends하면 생략가능함.
// JpaRepository는 CRUD 함수를 들고 있음.
public interface BookRepository extends JpaRepository<Book, Long>{

}

 

Service 만들기

[ 패키지 우클릭 ] - [ Name : com.cos.book.service ] - [finiish]

[ 서비스 패키지 우클릭 ] - [ name : BookService ] - [ finish ]

 

캡처버젼

package com.example.book.service;

import org.springframework.stereotype.Service;

//기능을 정의할 수 있고, 트랜잭션을 관리할 수 있음.

@Service
public class BookService {

}

 

20강 - Springboot 서버 만들기(2/3)

H2 접속 전

[ Connect ]

 

H2 접속

[ SELECT * FROM BOOK ] - [ ctrl +enter ] 

 

Service 기능 구현

롬복 설치 (mac용)

https://late90.tistory.com/476

 

BookService.java

package com.example.book.service;

import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.book.domain.Book;
import com.example.book.domain.BookRepository;

//기능을 정의할 수 있고, 트랜잭션을 관리할 수 있음.

@Service
public class BookService {

	private BookRepository bookRepository;
	
	//org.springframework.transaction.annotation.Transactional
	@Transactional //서비스 함수가 종료될 때 commit할지 rollback할지 트랜잭션 관리하겠다.
	public Book 저장하기(Book book) {
		return bookRepository.save(book);
	}
	
	@Transactional(readOnly = true) //JPA 변경감지라는 내부 기능 활성화 X, update시의 정합성을 유지해줌. insert의 유령데이터현상(팬텀현상) 못 막아줌.
	public Book 한건가져오기(Long id) {
		return bookRepository.findById(id)
				.orElseThrow(()->new IllegalArgumentException("id를 확인해 주세요!!"));
	}

//	 강의에선 아래를 위 처럼 간소화한거라는데 아래 코드를 쓰면 오류가 뜬다. 	
//	public Book 한건가져오기(Long id) {
//		return bookRepository.findById(id)
//				.orElseThrow(new Supplier<IllegalArgumentException>() {
//					
//					@Override
//					public IllegalArgumentException get() {
//						return new IllegalArgumentException("id를 확인해주세요!!");
//					}
//				});
//	}
	
	@Transactional(readOnly = true)
	public List<Book> 모두가져오기(){
		return bookRepository.findAll();
	}
	
	@Transactional
	public Book 수정하기(Long id, Book book) {
		//더티체킹 update치기
	
		Book bookEntity = bookRepository.findById(id)
				.orElseThrow(()->new IllegalArgumentException("id를 확인해 주세요!!")); //영속화 (book 오브젝트)->영속성 컨텍스트 보관
		bookEntity.setTitle(book.getTitle());
		bookEntity.setAuthor(book.getAuthor());
		return bookEntity;
	} //함수종료 => 트랜잭션 종료 => 영속화 되어있는 데이터를 DB로 갱신(flush) => commit ===> 더티체킹
	
	
	@Transactional
	public String 삭제하기(Long id) {
		bookRepository.deleteById(id); //오류가 터지면 익셉션을 타니깐 신경쓰지 말
		return "ok";
	}
}

 

 

22강 - Springboot Junit5 테스트(1/4)

[ src/test/java 폴더 ] - [ com.cos.book.web패키지 생성 ]

[ BookControllerUnitTest 클래스 생성 ]

[ BookControllerIntegreTest 클래스 생성 ]

 

Service, Repository 클래스도 만들기

 

Junit5 써보기

7행 작성 후 ctrl +shift + O 하면 3행 import확인가능. 마우스 커서를 3행에 올리면

82행과 같은 내용이 포함되어 있는지 꼭 확인할 것. 없으면 직접 입력해줘야한다.

스프링으로 확장해주는 구문이다. 필수로 필요하다.

 

package com.example.book.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

/*
 * 통합 테스트 (모든 Bean들을 똑같이 IoC 올리고 테스트 하는 것)
 * WebEnvironment.MOCK = 실제 톰캣을 올리는게 아니라, 다른 톰캣으로 테스트
 * WebEnvironment.RANDOM_POR = 실제 톰캣으로 테스트
 * @AutoConfigureMockMvc MockMvc를 IoC에 등록해줌.
 * @Transactional은 각각의 테스트 함수가 종료될 때마다 트랜잭션을 rollback해주는 어노테이션!!
 */

@Transactional
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
public class BookControllerIntegreTest {

	@Autowired
	private MockMvc mockMvc;
	
	
}
package com.example.book.web;

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;

//단위 테스트 (Filter, ControllerAdvice등은 메모리에 뜬다. controller관련 로직 뜸.)

@WebMvcTest
public class BookControllerUnitTest {

}
package com.example.book.service;

import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.example.book.domain.BookRepository;

/*
 * 단위 테스트 (Service와 관련된 애들만 메모리에 띄우면 됨.)
 * BoardRepository => 가짜 객체로 만들 수 있음.
 * 
 */

@ExtendWith(MockitoExtension.class)
public class BookServiceUnitTest {
	
	@InjectMocks // BookService 객체가 만들어질 때 BookServiceUnitTest 파일에 @Mock로 등록된 모든애들을 주입받는다.
	private BookService bookService;
	
	@Mock
	private BookRepository bookRepository;
}
package com.example.book.domain;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@AutoConfigureTestDatabase(replace = Replace.ANY)//가짜 db로 테스트. Replace.NONE 실제 DB로 테스
@DataJpaTest //Repository들을 다 IoC 등록해둠.
public class BookRepositoryUnitTest { 
	
	@Autowired
	private BookRepository bookRepository;
}

 

23강 - Springboot Junit5 테스트(2/4)

JUnit 실행하기

실행 단축키 : ctrl+F11

 

~ 12:00 

 

package com.example.book.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

/*
 * 통합 테스트 (모든 Bean들을 똑같이 IoC 올리고 테스트 하는 것)
 * WebEnvironment.MOCK = 실제 톰캣을 올리는게 아니라, 다른 톰캣으로 테스트
 * WebEnvironment.RANDOM_POR = 실제 톰캣으로 테스트
 * @AutoConfigureMockMvc MockMvc를 IoC에 등록해줌.
 * @Transactional은 각각의 테스트 함수가 종료될 때마다 트랜잭션을 rollback해주는 어노테이션!!
 */

@Transactional
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
public class BookControllerIntegreTest {

	@Autowired
	private MockMvc mockMvc;
	
	
}
package com.example.book.web;



import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;

import org.springframework.test.web.servlet.MockMvc;



import lombok.extern.slf4j.Slf4j;



//단위 테스트 (Filter, ControllerAdvice등은 메모리에 뜬다. controller관련 로직 뜸.)



@Slf4j

@WebMvcTest

public class BookControllerUnitTest {



@Autowired

private MockMvc mockMvc;



@Test

public void save_테스트() {

log.info("save_테스트() 시작 ===========");

}

}
package com.example.book.domain;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@AutoConfigureTestDatabase(replace = Replace.ANY)//가짜 db로 테스트. Replace.NONE 실제 DB로 테스
@DataJpaTest //Repository들을 다 IoC 등록해둠.
public class BookRepositoryUnitTest { 
	
	@Autowired
	private BookRepository bookRepository;
}

 

26강 - Spring서버와 react연동 준비

연동 준비 

지금까지 만든 스프링 폴더를 복사해서 새로운 폴더에 book-backend로 저장한다.

 

그리고 book이란 폴더 하위에 book-backend를 둔다.

 

 

그리고 sts4 워크 스페이스를 book 폴더로 새로 지정한다.

 

VS Code 실행

VS Code 실행하여 book 폴더 open한다.

그리고 터미널에서

npx create-react-app book-frontend

 

27강 - 라이브러리 세팅

리액트 실행 테스트

cd book-frontend
npm start

위 페이지 나오면 정상 출력된다.

 

서버종료 : Ctrl+C

 

라이브러리 설치

### React

brew install yarn

yarn add react-router-dom

yarn add redux react-redux

yarn add react-bootstrap bootstrap

 

28강 - 화면 구성하기

VS CODE 껐다가 다시 켜기

App.js

import React from 'react';
import { Container } from 'react-bootstrap';
import { Route, Routes } from 'react-router-dom';
import Header from './components/Header';
import Detail from './pages/book/Detail';
import Home from './pages/book/Home';
import SaveForm from './pages/book/SaveForm';
import UpdateForm from './pages/book/UpdateForm';
import JoinForm from './pages/user/JoinForm';
import LoginForm from './pages/user/LoginForm';

function App() {
  return (
    <div>
      <Header />
      <Container>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/saveForm" element={<SaveForm />} />
          <Route path="/book/:id" element={<Detail />} />
          <Route path="/loginForm" element={<LoginForm />} />
          <Route path="/joinForm" element={<JoinForm />} />
          <Route path="/updateForm/:id" element={<UpdateForm />} />
        </Routes>
      </Container>
    </div>
  );
}

export default App;

index.js

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.min.css';

const container = document.getElementById('root');
const root = createRoot(container); // createRoot(container!) if you use TypeScript
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
);
//document.getElementById('root'),

빼고 다 지우기

 

new file

.prettierrc 파일 생성

{
  "singleQuote": true,
  "semi": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 80
}

 

리액트 실행

cd book-frontend
npm start

잘 됨.

 

부트 스트랩 import

https://react-bootstrap.github.io/getting-started/introduction

 

 

화면 구성

이렇게 각 폴더와 파일들 만들어 주고

 

import React from 'react';
import Container from 'react-bootstrap/Container';
import Nav from 'react-bootstrap/Nav';
import Navbar from 'react-bootstrap/Navbar';
import { Link } from 'react-router-dom';

const Header = () => {
  return (
    <>
      <Navbar bg="dark" variant="dark">
        <Container>
          <Link to="/" className="navbar-brand">
            홈
          </Link>
          <Nav className="me-auto">
            <Link to="/joinForm" className="nav-link">
              회원가입
            </Link>
            <Link to="/loginForm" className="nav-link">
              로그인
            </Link>
            <Link to="/saveForm" className="nav-link">
              글쓰기
            </Link>
          </Nav>
        </Container>
      </Navbar>
      <br />
    </>
  );
};
export default Header;
import React from 'react';

const Detail = () => {
  return (
    <div>
      <h1>책 상세보기</h1>
    </div>
  );
};

export default Detail;
import React from 'react';

const Home = () => {
  return (
    <div>
      <h1>책 리스트 보기</h1>
    </div>
  );
};

export default Home;
import React from 'react';

const SaveForm = () => {
  return (
    <div>
      <h1>책 등록하기</h1>
    </div>
  );
};

export default SaveForm;
import React from 'react';

const UpdateForm = () => {
  return (
    <div>
      <h1>책 수정하기</h1>
    </div>
  );
};

export default UpdateForm;
import React from 'react';

const JoinForm = () => {
  return (
    <div>
      <h1>회원가입 폼</h1>
    </div>
  );
};

export default JoinForm;
import React from 'react';

const LoginForm = () => {
  return (
    <div>
      <h1>로그인 창</h1>
    </div>
  );
};

export default LoginForm;

 

 

29강 - 글목록보기

 

재사용되는 아이템은 component로 만들어서 쓰자.

 

import React from 'react';
import { Card } from 'react-bootstrap';
import { Link } from 'react-router-dom';

const BookItem = () => {
  return (
    <Card>
      <Card.Body>
        <Card.Title>제목1</Card.Title>
        <Link to={'/post/1'} className="btn btn-primary">
          상세보기
        </Link>
      </Card.Body>
    </Card>
  );
};
//button대신 Link to="" 가 더 편하다.
export default BookItem;

BookItem.js 생성 

import React from 'react';
import BookItem from '../../components/BookItem';

const Home = () => {
  return (
    <div>
      <BookItem />
    </div>
  );
};

export default Home;

Home.js 수정

 

STS4

Application.yml에서

ddl-auto : update로 수정 후

서버 실행

 

import React, { useEffect, useState } from 'react';
import BookItem from '../../components/BookItem';

const Home = () => {
  const [books, setBooks] = useState([]);

  //함수 실행시 최초 한번 실행되는 것

  useEffect(() => {
    fetch('http://localhost:8080/book')
      .then((res) => res.json())
      .then((res) => {
        console.log(1, res);
      }); //비동기 함수
  }, []);

  return (
    <div>
      <BookItem />
    </div>
  );
};

export default Home;

Home.js 수정해서 브라우저 콘솔 보면

CORS policy 위반을 확인할 수 있다.

이건 외부 자바스크립트를 막는 정책이다.

 

이걸 임시적으로 풀기 위해

 

30행에 @CrossOrigin을 쓰면 외부 자바스크립트 허용해준다.

 

그러고서

Postman으로 보낸 데이터를 리액트에서 받아보면

 

좌측 fetch구문으로 Postman에서 보낸 json 데이터를 받았고

이에 따라 우측에 <card>제목1<card/> 화면을 볼 수 있다.

그리고 그 아래 

  • index.js:1437 Warning: Each child in a list should have a unique "key" prop.

와 같은 에러를 볼 수 있다.

 

배열에 따른 key값을 할당해주지 않아서 그런거다.

20행에 key={book.id}처럼 구별할 수 있을 값을 넣어주자.

 

 

30강 - 글쓰기

강의 시작 전 코드 

App.js

import React from 'react';
import { Container } from 'react-bootstrap';
import { Route, Routes } from 'react-router-dom';
import Header from './components/Header';
import Detail from './pages/book/Detail';
import Home from './pages/book/Home';
import SaveForm from './pages/book/SaveForm';
import UpdateForm from './pages/book/UpdateForm';
import JoinForm from './pages/user/JoinForm';
import LoginForm from './pages/user/LoginForm';

function App() {
  return (
    <div>
      <Header />
      <Container>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/saveForm" element={<SaveForm />} />
          <Route path="/book/:id" element={<Detail />} />
          <Route path="/loginForm" element={<LoginForm />} />
          <Route path="/joinForm" element={<JoinForm />} />
          <Route path="/updateForm/:id" element={<UpdateForm />} />
        </Routes>
      </Container>
    </div>
  );
}

export default App;

index.js

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.min.css';

const container = document.getElementById('root');
const root = createRoot(container); // createRoot(container!) if you use TypeScript
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
);
//document.getElementById('root'),

./components/BookItem.js

import React from 'react';
import { Card } from 'react-bootstrap';
import { Link } from 'react-router-dom';

const BookItem = (props) => {
  const { id, title } = props.book;
  return (
    <Card>
      <Card.Body>
        <Card.Title>{title}</Card.Title>
        <Link to={'/post/' + id} className="btn btn-primary">
          상세보기
        </Link>
      </Card.Body>
    </Card>
  );
};
//button대신 Link to="" 가 더 편하다.
export default BookItem;

./components/Headers.js

import React from 'react';
import Container from 'react-bootstrap/Container';
import Nav from 'react-bootstrap/Nav';
import Navbar from 'react-bootstrap/Navbar';
import { Link } from 'react-router-dom';

const Header = () => {
  return (
    <>
      <Navbar bg="dark" variant="dark">
        <Container>
          <Link to="/" className="navbar-brand">
            홈
          </Link>
          <Nav className="me-auto">
            <Link to="/joinForm" className="nav-link">
              회원가입
            </Link>
            <Link to="/loginForm" className="nav-link">
              로그인
            </Link>
            <Link to="/saveForm" className="nav-link">
              글쓰기
            </Link>
          </Nav>
        </Container>
      </Navbar>
      <br />
    </>
  );
};
export default Header;

 

./pages/book/Detail.js

import React from 'react';

const Detail = () => {
  return (
    <div>
      <h1>책 상세보기</h1>
    </div>
  );
};

export default Detail;

./pages/book/Home.js

import React, { useEffect, useState } from 'react';
import BookItem from '../../components/BookItem';

const Home = () => {
  const [books, setBooks] = useState([]);

  //함수 실행시 최초 한번 실행되는 것

  useEffect(() => {
    fetch('http://localhost:8080/book')
      .then((res) => res.json())
      .then((res) => {
        console.log(1, [
          { id: 1, title: '제목1', author: '쌀' },
          { id: 2, title: '제목2', author: '쌀' },
        ]);
        setBooks([
          { id: 1, title: '제목1', author: '쌀' },
          { id: 2, title: '제목2', author: '쌀' },
        ]);
      }); //비동기 함수
  }, []);

  return (
    <div>
      {books.map((book) => (
        <BookItem key={book.id} book={book} />
      ))}
    </div>
  );
};

export default Home;

./pages/book/SaveForm.js

import React from 'react';

const SaveForm = () => {
  return (
    <div>
      <h1>책 등록하기</h1>
    </div>
  );
};

export default SaveForm;

./pages/book/UpdateForm.js

import React from 'react';

const UpdateForm = () => {
  return (
    <div>
      <h1>책 수정하기</h1>
    </div>
  );
};

export default UpdateForm;

 

./pages/user/JoinForm.js

import React from 'react';

const SaveForm = () => {
  return (
    <div>
      <h1>책 등록하기</h1>
    </div>
  );
};

export default SaveForm;

./pages/user/LoginForm.js

import React from 'react';

const UpdateForm = () => {
  return (
    <div>
      <h1>책 수정하기</h1>
    </div>
  );
};

export default UpdateForm;

 

강의 시작

리액트 부트스트랩 url:

https://react-bootstrap.netlify.app/forms/overview/#rb-docs-content

 

import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';

function BasicExample() {
  return (
    <Form>
      <Form.Group className="mb-3" controlId="formBasicEmail">
        <Form.Label>Email address</Form.Label>
        <Form.Control type="email" placeholder="Enter email" />
        <Form.Text className="text-muted">
          We'll never share your email with anyone else.
        </Form.Text>
      </Form.Group>

      <Form.Group className="mb-3" controlId="formBasicPassword">
        <Form.Label>Password</Form.Label>
        <Form.Control type="password" placeholder="Password" />
      </Form.Group>
      <Form.Group className="mb-3" controlId="formBasicCheckbox">
        <Form.Check type="checkbox" label="Check me out" />
      </Form.Group>
      <Button variant="primary" type="submit">
        Submit
      </Button>
    </Form>
  );
}

export default BasicExample;

부트 스트랩 copy 해서

 

import React from 'react';
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';

const SaveForm = () => {
  return (
    <Form>
      <Form.Group className="mb-3" controlId="formBasicEmail">
        <Form.Label>Email address</Form.Label>
        <Form.Control type="email" placeholder="Enter email" />
        <Form.Text className="text-muted">
          We'll never share your email with anyone else.
        </Form.Text>
      </Form.Group>

      <Form.Group className="mb-3" controlId="formBasicPassword">
        <Form.Label>Password</Form.Label>
        <Form.Control type="password" placeholder="Password" />
      </Form.Group>
      <Form.Group className="mb-3" controlId="formBasicCheckbox">
        <Form.Check type="checkbox" label="Check me out" />
      </Form.Group>
      <Button variant="primary" type="submit">
        Submit
      </Button>
    </Form>
  );
};

export default SaveForm;

./pages/book/SaveForm.js 

return ( 안에 붙여 놓고 코드 정리해 주기)

 

import React, { useState } from 'react';
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';

const SaveForm = () => {
  const [book, setBook] = useState({
    title: '',
    author: '',
  });

  const changeValue = (e) => {
    setBook({
      ...book,
      [e.target.name]: e.target.value,
    });
  };

  const submitBook = (e) => {
    e.preventDefault(); //submit이 action을 안타고 자기 할 일을 그만함.
    fetch('http://localhost:8080/book', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
      },
      body: JSON.stringify(book),
    })
      .then((res) => res.json())
      .then((res) => {
        console.log(res);
      });
  };

  return (
    <Form onSubmit={submitBook}>
      <Form.Group className="mb-3" controlId="formBasicEmail">
        <Form.Label>Title</Form.Label>
        <Form.Control
          type="text"
          placeholder="Enter Title"
          onChange={changeValue}
          name="title"
        />
      </Form.Group>
      <Form.Group className="mb-3" controlId="formBasicEmail">
        <Form.Label>Author</Form.Label>
        <Form.Control
          type="text"
          placeholder="Enter Author"
          onChange={changeValue}
          name="author"
        />
      </Form.Group>

      <Button variant="primary" type="submit">
        Submit
      </Button>
    </Form>
  );
};

export default SaveForm;

서버에 데이터 보내기 위한 코드 마저 작성 후 실행하면 또 CORS policy 오류가 뜬다.

 

그럼 다시 sts4에 가서 25행과 같이 작성해준다.

 

title과 author에 값 입력 후 submit하면 콘솔에 값이 전송된 걸 확인할 수 있다.

 

따라서

메뉴에서 홈으로 가면 방금 입력한 값이 추가 생성된 걸 확인 할 수 있다.

 

나의 경우 sts4에서

status값이 찍혀 나오지 않아 status값은 활용할 수 없었다.

이 부분은 복습하면서 재확인 해봐야겠다.

 

미흡한 SaveForm.js 

import React, { useState } from 'react';
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';

const SaveForm = (props) => {
  const [book, setBook] = useState({
    title: '',
    author: '',
  });

  const changeValue = (e) => {
    setBook({
      ...book,
      [e.target.name]: e.target.value,
    });
  };

  const submitBook = (e) => {
    e.preventDefault(); //submit이 action을 안타고 자기 할 일을 그만함.
    fetch('http://localhost:8080/book', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
      },
      body: JSON.stringify(book),
    })
      .then((res) => res.json())
      .then((res) => {
        console.log(res);
        if (res.st === 200) {
          return res.json();
        } else {
          return null;
        }
      })
      .then((res) => {
        if (res !== null) {
          props.history.push('/');
        } else {
          console.log('실패:', res);
          alert('책 등록에 실패하였습니다.');
        }
      })
      .catch((error) => {
        console.log(error);
      });
  };

  return (
    <Form onSubmit={submitBook}>
      <Form.Group className="mb-3" controlId="formBasicEmail">
        <Form.Label>Title</Form.Label>
        <Form.Control
          type="text"
          placeholder="Enter Title"
          onChange={changeValue}
          name="title"
        />
      </Form.Group>
      <Form.Group className="mb-3" controlId="formBasicEmail">
        <Form.Label>Author</Form.Label>
        <Form.Control
          type="text"
          placeholder="Enter Author"
          onChange={changeValue}
          name="author"
        />
      </Form.Group>

      <Button variant="primary" type="submit">
        Submit
      </Button>
    </Form>
  );
};

export default SaveForm;

(data 처리부분은 공부 후 다시 이해해보자)

 

31강 - 상세보기

./components/BookItem.js

import React from 'react';
import { Card } from 'react-bootstrap';
import { Link } from 'react-router-dom';

const BookItem = (props) => {
  const { id, title } = props.book;
  return (
    <Card>
      <Card.Body>
        <Card.Title>{title}</Card.Title>
        <Link to={'/book/' + id} className="btn btn-primary">
          상세보기
        </Link>
      </Card.Body>
    </Card>
  );
};
//button대신 Link to="" 가 더 편하다.
export default BookItem;

./pages/book/Detail.js

import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';

const Detail = (props) => {
  const propsParam = useParams();
  const id = propsParam.id;

  const [book, setBook] = useState({
    id: '',
    title: '',
    author: '',
  });

  useEffect(() => {
    fetch('http://localhost:8080/book/' + id)
      .then((res) => res.json())
      .then((res) => {
        setBook(res);
      });
  }, []);

  return (
    <div>
      <h1>책 상세보기</h1>
      <hr />
      <h3>{book.author}</h3>
      <h1>{book.title}</h1>
    </div>
  );
};

export default Detail;

 

32강 - 수정삭제하기

 

import React from 'react';
import { Container } from 'react-bootstrap';
import { Route, Routes } from 'react-router-dom';
import Header from './components/Header';
import Detail from './pages/book/Detail';
import Home from './pages/book/Home';
import SaveForm from './pages/book/SaveForm';
import UpdateForm from './pages/book/UpdateForm';
import JoinForm from './pages/user/JoinForm';
import LoginForm from './pages/user/LoginForm';

function App() {
  return (
    <div>
      <Header />
      <Container>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/saveForm" element={<SaveForm />} />
          <Route path="/book/:id" element={<Detail />} />
          <Route path="/loginForm" element={<LoginForm />} />
          <Route path="/joinForm" element={<JoinForm />} />
          <Route path="/updateForm/:id" element={<UpdateForm />} />
        </Routes>
      </Container>
    </div>
  );
}

export default App;
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.min.css';

const container = document.getElementById('root');
const root = createRoot(container); // createRoot(container!) if you use TypeScript
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
);
//document.getElementById('root'),

 

import React from 'react';
import { Card } from 'react-bootstrap';
import { Link } from 'react-router-dom';

const BookItem = (props) => {
  const { id, title } = props.book;
  return (
    <Card>
      <Card.Body>
        <Card.Title>{title}</Card.Title>
        <Link to={'/book/' + id} className="btn btn-primary">
          상세보기
        </Link>
      </Card.Body>
    </Card>
  );
};
//button대신 Link to="" 가 더 편하다.
export default BookItem;
import React from 'react';
import Container from 'react-bootstrap/Container';
import Nav from 'react-bootstrap/Nav';
import Navbar from 'react-bootstrap/Navbar';
import { Link } from 'react-router-dom';

const Header = () => {
  return (
    <>
      <Navbar bg="dark" variant="dark">
        <Container>
          <Link to="/" className="navbar-brand">
            홈
          </Link>
          <Nav className="me-auto">
            <Link to="/joinForm" className="nav-link">
              회원가입
            </Link>
            <Link to="/loginForm" className="nav-link">
              로그인
            </Link>
            <Link to="/saveForm" className="nav-link">
              글쓰기
            </Link>
          </Nav>
        </Container>
      </Navbar>
      <br />
    </>
  );
};
export default Header;

 

 

import React, { useEffect, useState } from 'react';
import { Button } from 'react-bootstrap';
import { useParams } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';

const Detail = (props) => {
  const propsParam = useParams();
  const id = propsParam.id;
  const navigate = useNavigate();
  const goHome = () => {
    navigate('/');
  };
  const [book, setBook] = useState({
    id: '',
    title: '',
    author: '',
  });

  useEffect(() => {
    fetch('http://localhost:8080/book/' + id)
      .then((res) => res.json())
      .then((res) => {
        setBook(res);
      });
  }, []);

  const deleteBook = () => {
    fetch('http://localhost:8080/book/' + id, { method: 'DELETE' })
      .then((res) => res.text())
      .then((res) => {
        goHome();
      });
  };

  const updateBook = () => {
    navigate('/updateForm/' + id);
  };

  return (
    <div>
      <h1>책 상세보기</h1>
      <Button variant="warning" onClick={updateBook}>
        수정
      </Button>{' '}
      <Button variant="danger" onClick={() => deleteBook(deleteBook)}>
        삭제
      </Button>
      <hr />
      <h3>{book.author}</h3>
      <h1>{book.title}</h1>
    </div>
  );
};

export default Detail;
import React, { useEffect, useState } from 'react';
import BookItem from '../../components/BookItem';

const Home = () => {
  const [books, setBooks] = useState([]);

  //함수 실행시 최초 한번 실행되는 것

  useEffect(() => {
    fetch('http://localhost:8080/book')
      .then((res) => res.json())
      .then((res) => {
        console.log(1, res);

        setBooks(res);
      }); //비동기 함수
  }, []);

  return (
    <div>
      {books.map((book) => (
        <BookItem key={book.id} book={book} />
      ))}
    </div>
  );
};

export default Home;
import React, { useState } from 'react';
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';

const SaveForm = (props) => {
  const [book, setBook] = useState({
    title: '',
    author: '',
  });

  const changeValue = (e) => {
    setBook({
      ...book,
      [e.target.name]: e.target.value,
    });
  };

  const submitBook = (e) => {
    e.preventDefault(); //submit이 action을 안타고 자기 할 일을 그만함.
    fetch('http://localhost:8080/book', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
      },
      body: JSON.stringify(book),
    })
      .then((res) => res.json())
      .then((res) => {
        console.log(res);
        if (res.st === 200) {
          return res.json();
        } else {
          return null;
        }
      })
      .then((res) => {
        if (res !== null) {
          props.history.push('/');
        } else {
          console.log('실패:', res);
        }
      })
      .catch((error) => {
        console.log(error);
      });
  };

  return (
    <Form onSubmit={submitBook}>
      <Form.Group className="mb-3" controlId="formBasicEmail">
        <Form.Label>Title</Form.Label>
        <Form.Control
          type="text"
          placeholder="Enter Title"
          onChange={changeValue}
          name="title"
        />
      </Form.Group>
      <Form.Group className="mb-3" controlId="formBasicEmail">
        <Form.Label>Author</Form.Label>
        <Form.Control
          type="text"
          placeholder="Enter Author"
          onChange={changeValue}
          name="author"
        />
      </Form.Group>

      <Button variant="primary" type="submit">
        Submit
      </Button>
    </Form>
  );
};

export default SaveForm;
import React, { useEffect, useState } from 'react';
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';
import { useNavigate, useParams } from 'react-router-dom';

const UpdateForm = (props) => {
  const propsParam = useParams();
  const id = propsParam.id;
  const navigate = useNavigate();
  const goHome = () => {
    navigate('/');
  };
  const [book, setBook] = useState({
    title: '',
    author: '',
  });

  useEffect(() => {
    fetch('http://localhost:8080/book/' + id)
      .then((res) => res.json())
      .then((res) => {
        setBook(res);
      });
  }, []);

  const changeValue = (e) => {
    setBook({
      ...book,
      [e.target.name]: e.target.value,
    });
  };

  const submitBook = (e) => {
    e.preventDefault(); //submit이 action을 안타고 자기 할 일을 그만함.
    fetch('http://localhost:8080/book/' + id, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
      },
      body: JSON.stringify(book),
    })
      .then((res) => res.json())
      .then((res) => {
        console.log(res);
        goHome();
      })
      .catch((error) => {
        console.log(error);
      });
  };

  return (
    <Form onSubmit={submitBook}>
      <Form.Group className="mb-3" controlId="formBasicEmail">
        <Form.Label>Title</Form.Label>
        <Form.Control
          type="text"
          placeholder="Enter Title"
          onChange={changeValue}
          name="title"
          value={book.title}
        />
      </Form.Group>
      <Form.Group className="mb-3" controlId="formBasicEmail">
        <Form.Label>Author</Form.Label>
        <Form.Control
          type="text"
          placeholder="Enter Author"
          onChange={changeValue}
          name="author"
          value={book.author}
        />
      </Form.Group>

      <Button variant="primary" type="submit">
        Submit
      </Button>
    </Form>
  );
};

export default UpdateForm;
import React from 'react';

const JoinForm = () => {
  return (
    <div>
      <h1>회원가입 폼</h1>
    </div>
  );
};

export default JoinForm;
import React from 'react';

const LoginForm = () => {
  return (
    <div>
      <h1>로그인 창</h1>
    </div>
  );
};

export default LoginForm;

배포 설명 : 영상 속 19:00~ 참고

https://www.youtube.com/watch?v=RPyvHmnOcas&list=PL93mKxaRDidEfLM0I_FFb-98vfAQgXT82&index=32 

Nginx로 React를 배포하는 방법

Nginx는 Aphache(아파치)와 같은 웹서버로, React 앱을 배포할 때 사용할 수 있습니다. 우분투 18.04 환경에서 Nginx로 React앱을 배포하는 방법을 알아보겠습니다.

React 앱 빌드

먼저 개발중인 React앱이 있어야 합니다. 기본앱으로 myapp 프로젝트를 만들고 실행되는 것을 확인하였습니다.

$ create-react-app myapp
$ cd myapp
$ npm start

웹서버(Nginx)는 빌드된 파일을 사용하기 때문에, 미리 빌드 산출물을 만들어 놔야 합니다.

$ npm run build

 

Nginx 설정

이제 빌드된 React앱을 웹서버인 Nginx를 통하여 실행되도록 만들면 됩니다.

먼저 다음 명령어로 Nginx를 설치해주세요.

$ sudo apt install nginx

설치가 끝나면 /etc/nginx 경로에 파일들이 생성됩니다. 기본 화면으로 연결되는 Nginx 설정파일들이 이미 만들어져 있는 상태인데요. 우리가 만드는 설정과 겹칠 수 있기 때문에 모두 지우고 시작하겠습니다. 아래 경로에 있는 default 파일들을 삭제해주세요.

$ sudo rm /etc/nginx/sites-available/default
$ sudo rm /etc/nginx/sites-enabled/default

이제 myapp에 대한 Nginx 설정파일을 생성해보겠습니다. 아래 경로로 이동해서 설정파일을 만들어주세요.

$ cd /etc/nginx/sites-available/
$ sudo touch myapp.conf

myapp.conf의 내용은 아래와 같이 입력해주세요. (root의 /home/user/myapp/build는 위에서 만든 React의 빌드 산출물 경로입니다. 자신의 빌드 파일 경로로 변경해야 합니다)

server {
  listen 80;
  location / {
    root   /home/user/myapp/build;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
  }
}

listen 80은 포트 80에 대한 설정을 의미합니다. location /는 URL이 '/'가 포함된 경로에 대한 설정을 의미합니다. root는 실행할 파일들의 루트 위치를 의미합니다. 위에서 빌드한 파일 경로를 입력하면 됩니다. index는 인덱스의 파일들을 지정하는 곳이고, 이 파일들 중 꼭 하나는 root 경로 안에 존재해야 합니다. try_files는 어떤 파일을 찾을 때 명시된 순서로 찾으며, 가장 먼저 발견되는 파일을 사용한다는 의미입니다.

/etc/nginx/sites-available/에 설정 파일을 만들었으면, 아래 명령어로 이 파일의 심볼릭 링크를 /etc/nginx/sites-enabled/에도 만들어주세요. 이름처럼 웹서버가 동작될 때 sites-enabled에 있는 설정파일을 참조합니다.

$ sudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/myapp.conf

파일 생성은 모두 끝났습니다. 이제 Nginx를 실행하시면 됩니다. 아래 명령어로 nginx를 재실행해주세요.

$ sudo systemctl stop nginx
$ sudo systemctl start nginx

웹서버가 잘 동작하는지 상태를 확인하려면 아래 명령어를 사용하면 됩니다.

sudo systemctl status nginx

Nginx가 동작중이라면, 브라우저에서 localhost:80로 접속해보세요.

 

 

출처:

https://codechacha.com/ko/deploy-react-with-nginx/

 

Nginx로 React를 배포하는 방법

React(리액트) 앱을 배포할 때 Nginx, Aphache와 같은 웹서버로 배포를 해야 합니다. Nginx는 오픈소스이며 매우 효율적인 웹서버입니다. 이 글에서 우분투 18.04환경에서 nginx로 React 앱을 배포하는 방법

codechacha.com

https://www.youtube.com/watch?v=RPyvHmnOcas&list=PL93mKxaRDidEfLM0I_FFb-98vfAQgXT82&index=32 

 

728x90

'React' 카테고리의 다른 글

[리액트] react-router v5->v6 변경점  (0) 2022.08.07
[React]react hook & SpringBoot [1/2]  (0) 2022.03.23

댓글