요소의(Element) children 속성
읽기 전용인(read-only) children 속성은 children을 호출한 상위 요소(the element upon)의
모든 자식(child) 요소들을 포함하는 동적인(a live) HTMLCollection을 반환한다.
→ 호출된 요소의 자식 요소들을 동적으로 반환하며,
"동적으로" 반환하기에 자식 요소가 변경되면 반환된 컬렉션도 실시간으로 업데이트 된다.
Element.children 은 오직 요소 노드만 포함한다 → 요소 노드만을 포함한다는 것은 HTML 요소 노드만을 포함한다는 것
즉 Element.children은 특정 DOM 요소의 자식들 중에서 "요소 노드"만 포함하는 동적 HTMLCollection을 반환한다.
텍스트 노드, 주석 노드 등은 포함되지 않는다.
텍스트 및 주석 노드와 같이 요소가 아닌 노드를 포함한 모든 자식 노드를 가져오기 위해서는
Node.childNodes 를 사용한다.
Node.childNodes는 특정 노드의 자식들 중에서 모든 노드를 포함하는 NodeList를 반환한다.
요소 노드(열린태그와 닫힌태그가 한 쌍을 이루면 하나의 요소가 된다), 텍스트 노드(HTML 태그 없이 작성된 그냥 텍스트를 말하는 듯?), 주석노드( // 대각석 기호 두 개를 입력한 후 작성한 것은 프로그램이 읽어들이지 않는다, 코드에 대한 부연 설명을 위해 작성하는 것 ) 등 모든 유형의 자식 노드가 포함된다. Node.childNodes 는 기본적으로 동적으로 노드를 반환하지 않는다(static)
HTMLCollection
"동적인" HTMLCollection은 특정 DOM 노드의 자식 요소들을 순서가 정해진 집합으로 구성된다.
Element.children 으로 반환된 동적인 HTMLCollection을 대상으로 collection 관련 메서드인 item() 을 사용하여
HTMLCollection에 포함된 각각의 노드들에 접근하거나 자바스크립트 배열 방식(Javascript array-style notation, 자바스크립트 배열 표기법[대괄호 표기법])을 사용하여 각각의 노드들에 접근할 수 있다. 자바스크립트 배열 방식이란 array[index], ex. array[0] 과 같은 자바스크립트 배열 표기법을 의미하며, 이 표기법을 사용하면 item() 대신 배열처럼 인덱스를 통해 자식 노드에 접근할 수 있다.
Element.children 으로 반환된 HTMLCollection에는 해당 요소(Element)의 모든 자식 요소들이 순서 있게 포함되어 있으며
컬렉션 항목의 갯수를 반환하는, 즉 HTMLCollection에 포함된 요소의 갯수를 반환하는 length가 0을 반환하면 빈 목록이라는 것이다. HTMLCollection.length
EL과 JSTL
EL은 Expression Language 약어로 표현 언어, 스크립틀릿 <%= %> 이나 JSP 내장 객체인 out을 사용하여 out.println() 과 같은 자바코드를 사용하지 않고, 객체에 좀 더 쉽게 접근하여 데이터를 간편하게 출력하기 위한 도구다. attribute 형식에서는 <%= count+1 %> 를 사용하지 않고, ${count+1} 로 작성, parameter 형식에서는 ${bParam.abc} 로 작성한다.
JSTL은 태그를 사용하여 연산, 조건문, 반복문인 if문/for문, DB 관련 데이터를 좀 더 간편하게 처리할 수 있게 도와주는 도구다.
function appendPage() {
// 현재 서버에서 넘겨준 페이지 번호
const currentPage = '${maker.page.pageNo}'
// DOM 요소중 클래스가 .pagination 인 요소를 찾아 $ul 변수에 담는다.
const $ul = document.querySelector('.pagination');
// $ul에 담긴 요소의 모든 자식 요소들을 얻는다.
// $ul.children은 HTMLCollection을 반환한다.
// HTMLCollection은 유사배열이라 배열 메서드를 사용할 수 있도록
// 스프레드 문법을 사용하여 배열로 변경한다.
const $liList = [...$ul.children]
// 클래스를 추가하는 가장 쉬운 방법은 자바스크립트 언어를 사용하는 것
// 고차함수 forEach()를 사용하면 요소를 하나씩 받고,
// 현재 페이지 번호와 일치하는 li를 찾은 후 active 클래스 이름을 붙인다.
$liList.forEach( $li => {
if(currentPage === $li.dataset.pageNum) {
// <li data-page-num=${i} class="page-item">
// li 요소를 하나씩 얻어서 data-page-num의 값이 현재 페이지와 일치한다면
// 해당 li 요소에 .active라는 class명을 추가할 것
$li.classList.add('active');
}
});
// 검색한 목록 반환하면서 검색한 영역에 검색한 키워드 남기기 → 자바스크립트로 작업
// 검색조건 셀렉트박스 옵션 타입 고정하기
function fixSearchOption() {
const $select = document.getElementById('search-type');
const $options = [...$select.child];
$options.forEach( $opt => {
if($option.value === '${s.type}') {
$opt.setAttribute('selected', 'selected');
}
});
}
// 선언한 함수를 호출하기
appendPage();
fixSearchOption();
}
에러페이지 설정하기
1. ErrorPageRegistrar 인터페이스를 구현한 클래스에서 추상 메서드인
registerErrorPages(ErrorPageRegistry register) 메서드 구현부를 작성한다.
2. error가 발생했을 때, 발생한 에러 상태 값(HttpStatus status)과 그 에러가 발생했을 때 연결할 페이지 값을(path)
전달하여 errorPage 관련 객체를 생성하고,
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error/error404");
***path 값에 등록하는 주소?를 받을 컨트롤러는 작성해야 한다.
3. ErrorPageRegistry타입 객체에 등록한다.
registry.addErrorPages(errorPage404);
// error가 발생했을 때 그 요청을 만들어서 서버로 전송할 것임
// error를 처리하는 컨트롤러
@Controller
@RequestMapping("/error")
public class ErrorController {
@GetMapping("/404")
public String error404() {
// error404.jsp 만들어야 함
return "error/error404";
}
@GetMapping("/500")
public String error500() {
return "error/error500";
}
}
// CustomErrorPageConfig --------------------
// error가 발생하면 url 링크를 만들어서 처리하기
// Configuration 설정용으로 만들 빈등록
// 스프링부트에서 에러페이지를 설정하고 싶으면
// ErrorPageRegistrar 인터페이스를 구현한 클래스를 작성할 것
public class CustomErrorPageConfig implements ErrorPageRegistrar {
// 추상메서드 오버라이딩하기
@Override
public void registerErrorPages(ErrorPageRegistry registry) {
// ErrorPage() 생성자에 매개 값 전달하기: HttpStatus status, String path
// 첫 번째 매개변수로 전달해야할 값은 http 상태코드,
// http 상태코드]는 웹 서버에서 클라이언트(웹 브라우저)에게 응답할 때 사용되는 세자리 수
// 이 코드는 요청에 대한 결과를 나타낸다.
// 200번대는 요청이 성공적으로 수신, 이해, 수락되었음을 나타냄
// 400번대는 클라이언트의 요청에 오류가 있음을 나타낸다.
// 500번대는 서버가 유효한 요청을 수행하지 못함을 나타낸다.
// 404 Not Found: 요청한 리소스를 찾을 수 없다.
// 400 Bad Request: 클라이언트의 요청이 잘못된 형식으로 전달된 경우에 발생한다.
// → 예를 들어 get으로 요청이 들어와야 하는데, post 방식으로 요청이 들어올 경우 발생
// 401 Unauthorized: 인증이 필요한 리소스에 인증 없이 요청이 들어온 경우
// → 회원만 작성할 수 있는 게시판에 비회원이 들어온 경우 발생한다.
// 500 번대는 서버 오류]
// 500 Internal Server Error: 서버가 클라이언트의 요청을 처리하는데 실패한 경우에 발생
// 503 Service Unavailable: 서버가 일시적으로 요청을 처리할 수 없는 상태인 경우
// → 대개 서버의 과부하 또는 유지 보수 작업으로 인해 발생하는 에러
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error/error404");
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/error500");
registry.addErrorPages(errorPage404, errorPage500);
}
}
검색 기능 추가
검색기준에 맞는 게시글들을 불러올 때 또한 페이지네이션이 적용되어야 한다는 점,
그래서 검색 관련 클래스 작성시 Page 클래스를 상속하도록 설계함
MyBatis에서 동적 SQL을 사용하도록 설계함 → BoardMapper.xml 파일에 동적 SQL문을 작성함.
BoardMapper.xml 에 동적 SQL 작성하기(if태그 작성하기)
```
SELECT * FROM tbl_board
<!-- 동적 SQL 작성하기 -->
<if test="type == 'title'">
WHERE title LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="type == 'content'">
WHERE content LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="type == 'writer'">
WHERE writer LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="type == 'tc'">
WHERE title LIKE CONCAT('%', #{keyword}, '%')
OR content LIKE CONCAT('%', #{keyword}, '%')
</if>
ORDER BY board_no DESC
LIMIT #{pageStart}, #{amount}
```
@Setter @Getter @ToString @EqualsAndHashCode
public class Search extends Page {
// 검색 조건, 검색어
private String type, keyword;
// Search 라는 객체 생성시 기본 값 설정
public Search() {
this.type = "";
this.keyword = "";
}
}
// BoardController
@GetMapping("/list")
public String list(Model model, @ModelAttribute("s") Search search) {
System.out.println("search = "+search);
List<BoardListResponseDTO> dtoList = service.getList(search);
// 사용자가 요청한 페이지 정보, 총 게시물 개수를 전달
// 페이징 알고리즘 자동 호출
PageMaker pageMaker = new PageMaker(page, service.getCount(search));
model.addAttribute("bList", dtoList);
model.addAttribute("maker", pageMaker);
// 메서드의 파라미터 값을 model 객체에 바로 추가하고 싶다면,
// @ModelAttribute 아노테이션을 사용할 것
// model.addAttribute("s", search);
return "chap05/list";
}
// BoardService
public getList(Search search) {
List<BoardListResponseDTO> dtoList = new ArrayList<>();
List<Board> boardList = mapper.findAll(search);
for(Board board: boardList) {
BoardListResponseDTO dto = new BoardListResponseDTO(board);
dtoList.add(dto);
}
return dtoList;
}
public int getCount(Search search) {
return mapper.getCount(search);
}
// BoardMapper
public interface BoardMapper {
List<Board> findAll(Search search);
int getCount(Search search);
}
// BoardMapper.xml
<sql id="search">
<if test="type == 'title'">
WHERE title LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="type == 'content'">
WHERE content LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="type == 'writer'">
WHERE writer LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="type == 'tc'">
WHERE title LIKE CONCAT('%', #{keyword}, '%')
OR content LIKE CONCAT('%', #{keyword}, '%')
</if>
</sql>
<select id="findAll" resultType="board">
SELECT * FROM tbl_board
<!-- 동적 SQL 작성하기
<if test="type == 'title'">
WHERE title LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="type == 'content'">
WHERE content LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="type == 'writer'">
WHERE writer LIKE CONCAT('%', #{keyword}, '%')
</if>
<if test="type == 'tc'">
WHERE title LIKE CONCAT('%', #{keyword}, '%')
OR content LIKE CONCAT('%', #{keyword}, '%')
</if>
-->
<include refid="search" />
ORDER BY board_no DESC
LIMIT #{pageStart}, #{amount}
</select>
<select id="getCount" resultType="int">
SELECT COUNT(*)
FROM tbl_board
<!-- 동적 SQL 작성하기 -->
<include refid="search" />
</select>
MyBatis는 데이터베이스 관리 시스템에 저장되어 있는 특정 테이블과 매핑할 경우
해당 테이블 컬럼 및 형식에 기반하여 자바 어플리케이션에서 엔터티를 작성한다.
→ DB 시스템에서 테이블을 먼저 만들고, 해당 테이블의 정보에 기반하여 엔터티를 작성
// Reply Entity
/*
CREATE TABLE tbl_reply
(
reply_no INT AUTO_INCREMENT,
reply_text VARCHAR(1000) NOT NULL,
reply_writer VARCHAR(100) NOT NULL,
reply_date DATETIME DEFAULT CURRENT_TIMESTAMP,
board_no INT,
CONSTRAINT pk_reply PRIMARY KEY(reply_no),
CONSTRAINT fk_reply FOREIGN KEY(board_no) REFERENCES tbl_board(board_no)
ON DELETE CASCADE
);
// 컬럼에 account(계정) 추가하기
ALTER TABLE tbl_reply
ADD account VARCHAR(100);
*/
@Setter @Getter @ToString
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@Builder
public class Reply {
private int replyNo;
private String replyText;
private String replyWriter;
private LocalDateTime replyDate;
private int boardNo;
private String account;
}
// ReplyMapper 인터페이스
// @Mapper 아노테이션을 작성해야 ReplyMapper.xml 파일이
// 이 인터페이스를 구현한 객체로 마이바티스가 변환해준다.
@Mapper
public interface ReplyMapper {
// 댓글 등록
// 컨트롤러에서 dto로 받은다음에 dto를 entity로 변환해서 mapper에게 전달할 예정
void save(Reply reply);
// 댓글 수정(내용만 수정)
// replyNo, replyText만 필요하지만 그냥 엔터티 객체를 보낼 것?
// 엔터티 객체에서 필요한 값을 꺼내 쓸 것
void modify(Reply reply);
// 댓글 삭제
void delete(int replyNo);
// 댓글 개별 조회
Reply findOne(int replyNo);
// 댓글 전체 목록 조회
// "특정 게시글"에 달린 모든 댓글을 조회하기 → 그래서 게시글 번호가 필요함
List<Reply> findAll(int boardNo);
// 댓글 총 개수 조회
int count(int boardNo);
}
// ReplyMapper.xml → resources 폴더에 작성한다.
<!-- 이 문서는 mapper다 라는 것을 선언하는 것 -->
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
그리고 이 mapper는 어떤 인터페이스를 구현하는지?
어떤 파일과 관련되어 있는지
mapper 태그를 통해 알려줄 것 namespace에 해당 인터페이스 파일의 풀 경로를 적을 것
지금 이 xml 파일이 구현하고자 하는 인터페이스의 풀 경로를
mapper 태그의 속성인 namespace의 값에 작성할 것
-->
<mapper namespace="com.spring.mvc.chap05.mapper.ReplyMapper">
<!-- 풀 경로라함은 src/main/java 폴더 하기부터 적을 것 -->
<!-- 그리고 interface에 적히 추상메서드를 이 파일에 구현할 것 -->
<!-- Reply 객체가 전달되어, Reply 객체가 지니는 속성의 값을 사용할 수 있음 -->
<insert id="save">
INSERT INTO tbl_reply
(reply_text, reply_writer, board_no, account)
VALUES (#{replyText}, #{replyWriter}, #{boardNo}, #{account})
</insert>
<!-- Reply 객체가 전달됨 -->
<update id="modify">
UPDATE tbl_replay
SET reply_text = #{replyText}
WHERE reply_no = #{replyNo}
</update>
<delete id="delete">
DELETE FROM tbl_reply
WHERE reply_no = #{replyNo}
</delete>
<!-- select 태그들은 반드시 resultType을 작성해주어야 한다. -->
<select id="findOne" reslutType="reply">
SELECT * FROM tbl_reply
WHERE reply_no = #{replyNo}
</select>
<select id="findAll" resultType="reply">
SELECT * FROM tbl_reply
WHERE board_no = #{boardNo}
ORDER BY reply_no
</select>
<select id="count" resultType="int">
SELECT COUNT(*)
FROM tbl_reply
WHERE board_no = #{boardNo}
</select>
</mapper>
// MyBatis config(설정 파일) 만들기 → resources 폴더 안에 작성할 것
// mybatis-config.xml
<!-- MyBatis 설정 파일이라는 것을 선언 -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 카멜케이스와 스네이크케이스를 자동변환해주는 설정 -->
<setting name="mapUnderToCamelCase" value="true" />
<!-- 조회된 결과가 없을 시에는 null을 반환하는 설정 -->
<setting name="jdbcTypeForNull" value="NULL" />
</settings>
<!--
resultType 값에 관련 엔터티 풀 경로를
적는 번거로움 방지하기 위해
엔터티 클래스 경로에 별칭 붙이기
-->
<typeAliases>
<typeAlias type="com.spring.mvc.chap05.entity.ReplyMapper" alias="reply">
</typeAliases>
<!-- SQL을 적어놓은 xml 파일의 위치를 설정 -->
<mappers>
<mapper resource="mapper/ReplyMapper.xml" />
</mappers>
</typeAlias>
</configuration>
//
@Builder 아노테이션
Builder라는 디자인 패턴을 롬복이 자동으로 만들어준다.
생성자가 NoArgsConstructor(기본 생성자)와 AllArgsConstructor(모든 필드의 값을 설정하는 생성자)
만 있을 때, 원하는 필드의 값만 세팅하여 생성자를 만들고 싶다면 @Builder 아노테이션을 사용하여
원하는 각 필드의 값을 세팅하여 새로운 객체를 생성할 수 있다.
클래스명Builder라는 내부 클래스가 생김.
클래스명.Builder() 를 호출한 후에 원하는 필드명을 설정한다. 그리고 원하는 필드들을 설정한 후에 build() 메서드를 호출하면
새로운 객체가 생성되고, 생성된 객체를 해당 클래스명 타입 참조변수에 대입하면 해당 참조변수는 생성된 객체를 참조하게 되는 것이다.
@Builder 아노테이션은 각 필드별로 값을 설정하는 메서드를 만들어준다.
만약 @Builder 아노테이션을 설정하지 않았다면,
각 필드별로 값을 설정하는 생성자를 작성하지 않았다면,
기본 생성자로 객체를 생성해서 생성된 객체의 필드를 각각 setter를 통해 값을 설정해주어야 한다.
예를 들면,
```
Board b = new Board();
b.setTitle("재밌는 글");
b.setContent("재밌는 내용");
b.setWriter("재밌는 작성자");
mapper.save(b);
```
예를 들어,
// Board 클래스에 @Builder 아노테이션과 @Setter 아노테이션을 작성해야 함
/*
Board.builder() // builder() 메서드를 호출하고,
.필드명("해당 필드명에 설정할 값")
.필드명(필드의 타입에 따라 값이 달라진다.)
.build(); // → 반환 값은 Board타입 객체다.
*/
@Autowired // @Autowired 아노테이션으로 boardMapper참조변수에 BoardMapper 객체를 주입
BoardMapper boardMapper;
int i = 1;
for(int i = 1; i<=100; i++) {
Board b = Board.build()
.title("재밌는 글 "+i)
.content("재밌는 글 내용 "+i)
.writer("재밌는 사람 "+i)
.build();
boardMapper.save(b);
}
동기와 비동기 코드
동기 코드란(Synchronous Code), 작업이 순서대로[코드가 적힌 순서대로] 진행된다.
한 작업이 완료될 때까지 다음 작업은 대기 상태가 된다.
비동기 코드란(Asynchronous Code), 특정 작업이 완료되기를 기다리지 않고, 다음 작업으로 넘어간다.
콜백, 프로미스(Promise), asyn/await 등의 패턴을 사용하여 비동기 코드를 관리할 수 있다.
setTimeout() 함수는 비동기 함수로써 주어진 시간이 경과한 후에 콜백 함수를 실행한다.
비동기 방식의 코드를 편하게 사용할 수 있도록 프로미스 객체를 사용한다 → 프로미스 객체는 자바스크립트에서 제공하고 있다.
- 프로미스(Promise)는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체다.
- async/await 키워드는 비동기 코드를 마치 동기 코드처럼 작동할 수 있게 해주는 ES7의 기능이다.
비동기 코드는 코드의 흐름을 효율적으로 관리하기 위해 필요하며,
특히 네트워크 요청, 파일 시스템 작업, 타이머 등의 작업에서 중요한 역할을 한다.
네트워크 요청을 비동기 방식으로 해볼 예정
대부분의 코드는 바뀌지 않고, 특정 부분의 코드만? 변화될 예정,
예를 들어 댓글 목록 부분을 비동기로 요청하면 페이지 전체가 바뀌는 것이 아니라[페이지를 요청하는 것이 아니라]
댓글 목록 부분만 변화된다. 전체 페이지를 요청하는 것보다 댓글 목록 부분만 불러올 때 사용자의 이용에 있어서
빠른 반응성 및 속도 개선 등 장점이 있다.
XMLHttpRequest(XHR) 객체는 서버와 상호작용하는 것에 사용된다.
전체 페이지를 새로고침 하지 않고, url로부터 데이터를 검색할 수 있다.
XMLHttpRequest 객체는 사용자가 웹 페이지를 이용하는 것을 방해하지 않고,
웹 페이지의 일부만을 업데이트 할 수 있다.
통신에 있어서 서버로부터 이벤트 데이터 또는 메시지 데이터를 받는 것이 필요하다면(to involve receiving)
→ to involve receiving 은 받는 행위가 필수적이라는 것을 의미한다.
EventSource 인터페이스를 통해서 server-sent events 를 사용하는 것을 고려해라
XMLHttpRequest 객체 사용하기
1. 서버와 통신하기 위해[서버로부터 데이터를 갖고 오기 위해] XMLHttpRequest 객체를 생성한다.
const request = new XMLHttpRequest();
2. onreadyChange 에 함수 대입하기?
onreadyChange는 XMLHttpRequest 객체 속성인 readyState 가 변할 때마다 호출된다[발생한다]
onreadyChange는 event로 분류되는 것 같다.
XMLHttpRequest 객체의 참조변수.onreadyChange = function() { ... }
```
request.onreadyChange = function() {
// 클라이언트에서 서버로 요청할 준비가 되었는지를 확인하는 XMLHttpRequest.readyState
// XMLHttpRequest.status 는 요청했을 때 서버로부터 요청에 기반한 응답이 왔는지를 체크하는 것
if(request.readyState === XMLHttpRequest.DONE && request.status === 200) {
// 클라이언트가 서버에 요청할 준비가 되었고, 요청했을 때 서버로부터 응답받기를 성공했다면
// 응답받은 값을 텍스트로 콘솔에 출력한다.
console.log('request.responseText: ', request.responseText);
}
}
```
3. 클라이언트가 서버로 요청 보내는 것을 준비
XMLHttpRequest타입 객체.open("서버에 어떤 방식으로 데이터를 요청할 건지, ex. GET, POST 등", "요청할 서버 url", 비동기로 요청시 true, 동기로 요청시 false);
4. 클라이언트가 서버로 요청보내기
XMLHttpRequest타입 객체.send();