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
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는 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/
https://www.youtube.com/watch?v=RPyvHmnOcas&list=PL93mKxaRDidEfLM0I_FFb-98vfAQgXT82&index=32
'React' 카테고리의 다른 글
[리액트] react-router v5->v6 변경점 (0) | 2022.08.07 |
---|---|
[React]react hook & SpringBoot [1/2] (0) | 2022.03.23 |
댓글