Spring을 통한 MVC 패턴 적용하기
1. 컨트롤 계층
어떤 경우에 Notice~ : 응답이 페이지로 나간다. 따라서 Model 객체가 필요하다. 사용자로부터 입력받는 값을 읽어오기 : @RequestParam 지원. 따라서 req.getParameter() 필요없다 - req 없어도 됨.
어떤 경우에 Rest~ : 응답이 문자열로 나가는 경우
둘의 주요한 차이점은 Http Response Body가 생성되는 방식이다.
@RestController는 @Controller와 @ResponseBody의 조합이다.
@Controller는 View를 거치고 @RestController는 View를 거치지 않음
@Controller에 @ResponseBody를 붙이면 @RestController와 동일하게 작동함
p.935
Model 클래스를 이용하면 메소드 호출 시 JSP(UI, View)로 값을 바로 바인딩하여 전달할 수 있다.
Model 클래스의 addAttribute() 메소드는 ModelAndView의 addObject() 메소드와 같은 기능을 한다.
Model 클래스는 따로 뷰 정보를 전달할 필요가 없을 때 사용하면 편리하다.
@GetMapping("noticeList")
public String noticeList(Model model, @RequestParam Map<String, Object> pMap, HttpServletRequest req){
logger.info("noticeList");
// logger.info(pMap.get("gubun").toString());
// logger.info(req.getParameter("gubun").toString());
// logger.info(pMap.get("keyword").toString());
List<Map<String, Object>> nList = null;
nList = noticeLogic.noticeList(pMap);
model.addAttribute("nList", nList);
return "forward:noticeList.jsp"; // forward니까 webapp 아래에서 찾는다
@Controller("loginCtrl") - "loginController"
< bean id="loginController" class="com.XXX.LoginController"/>
LoginController loginController = new LoginController();
WEB-INF/views/test/result.jspview:
prefix: /WEB-INF/views/
suffix: .jsp
p.936
3. 로그인창에서 요청한 실행결과
그림26-13 url) ~login5.do 이지만 화면은 .jsp가 출력되고 있다 : forward
if문으로 했던 것을 클래스(Model)로 변경했다.
package com.example.demo.pojo1;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.annotation.WebServlet;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
/*
* http://localhost:8000/member/memberInsert.gd - insert:int - 입력
* http://localhost:8000/lecture/lectureInsert.gd
* http://localhost:8000/notice/noticeInsert.gd
* http://localhost:8000/member/memberUpdate.gd - update:int -수정
* http://localhost:8000/lecture/lectureUpdate.gd
* http://localhost:8000/notice/noticeUpdate.gd
* http://localhost:8000/member/memberDelete.gd - delete:int - 삭제
* http://localhost:8000/lecture/lectureDelete.gd
* http://localhost:8000/notice/noticeDelete.gd
* http://localhost:8000/member/memberList.gd - select:List<Map> - dispatcher(forward) - false
* http://localhost:8000/lecture/lectureList.gd - select - forward - false
* http://localhost:8000/notice/noticeLIst.gd - select - forward - false
*
* 왜 서블릿을 상속받는가? - 스프링도 서블릿을 상속받았다
* 웹서비스 제공 - 네트워크 - 프로토콜 - 통신규약 - 준수하는 http
* stateless - 장단점 - 단점보완:세션과쿠키 - 장점:동시접속자
* http프로토콜을 지원하는 api -> request, response
* req - getParameter:String, getAttribute(세션):Object - 유효범위(scope)
* A a = new A(); request, session application
* 페이지 전환(화면전환) - 유지되어야 하는 정보가 있다. - 너의 아이디, 이름 -유지문제
* Sonata myCar = new Sonata();ActionTag-xml문법
* <jsp:useBean id="myCar" class="com.di.Sonata" scope=page|request|session|application/>
* 액션태그로 객체생성이 가능하다
* 저 태그는 jsp에 작성이 된다. - 치명적이다
* jsp -> jsp가 받는다 -> 모델1 -> 재사용성, 이식성 문제제기 -> 별로다 -> 이렇게 하지 않는다 -> 요청을 jsp가 받고 있다???
* jsp -> servlet - > 모델2 : 요청을 서블릿이 받는다
* res - setContentType("text/html;utf-8");
* 응답처리 - html - DataSet을 가지고 있나? - 없음 - javascript - > application/json -> JSON.stringyfy, JSON.parse -> html끼워넣는다 - > DOM API
* UI솔루션, ReactJS 라이브러리 - 다른 언어(이종간)
*
*/
@WebServlet("*.gd")
public class FrontMVC extends HttpServlet {
Logger logger = Logger.getLogger(FrontMVC.class);
private static final long serialVersionUID = 1L;
protected void doService(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
logger.info("doService");
String uri = req.getRequestURI(); // => /notice/noticeInsert.gd?n_title=a&n_content=b
logger.info(uri);
String context = req.getContextPath();// /
logger.info(context);
String command = uri.substring(context.length()+1);//-> notice/noticeInsert.gd
logger.info(command);
//뒤에 의미없는 확장자 gd를 잘라내기
int end = command.lastIndexOf(".");//점이 있는 위치정보를 가져온다
logger.info(""+end);
command = command.substring(0,end);//-> notice/noticeInsert까지만 가져온다. .gd는 빼고서....
logger.info(command);//-> notice/noticeList or notice/noticeInsert or notice/noticeUpdate or notice/noticeDelete
String upmu[] = null;
upmu = command.split("/");
for(String name:upmu) {
logger.info(name);
}
//////////////////////////////////[[ upmu[] ]]///////////////////////////////////
ActionForward af = null;
NoticeController nc = new NoticeController();//결합도가 강하다-별로다-제어역전아니다 - 스프링관계된 포인트
//MemberController mc = new MemberController();
//LectureController lc = new LectureController();
////////////////////////[[ 어떤 컨트롤러를 태울것인가? ]]/////////////////////////
//이 지점은 내려가는 방향이다
if("notice".equals(upmu[0])) {
//왜 NoticeController클래스에 upmu[]을 넣어주나요? - 메소드
//메소드 이름을 가지고 NoticeController에서 if문(조건식이 필요함-upmu[1])을 적어야 한다.
//4가지 - noticeList, noticeInsert, noticeUpdate, noticeDelete
//왜냐면 NoticeController에서 NoticeLogic클래스를 인스턴스화 해야 하니까
//왜요? NoticeLogic에 정의된 메소드를 여기서 호출해야 하니까...
//설계 관점 아쉬움 - 우리는 XXXController에서 부터 메소드를 가질 수 없었나?
//이유- 나는 아직은 메소드마다 req, res에 대한 객체 주입을 처리할 수 없는 구조이니까
req.setAttribute("upmu",upmu);
//컨트롤러가 결정된 이름으로 메소드가 호출되고 있다. - 별로다 - 메소드마다 url패턴을 적용할 수 없어서 if문으로 처리하였다
//스프링에서는 메소드마다 매핑을 지원하는 어노테이션(@RequestMapping, @GetMpping, @PostMapping)이 지원되고 있다
//그런데 나는 이런 설계가 안되어서 getRequestURI() -> upmu[0]=notice, upmu[1]=noticeList
// req.setAttribute("upmu", upmu);
af = nc.execute(req, res);//NoticeController클래스로 건너감 - upmu[1]-메소드이름이니까...
}
else if("member".equals(upmu[0])) {
}
else if("lecture".equals(upmu[0])) {
}
//////////////////////////////////{{ *.gd 어떤 컨트롤클래스를 주입받아야 하는가? }}//////////////////////////////////////
//////////////////////[[ 컨트롤을 타고 난 뒤에 내가 할일은? ]]///////////////////////
//ViewResolver 클래스가 추가될 코드 블록이다 : 1-3에서 개선해본다 - if문으로 분기되는 걸 클래스 설계로 변경해서 처리해본다(의의)
//해당 업무에 대응하는 컨트롤러에서 결정된 페이지 이름을 받아온다
//위에서 결정된 true 혹은 false값에 따라 sendRedirect와 forward를 통해
//응답페이지를 호출해준다. - 이것이 FrontMVC의 역할이다.
//이 지점은 java와 오라클 서버를 경유한 뒤 시점이다.
//문제제기) 여러가지 타입을 고려하다보니 if문이 중첩되었다 : 별로다. 1-3버전에서는 개선해본다(Spring에서는 ViewResolver 클래스로 지원하고 있다)
if(af !=null) {
if(af.isRedirect()) { //true라는 건 sendRedirect인 경우임
//첨부파일을 업로드 하는 것은 페이지 이동과 무관하다
//첨부파일이 처리된 경우에는 path에 null을 반환하게 한다
if(af.getPath() == null) {
return; //해당 메소드 탈출(for문은 break;)
}else {
//파일업로드가 아닌 경우 응답으로 나갈 페이지 url이 담기는 변수가 path이다
//이런 부분들을 스프링에서는 XXXXViewResolver라는 클래스가 지원하는 부분
res.sendRedirect(af.getPath()); //-> notice/noticeList.jsp
}
}
else{ //forward인 경우임 : url 안 바뀜. 화면 바뀜. 유지됨. a페이지에서 쥐는 있는 정보를 b페이지에서도 사용가능함
//슬래쉬가 포함된 경우
if(af.getPath().contains("/")) {
RequestDispatcher view = req.getRequestDispatcher(af.getPath());
view.forward(req, res);
}
else if(af.getPath() == null) { //파일 업로드 처리시 ActionForward를 통해서 값을 리턴받을 때 문제가 발생됨. 이 부분에 대한 해결 프로세스 추가함
logger.info("path가 null일때");
}
//슬래쉬가 미포함인 경우
//-> 슬래쉬가 포함되었다는 건 응답으로 나가는 마임타입이 html이다. path.append(notice/noticeList.jsp)
//1. json 포맷이라면 당연히 없음
//2. 문자열 형식일 때 : ReactJS 같이 이종간의 언어가 뷰계층을 담당할 때 필수템
//3. null일 때 : 이미지 업로드 처리 시나 첨부파일 처리 시에는 리턴으로 나갈 값이 필요없다(이미 업로드를 했으니)
else { // @RestController 지원하기 때문에 추가함
logger.info("슬래쉬가 미포함인 경우 ===> " + af.getPath());
res.setCharacterEncoding("utf-8");
// jsp : html, css, js, java, <%%> 혼재되어 있다. 정적페이지, 동적페이지가 혼재
// ReactJS : 지원하는 방법 - @RestController
res.setContentType("text/plain;utf-8"); // @RestCotroller or @ResponseBody
PrintWriter out = res.getWriter();
out.print(af.getPath());
return;
}
}
}/////////// end of if - 응답페이지 호출하기 - 포워드
//////////////////////////////////////////////////////////////////
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
doService(req,res);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
doService(req,res);
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
logger.info("doPut- 수정할때");
String n_no = req.getParameter("n_no");
String n_title = req.getParameter("n_title");
String n_content = req.getParameter("n_content");
String n_writer = req.getParameter("n_writer");
logger.info(n_no+", "+n_title+", "+n_content+", "+n_writer);
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
logger.info("doDelete- 삭제할때");
String n_no = req.getParameter("n_no");
logger.info(n_no);
}
}
package com.example.demo.pojo2;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
import lombok.extern.slf4j.Slf4j;
//Restful API란 무엇인가?
//전송방식 : 바이너리 - UI솔루션이 지원하는 모드 중 한 가지
@SuppressWarnings("serial")
//@Slf4j : Logger를 쓰지 않고도 사용할 수 있다
@WebServlet("*.gd2")
public class ActionServlet extends HttpServlet {
Logger logger = Logger.getLogger(ActionServlet.class);
protected void doService(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
String uri = req.getRequestURI(); // => /notice/noticeInsert.gd?n_title=a&n_content=b
logger.info(uri);
String context = req.getContextPath();// /
logger.info(context);
String command = uri.substring(context.length()+1);//-> notice/noticeInsert.gd
logger.info(command);
//뒤에 의미없는 확장자 gd를 잘라내기
int end = command.lastIndexOf(".");//점이 있는 위치정보를 가져온다
logger.info(""+end);
command = command.substring(0,end);//-> notice/noticeInsert까지만 가져온다. .gd는 빼고서....
logger.info(command);//-> board/boardList or notice/noticeInsert or notice/noticeUpdate or notice/noticeDelete
String upmu[] = null;
String result = null; // -> redirect:/board/boardList.jsp, forward:/board/boardList.jsp
upmu = command.split("/");
for(String name:upmu) {
logger.info(name);
}
Controller controller = new BoardController();
if("board".equals(upmu[0])) {
logger.info("workname - board - execute 호출");
req.setAttribute("upmu",upmu);
result = controller.execute(req, res);//여기서 BoardController의 path값이 담김
}
//BoardController를 경유한 다음 리턴 값으로 문자열을 받았다
//이 문자열을 잘라서 pageMove에 담아준다
// 1. 널 체크하기
// 2. 문자열 배열을 선언할 것
// 3. 콜론이 포함되어 있나?
// 4. 콜론이 없는 경우도 처리할 것
// 1) redirect 인 경우 : webapp - "redirect:/board/boardList.jsp"
// 2) forward 인 경우 : webapp - "forward:/board/boardList.jsp"
// 3)/WEB-INF/jsp/ - "/board/boardList.jsp"
if(result != null) {
String pageMove[] = null;
if(result.contains(":")) {
logger.info(": 이 포함되어 있어요");
//-> redirect:
pageMove = result.split(":"); //[0]=redirect or forward [1]=board/boardList
logger.info(pageMove);
}// end of 콜론이 있는 경우
//콜론이 없는 경우
else {
pageMove = result.split("/"); //[0]=board [1]=boardList - ModelAndView (WEB-INF/views/+ path(notice/noticeList) + .jsp)
logger.info(pageMove);
}// end of 콜론이 없는 경우
logger.info(pageMove[0] + ", " + pageMove[1]);
if(pageMove != null) {
//치환을 한 번 더 함
String path = pageMove[1]; // board/boardList
if("redirect".equals(pageMove[0])) {
res.sendRedirect(path); // board/boardList.jsp
//forward 처리 시와 동일한 컨벤션을 적용하기 위해 접두어와 접미어를 붙이는 과정에서 오류 발동함.
//현재 구조 상 입력|수정|삭제 모두 처리 성공 시 목록 페이지로 응답이 나가도록 설계 되어 있다는 것을 간과함.
//res.sendRedirect("/"+path+".jsp"); // board/boardList.gd2.jsp
}// end of sendRedirect
else if("forward".equals(pageMove[0])) {
RequestDispatcher view = req.getRequestDispatcher("/"+path+".jsp"); // webapp에 배포
view.forward(req, res);
}// end of forward
else {//콜론이 없는 경우에 실행되는 코드 : WEB-INF
path = pageMove[0]+"/"+pageMove[1]; //board/boardList
// /WEB-INF/jsp/board/boardList.jsp : 스프링에서는 ViewResolver가 해 줌
// URL로 요청 불가한 경우이다 - 404
RequestDispatcher view = req.getRequestDispatcher("/WEB-INF/jsp/"+path+".jsp"); // WEB-INF에 배포
view.forward(req, res);
}//end of 배포 위치가 WEB-INF/ 아래인 경우
}////end of pageMove 배열이 null이 아닌 경우
}//////end of if
}////////end of doService
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
// TODO Auto-generated method stub
doService(req, res);
}
//쿼리스트링, ?, 링크, header, 제한적임
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
// TODO Auto-generated method stub
doService(req, res);
}
//body, 서버인터셉트 안 당함, 무조건 서버전달, 제한이 없음 - 바이너리 타입(첨부파일)
//POST 방식 : enctype="multipart/form-data" - 바이너리 전달 - 문자+숫자 -
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
// TODO Auto-generated method stub
doService(req, res);
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
// TODO Auto-generated method stub
doService(req, res);
}
}
package com.example.demo.pojo3;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
import com.example.demo.pojo2.ActionServlet;
// url을 통해서 요청을 받으므로 그 값을 이용하여 메소드 이름을 결정한다 - upmu.length=2([0]=workname, [1]=~.gd3)
// 표준 서블릿을 상속 받아서 FrontController의 역할을 하는 클래스를 추가하였다
// 스프링에서는 DispatcherServlet이라는 이름으로 제공됨
@SuppressWarnings("serial")
@WebServlet("*.gd3")
public class ActionSupport extends HttpServlet {
Logger logger = Logger.getLogger(ActionServlet.class);
private void doService(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException //입출력은 내 담당 아니니까 컨테이너에게 넘긴다
{
// 표준 서블릿을 이용해서 메소드를 호출해야 했다.
// url이름에서 업무이름과 메소드이름을 추출했다
// -> notice, board, qna, noticeList, boardList, qnaList
String uri = req.getRequestURI(); // => /board/boardInsert.gd?n_title=a&n_content=b
logger.info(uri);
String context = req.getContextPath();// /
logger.info(context);
String command = uri.substring(context.length()+1);//-> board/boardInsert.gd
logger.info(command);
//뒤에 의미없는 확장자 gd를 잘라내기
int end = command.lastIndexOf(".");//점이 있는 위치정보를 가져온다
logger.info(""+end);
command = command.substring(0,end);//-> notice/noticeInsert까지만 가져온다. .gd는 빼고서....
logger.info(command);//-> board/boardList or notice/noticeInsert or notice/noticeUpdate or notice/noticeDelete
String upmu[] = null;
String result = null; // -> redirect:/board/boardList.jsp, forward:/board/boardList.jsp
upmu = command.split("/");
for(String name:upmu) {
//단, 반드시 추상메소드를 먼저 설계할 것 - 이 약속이 지켜져야 컴파일 에러가 없다
logger.info(name); //upmu[0]=board2 upmu[1]=boardList.jsp / boardList.gd3 / boardInsert.gd3 / boardUpdate.gd3 / boardDelete.gd3
}
//여기서 getController() 호출 할 것!!
Object obj = null;
try {
// 이 요청을 어떤 컨트롤러 클래스가 담당하나요?
obj = HandlerMapping.getController(upmu, req, res); //upmu[0]: board2
} catch (Exception e) {
// TODO: handle exception
}
//ViewResolver와 관련된 코드 시작됨
//getController 호출된 다음에 반드시 리턴타입을 받아 타입체크하고 String일 때 ModelAndView일 때를 나누어 처리해야 하니까
//NullPointerException 발동할 가능성 있는 섹션이다. 따라서 반드시 null 체크해야 함
//아래 블록이 ViewResolver 안에 들어가게 됨
if(obj != null) {
logger.info(obj);
String pageMove[] = null;
ModelAndView mav = null; //화면 처리를 위해 등장
//path라면 : 이 너 안에 포함되어 있어? 네 : redirect or forward
// : 이 없는 경우는 WEB-INF/jsp/XXXXX.jsp
if(obj instanceof String) { //너 String타입이니? 네 : json or path
if(((String) obj).contains(":")) { //너 : 있어? 응
pageMove = obj.toString().split(":"); //리턴타입을 Object로 바꿨기 때문에 String으로 변환해야 문자열로 담겨 split 할 수 있다
}
else if(((String) obj).contains("/")) { // 아니
pageMove = obj.toString().split("/");
}
//콜론도 없고 슬래쉬도 없는 경우라면(단순한 문자열을 반환하는 경우 - String path = "avatar.png", "JSON 형식 문자열"
// http://localhost:8000/board2/jsonBoardList.gd3
else {
logger.info(obj.toString()); //JSON
pageMove = new String[1];
pageMove[0] = obj.toString(); //파일이름 같은 경우 저장됨 : 화면에서 후처리하는 용도로 사용할 수 있다
}
}
else if(obj instanceof ModelAndView) {
mav = (ModelAndView)obj;
pageMove = new String[2];
pageMove[0] = "";
pageMove[1] = mav.getViewName();
logger.info("pageMove ==> "+pageMove[0]+", "+pageMove[1]);
}
else if(obj instanceof byte[]) { //바이너리야? //응답이 png일 때 - Quill Editor
res.setContentType("image/png;utf-8");
PrintWriter out = res.getWriter(); //이것의 try catch는 하지 않고 컨테이너에게 넘김. 입출력은 내 담당 아니니까
out.print(obj);
return;
}
if(pageMove != null && pageMove.length == 2) {
logger.info("pageMove 원소의 갯수가 2개 일 때");
String path = pageMove[1];
if("redirect".equals(pageMove[0])) {
res.sendRedirect(path);
} else if("forward".equals(pageMove[0])) {
RequestDispatcher view = req.getRequestDispatcher(path);
view.forward(req, res);
} else { //콜론이 없는 경우에 실행되는 코드 : WEB-INF
// /WEB-INF/jsp/board/boardList.jsp : 스프링에서는 ViewResolver가 해 줌
RequestDispatcher view = req.getRequestDispatcher("/WEB-INF/jsp/"+path+".jsp");
view.forward(req, res);
}
}////////////////////end of if
else if(pageMove != null && pageMove.length == 1) { //quill editor에서 이미지 선택 시 파일이름 반환
res.setContentType("text/plain;charset=utf-8");
PrintWriter out = res.getWriter();
out.print(obj);
return;
}
//JSON포맷으로 반환되는 값을 출력하기 - @ResponseBody(Spring 4.0), @RestController(Spring 5.0) 역할 재현
else {
res.setContentType("text/plain;charset=utf-8");
PrintWriter out = res.getWriter();
out.print(obj);
return;
}
}
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
// TODO Auto-generated method stub
doService(req, res);
}
//쿼리스트링, ?, 링크, header, 제한적임
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
// TODO Auto-generated method stub
doService(req, res);
}
//body, 서버인터셉트 안 당함, 무조건 서버전달, 제한이 없음 - 바이너리 타입(첨부파일)
//POST 방식 : enctype="multipart/form-data" - 바이너리 전달 - 문자+숫자 -
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
// TODO Auto-generated method stub
doService(req, res);
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
// TODO Auto-generated method stub
doService(req, res);
}
}
node_modules 파일크기가 크기 때문에 npm install 해서 설치
가상의 DOM을 사용해 메모리에 미리 렌더링을 함. 부분만 바꾸기 위해.
React는 실제 DOM과 비교하는 알고리즘을 갖고 있다.
state : 데이터. 변하니까 state(상태)라고 부름.
상태가 바뀔 때마다 렌더링을 함.
👇index.html
여기에는 아무것도 없다. div 태그 뿐.
div 태그에 화면 끼워넣기
<div id="root"></div>
👇index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "bootstrap/dist/css/bootstrap.min.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render( // 함수의 파라미터 자리에 화면을 그린다.
<> // Fragment를 의미. 멀티 태그를 사용할 수 있도록 해준다(div태그>div태그처럼 중복 발생하지 않게 해 줌)
<BrowserRouter> // Application을 Router로 감싼다.
<App />
</BrowserRouter>
</>
);
👇App.jsx
화면이 연결되는 부분
import { Route, Routes } from 'react-router-dom';
import HomePage from './components/page/HomePage';
import BoardPage from './components/page/BoardPage';
import NoticePage from './components/page/NoticePage';
import NoticeWrite from './components/notice/NoticeWrite';
function App() {
return ( // 멀티태그<>가 있기 때문에 ()로 묶음
<>
<Routes> // 유효범위 중 3번
<Route path="/" exact={true} element={<HomePage />} /> // "/" 일치할 때만 사용
<Route path="/notice" element={<NoticePage />} />
<Route path="/notice/write" element={<NoticeWrite />} />
<Route path="/board" element={<BoardPage />} /> // {}: JS코드 사용. 안에 함수(일급 객체) 사용
</Routes>
</>
);
}
export default App;
👇.env
링크, 키 등을 숨겨 배포할 수 있다.
# 일괄처리 가능
REACT_APP_PUBLIC_IP=http://localhost:3000/
REACT_APP_SPRING_IP=http://localhost:8000/
👇HomePage.jsx
import axios from 'axios'
import React, { useEffect, useState } from 'react'
import Header from '../include/Header';
import Bottom from '../include/Bottom';
import Home from '../home/Home';
const HomePage = () => {
// 상태값은 콤포넌트가 아닌 페이지가 들고 있게 하는게 좋다
const [boards, setBoards] = useState([]);
const [number, setNumber] = useState(0);
const [user, setUser] = useState({});
const [users, setUsers] = useState([]);
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/users')
.then(response => {
console.log(response);
console.log('data:'+response.data);
console.log('data:'+response.data[0]);
console.log('data:'+response.data[0].name);
console.log('data:'+response.data[0].address['city']);
setUsers(response.data);
});
},[]);
useEffect(() => {
//console.log('useEffect 호출');
// 다운로드 가정 - 여기서는 io가 발생됨. 네트워크발생
let data = [
{ b_no:1, b_title:'Ajax프로그래밍', b_writer: '이재선', b_content: '내용1' },
{ b_no:2, b_title:'JSP프로그래밍', b_writer: '김유신', b_content: '내용2' },
{ b_no:3, b_title:'Ajax프로그래밍', b_writer: '강감찬', b_content: '내용3' },
];
//setBoards(data);//기존에 있는거 무시하고 새로 넘김
// 다운로드가 안된 상태에서 빈데이터가 setBoards에 들어간다
setBoards([...data]);//기존에 있는게 있으면 뒤에 붙여서 넘김
setUser({ id: 1, username: 'good' });
},[]);// 빈 배열은 어디에도 의존하고 있지 않으므로 한번만 실행됨
const boardList = () => {
let data = [
{ b_no:1, b_title:'Ajax프로그래밍', b_writer: '이재선', b_content: '내용1' },
{ b_no:2, b_title:'JSP프로그래밍', b_writer: '김유신', b_content: '내용2' },
{ b_no:3, b_title:'Ajax프로그래밍', b_writer: '강감찬', b_content: '내용3' },
];
setBoards([...data]);
};
const increase = () => {
setNumber(number+1);
}
return (
<>
<Header />
<Home boards={boards} user={user} users={users}
setBoards={setBoards} boardList={boardList} number={number}
increase={increase}/>
<Bottom />
</>
)
}
export default HomePage
useState 훅은 함수형 컴포넌트에서 상태를 초기화하고 상태를 변경할 때 사용된다. 이들은 각각의 상태를 초기화하고, 해당 상태를 업데이트하는 함수를 반환한다. 이렇게 반환된 함수들은 해당 상태를 변경할 때마다 컴포넌트를 다시 렌더링한다.
boards는 배열을 초기값으로 갖고, 해당 배열에 대한 상태 업데이트를 수행할 때 setBoards 함수를 사용할 수 있다.
const [boards, setBoards] = useState([]);
user는 빈 객체를 초기값으로 가지며, setUser 함수를 사용하여 해당 객체 상태를 업데이트할 수 있다.
const [user, setUser] = useState({});
이 코드는 React의 useEffect 훅을 사용하여 컴포넌트가 마운트될 때 실행되는 비동기 작업을 수행한다. useEffect는 함수형 컴포넌트에서 부수 효과(비동기 작업, 데이터 가져오기 등)를 처리하기 위해 사용된다.
해당 코드에서는 axios 라이브러리를 사용하여 외부 API(https://jsonplaceholder.typicode.com/users)로 GET 요청을 보내고, 응답 데이터를 받아와 처리하고 있다.
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/users')
.then(response => {
console.log(response);
console.log('data:'+response.data);
console.log('data:'+response.data[0]);
console.log('data:'+response.data[0].name);
console.log('data:'+response.data[0].address['city']);
setUsers(response.data);
});
},[]);
useEffect 훅은 컴포넌트가 처음 렌더링될 때(빈 배열을 두 번째 인자로 전달하여) 한 번 실행된다.
axios.get을 사용하여 외부 API에서 데이터를 가져온다.
.then을 사용하여 GET 요청의 응답을 처리한다. response에는 서버로부터 받은 응답 데이터가 들어 있다.
console.log를 사용하여 응답 데이터를 콘솔에 출력하고 있다. response.data는 API로부터 받은 실제 데이터이다. 각각의 console.log는 응답 데이터를 확인하기 위해 사용되고 있다.
마지막으로 setUsers(response.data)를 사용하여 API로부터 받은 데이터를 컴포넌트의 상태인 users에 저장하고 있다.
위 코드는 주어진 URL에서 사용자 데이터를 가져와 users 상태를 업데이트하는데 각각의 console.log는 응답 데이터의 일부를 콘솔에 출력하여 확인하고 있다. 가져온 데이터는 setUsers를 사용하여 컴포넌트의 users 상태로 설정된다.
👇Header.jsx
import React from 'react'
import { Container, Nav, Navbar, NavDropdown } from 'react-bootstrap';
import { Link } from 'react-router-dom';
const Header = () => {
return (
<>
<Navbar bg="light" expand="lg">
<Container fluid>
<Navbar.Brand href="#">리액트캠프</Navbar.Brand>
<Navbar.Toggle aria-controls="navbarScroll" />
<Navbar.Collapse id="navbarScroll">
<Nav
className="me-auto my-2 my-lg-0"
style={{ maxHeight: '100px' }}
navbarScroll
>
<Link to="/" className='nav-link'>Home</Link>
{/* <Link to="/login/10/test" className='nav-link'>로그인</Link> */}
<Link to="/notice" className='nav-link'>공지사항</Link>
<Link to="/board" className='nav-link'>게시판</Link>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
</>
)
}
export default Header
👇NoticePage.jsx
import React, { useEffect, useState } from 'react'
import Header from '../include/Header';
import { Button, Table } from 'react-bootstrap';
import Bottom from '../include/Bottom';
import { useNavigate } from 'react-router';
import NoticeRow from '../notice/NoticeRow';
import { noticeListDB } from '../../service/dbLogic';
const NoticePage = () => {
console.log('first');
const navigate = useNavigate();
const [gubun, setGubun] = useState('')
const [keyword, setKeyword] = useState('')
const [notices, setNotices] = useState([{}]) // List<Map<>>의 형태 // 전체 목록을 notices가 쥐고 있음
console.log(notices); // 아직 DB를 경유하지 않았기 때문에 빈 깡통
// useEffect 안에 실행문을 넣지 않고 함수 호출로 대체한 이유
// : 최초 목록 출력할 때 한 번, 검색버튼이나 text박스에 엔터 쳤을 때 한 번 - 여러 번 필요하다
useEffect(() => { // effect Hook : 이름 앞에 useXXX 형태면 Hook이다(React 16.8 ver부터)
console.log('effect');
noticeList();
}, []); // 의존성 배열이 빈 깡통이면 최초 한 번만 실행됨
// 배열 안에 여러가지의 상태값이 올 수 있는데 이 상태가 바뀔때마다 매번 실행됨
// 함수 파라미터 앞에 async 붙인 건 비동기처리 - 스프링 플젝과 연계!(await noticeListDB() - jsonNoticeList())
const noticeList = async () => {
console.log("noticeList호출");
const gubun = document.querySelector("#gubun").value; // n_title, n_writer, n_content : select combo
const keyword = document.querySelector("#keyword").value; // 휴관, 이벤트..
console.log(`${gubun}, ${keyword}`);
const notice = { // 괄호가 있는 것 : JS 객체이다
gubun: gubun,
keyword: keyword,
};
const res = await noticeListDB(notice) // 스프링 플젝에서 요청하기 - 비동기 상황 연출 : 지연되는 것은 8000번 서버 -> 1521번 경유
// 조회결과를 요청한 다음에 초기화 한다
document.querySelector("#gubun").value = '분류선택'; // 분류선택이 default니까
document.querySelector("#keyword").value = ''; // 빈 문자열을 주면 자동으로 '검색어를 입력하세요'가 뜨니까
console.log(res);
setNotices(res.data); // 여기서 useState에 값이 채워짐
console.log(notices);
} // end of noticeList
const noticeSearch = (event) => {
console.log(`noticeSearch ==> ${event.key}`); // Enter 출력
if(event.key === 'Enter'){ // == 값만 비교, === 타입까지 비교
noticeList();
}
}
return (
<>
<Header />
<div className="container">
<div className="page-header">
<h2>
공지관리 <small>글목록</small>
</h2>
<hr />
</div>
<div className="row">
<div className="col-3">
<select id="gubun" className="form-select" aria-label="분류선택">
<option defaultValue>분류선택</option>
<option value="n_title">제목</option> {/* gubun에 있는 값을 notice.xml의 n_title과 같은지 비교해야 하기 때문에 value를 n_title로 */}
<option value="n_writer">작성자</option>
<option value="n_content">내용</option>
</select>
</div>
<div className="col-6">
<input
type="text"
id="keyword"
className="form-control"
placeholder="검색어를 입력하세요"
aria-label="검색어를 입력하세요"
aria-describedby="btn_search"
onKeyUp={noticeSearch}
/>
</div>
<div className="col-3">
<Button variant="danger" id="btn_search" onClick={noticeList}>
검색
</Button>
</div>
</div>
<div className="board-list">
<Table striped bordered hover>
<thead>
<tr>
<th>#</th>
<th>제목</th>
<th>작성자</th>
</tr>
</thead>
<tbody>
{notices &&
notices.map((notice, key) => ( // notices : Array 형태 // Notice : 값
/* props : 리액트에서는 태그 안에 속성값으로 현재 함수의 주소번지를 넘길 수 있다 */
/* key에 대응하는 값은 반드시 유일무이 해야 함. 알고리즘이 값을 비교해서 변경되면 새로 그린다 */
<NoticeRow key={key} notice={notice} /> // notice : 한 개의 row // 한 건씩 반복 // notice의 주소번지 값을 {}에 넣음
))}
</tbody>
</Table>
<div
style={{
margin: "10px",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: "100%",
}}
>
</div>
<hr />
<div className="boardlist-footer">
<Button variant="warning" onClick={noticeList}>
전체조회
</Button>
<Button
variant="success"
onClick={() => {
navigate(`/notice/write`);
}}
>
글쓰기
</Button>
</div>
</div>
</div>
<Bottom /> {/* header, cotent, bottom : 삼단 구성 */}
</>
)
}
export default NoticePage
/*
최초 실행 페이지 : HomePage.jsx 출력
상단(Header.jsx) 메뉴바에서 공지사항을 누르면 NoticePage함수 호출됨
이 때 useEffect Hook에서 noticeListDB 함수를 최초 한 번만 호출함. 의존성 배열이 빈깡통이기 때문.
*/

react-bootstrap 패키지에서 Button과 Table 컴포넌트를 가져오고 있다. 이 두 컴포넌트는 Bootstrap 스타일을 활용하여 버튼과 테이블을 생성하는 데 사용된다.
Button: Bootstrap 스타일을 적용한 버튼을 생성하는 컴포넌트이다. 예를 들어, onClick 이벤트 핸들러와 함께 사용하여 클릭 이벤트를 처리하거나, 특정 동작을 수행하는 버튼을 만들 수 있다.
Table: Bootstrap 스타일을 적용한 테이블을 생성하는 컴포넌트이다. 테이블의 행과 열을 구성하고, 데이터를 보여주거나 테이블 형태로 정보를 표시할 때 사용된다.
또한 useNavigate는 React Router의 훅 중 하나로 프로그래밍 방식으로 라우팅을 변경할 수 있게 해준다. 예를 들어, 버튼 클릭 시 특정 경로로 이동하거나 조건에 따라 라우팅을 변경할 때 사용될 수 있다.
react-router의 useNavigate 훅을 사용하면 다른 경로로 이동하는 것이 가능하므로, 버튼 클릭 등의 상황에서 페이지 이동을 처리하는 데 사용할 수 있다. 이를 활용하여 사용자가 버튼을 누를 때 다른 페이지로 이동하거나 특정 상황에서 동적으로 라우팅을 변경할 수 있다.
User
import { Button, Table } from 'react-bootstrap';
import { useNavigate } from 'react-router';
👇NoticeRow는 함수. 함수가 곧 객체이다.
import React from 'react'
import { Link } from 'react-router-dom'
// 전체 조회결과 13건에서 한 건씩 처리할 화면을 그려줄 함수를 따로 선언하였다.
// 함수를 태그이름으로 사용할 수 있다 - props를 통해 현재 페이지의 주소번지를 하위 페이지에 넘길 수 있다.
const NoticeRow = ({notice}) => { // NoticeRow의 {props}
console.log(notice);
console.log(notice.N_TITLE);
return (
<>
<tr>
<td>{notice.N_NO}</td>
<td>
<Link to={"/noticedetail/"+notice.N_NO} className='btn btn-primary'>{notice.N_TITLE}</Link>
</td>
<td>{notice.N_WRITER}</td>
</tr>
</>
)
}
export default NoticeRow
👇NoticeWriter.jsx
import React, { useCallback, useState } from 'react'
import { BButton, ContainerDiv, FormDiv, HeaderDiv } from '../style/FormStyle'
import { noticeInsertDB } from '../../service/dbLogic';
const NoticeWrite = () => {
// input type의 속성을 굳이 state Hook으로 하는 건 값이 변경될 때 마다 동기화 처리하기 위함
// 또한 리액트는 상태값이 바뀔 때마다 함수가 자꾸 호출됨
// 아래 선언한 함수들이 반복적으로 생성됨 - 비효율적 -> 따라서 useCallback() 사용함(메모리에 저장했다가 사용)
const[title, setTitle] = useState('');
const[writer, setWriter] = useState('');
const[content, setContent] = useState('');
const handleTitle = useCallback((value) => {
setTitle(value);
},[]);
const handleWriter = useCallback((value) => {
setWriter(value);
},[]);
const handleContent = useCallback((value) => {
console.log(value);
setContent(value);
},[]);
const noticeInsert = async() => {
console.log('noticeInsert');
const notice = {
n_title : title,
n_writer : writer,
n_content : content
}
console.log(notice);
const res = await noticeInsertDB(notice);
console.log(res.data); // 1만 출력됨
//window.location.replace(`/notice`);
}
return (
<>
<ContainerDiv>
<HeaderDiv>
<h3 style={{marginLeft:"10px"}}>공지사항 글작성</h3>
</HeaderDiv>
<FormDiv>
<div style={{width:"100%", maxWidth:"2000px"}}>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
<h3>제목</h3>
<BButton onClick={()=>{noticeInsert()}}>글쓰기</BButton>
</div>
<input id="dataset-title" type="text" maxLength="50" placeholder="제목을 입력하세요."
style={{width:"100%",height:'40px' , border:'1px solid lightGray', marginBottom:'5px'}} onChange={(e)=>{handleTitle(e.target.value)}}/>
<input id="dataset-writer" type="text" maxLength="50" placeholder="작성자를 입력하세요."
style={{width:"30%",height:'40px' , border:'1px solid lightGray', marginBottom:'5px'}} onChange={(e)=>{handleWriter(e.target.value)}}/>
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}></div>
<h3>상세내용</h3>
<hr style={{margin:'10px 0px 10px 0px'}}/>
<textarea placeholder="내용을 입력하세요." className="form-control" name='content' onChange={(e)=>{handleContent(e.target.value)}} rows="15"></textarea>
</div>
</FormDiv>
</ContainerDiv>
</>
)
}
export default NoticeWrite
👇dbLogic.js
import axios from "axios";
// <Spring 프로젝트와 연계하기>
// 공지사항 조회하기 : select
// 자바스크립트에서 좌중괄호와 우중괄호가 있으면 객체이다.
// const notice = { gubun:'n_title or n_writer or n_content', keyword:'휴관' }
// ES6(2015) : 가장 큰 변화가 있었다
// 1) module
// 2) const(상수), let(가변적) : 선언할 때 사용함
// JS는 컴파일 하지 않음. 런타임 때 결정됨
// JS는 동기가 default - 비동기 처리가 필요할 때의 해결방법
export const noticeListDB = (notice) => {
return new Promise((resolve, reject) => { // 성공 : resolve로 값 받아냄, 실패 : reject로 에러처리
try {
console.log(notice); // 브라우저 개발자도구에서 확인 - 사용자가 입력한 n_title, n_content, n_writer, keyword
//axios - 비동기 요청 처리 ajax - fetch(브라우저) - axios(NodeJS- oracle서버연동)
const response = axios({ // 비동기처리(스프링 플젝 - 8000번 서버 연동)를 위해
//3000번 서버에서 8000서버로 요청을 함 - 네트워크(다른서버 - CORS이슈)
method: "get",
url: process.env.REACT_APP_SPRING_IP + "notice/jsonNoticeList",
params: notice, //쿼리스트링은 header에 담김 - get방식
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const noticeDetailDB = (notice) => {
return new Promise((resolve, reject) => {
try {
console.log(notice);
//axios - 비동기 요청 처리 ajax - fetch(브라우저) - axios(NodeJS- oracle서버연동)
const response = axios({
//3000번 서버에서 8000서버로 요청을 함 - 네트워크(다른서버 - CORS이슈)
method: "get",
url: process.env.REACT_APP_SPRING_IP + "notice/detail",
params: notice, //쿼리스트링은 header에 담김 - get방식
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const noticeInsertDB = (notice) => {
return new Promise((resolve, reject) => {
try {
console.log(notice);
const response = axios({
method: "post", //@RequestBody
url: process.env.REACT_APP_SPRING_IP + "notice/noticeInsert2",
data: notice, //post방식으로 전송시 반드시 data속성으로 파라미터 줄것
});
console.log(response);
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const noticeUpdateDB = (notice) => {
console.log(notice);
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "post", //@RequestBody
url: process.env.REACT_APP_SPRING_IP + "notice/noticeUpdate",
data: notice, //post방식으로 전송시 반드시 data속성으로 파라미터 줄것
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
export const noticeDeleteDB = (board) => {
return new Promise((resolve, reject) => {
try {
const response = axios({
method: "get",
url: process.env.REACT_APP_SPRING_IP + "reple/qnaDelete",
params: board,
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
아래 코드는 noticeListDB라는 함수를 내보내며, 주어진 notice 데이터를 사용하여 알림 목록을 가져오는 비동기 요청을 처리한다.
export const noticeListDB = (notice) => {
return new Promise((resolve, reject) => { // 성공 : resolve로 값 받아냄, 실패 : reject로 에러처리
try {
console.log(notice); // 브라우저 개발자도구에서 확인 - 사용자가 입력한 n_title, n_content, n_writer, keyword
//axios - 비동기 요청 처리 ajax - fetch(브라우저) - axios(NodeJS- oracle서버연동)
const response = axios({ // 비동기처리(스프링 플젝 - 8000번 서버 연동)를 위해
//3000번 서버에서 8000서버로 요청을 함 - 네트워크(다른서버 - CORS이슈)
method: "get",
url: process.env.REACT_APP_SPRING_IP + "notice/jsonNoticeList",
params: notice, //쿼리스트링은 header에 담김 - get방식
});
resolve(response);
} catch (error) {
reject(error);
}
});
};
👇RestNoticeController.java에 noticeInsert2 추가
package com.example.demo.controller;
import java.util.Map;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.logic.NoticeLogic;
import com.google.gson.Gson;
import jakarta.servlet.http.HttpServletRequest;
@RestController // 화면없이 조회 결과를 문자열 포맷으로 처리할 때 사용 - @ResponseBody 대체로 제공됨
@RequestMapping("/notice/*")
public class RestNoticeController {
Logger logger = LoggerFactory.getLogger(RestNoticeController.class);
@Autowired
NoticeLogic noticeLogic = null;
// get방식으로 사용자가 입력한 값을 받을 땐 : @RequestParam 사용
// post방식을 받을 땐 : @RequestBody 사용 (리액트)
@GetMapping("jsonNoticeList")
public String jsonNoticeList(@RequestParam Map<String, Object> pMap, HttpServletRequest req){
// Test url : http://localhost:8000/notice/jsonNoticeList?gubun=n_title&keyword=휴관
logger.info(pMap.toString()); // n_title ro n_writer or n_content, keyword=휴관
// logger.info(pMap.get("gubun").toString());
// logger.info(req.getParameter("gubun").toString());
// logger.info(pMap.get("keyword").toString());
List<Map<String, Object>> list = null;
list = noticeLogic.noticeList(pMap);
Gson g = new Gson();
String temp = g.toJson(list); // toJson() : 파라미터로 받은 List<Map<>>형태를 JSON형식으로 전환해 줌
return temp;
}
// React는 JSON으로 문자열을 주고 받기 때문에 @RestController에서 @ResponseBody를 사용함
@PostMapping("noticeInsert2")
public String noticeInsert2(@RequestBody Map<String, Object> pMap){
logger.info("noticeInsert2");
logger.info(pMap.toString());
int result = 0;
result = noticeLogic.noticeInsert(pMap);
return String.valueOf(result); // 성공 : 1, 실패 : 0 반환
}
}
https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Modules
https://fromnowwon.tistory.com/entry/useEffect-%EC%9D%98%EC%A1%B4%EC%84%B1%EB%B0%B0%EC%97%B4
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Map