본문 바로가기

Spring/스프링 프로젝트

스프링에서 Ajax를 통해 JSON 데이터 주고받기

ajax를 이용한 스프링 실시간검색 구현

처음에는 Get요청을 통해 DB에서 값을 가져와 jsp에서 EL로 꺼내오는 방식을 사용했다

그러나 검색 기능을 추가하면서 키워드를 입력할때마다 거기에 맞는 데이터만 가져오게 하고 싶었다.

뷰단(클라이언트)과 자바(서버)가 새로고침없이 어떻게 상호작용할 수 있을까?

검색하다보니 ajax와 자바스크립트를 통해 가능하다는 것을 알게 된 후 구현을 시도하게 되었다.

 

 

 

배경지식

  • 클라이언트의 자바스크립트 객체(데이터)를 서버단에서 사용하기 위해 (혹은 반대) 데이터를 변환하여 전송하는 방식은 3가지
    • csv(컴마로 구분된 문자열), xml(extensible markup language), JSON(javascript object notation)
    • 요즘에는 경량화된 json을 쓰는 추세
    • 자바-제이슨 파싱이 귀찮으나, 스프링이 많은 것들을 대신하고 있기 때문에 제이슨을 이용해 손쉽게 구현 가능
  • ajax는 통신을 가능하게 하는 기술이라면, json은 클라이언트-서버 간의 통신을 위해 데이터를 저장하는 방법.
  • ajax : Asynchronous JavaScript and XML, 비동기적 방식 사용하여 페이지를 리로드하지 않고 서버에 요청한 값을 받아 부분적으로 갱신 가능하게 함

 


 

개별 연습해본 것

  • js(제이쿼리)를 통해 인풋태그(검색창)의 키워드를 가져오기
    • 자바스크립트 문법으로도 구현 가능하나, ajax가 제이쿼리 기반이기 때문에 함께 사용할 것이 권장된다고 한다.
  • 자바스크립트 객체 - 제이슨 데이터타입 변환
    • stringify(), JSON.parse()를 통한 타입변환
    • 하지만 스프링의 도움을 받아 stringify()만을 사용해 구현 가능
    • console.log를 통해 결과 확인
  • ajax로 js에서 컨트롤러로 데이터 넘기기 (제이슨 문자열이 넘어온다)
  • 컨트롤러에서 데이터 변환 (제이슨 문자열을 다시 객체로 변환)

 

 

 


구현 순서

 

0. mapper.xml (DB에서 검색 결과를 가져오는 쿼리문 작성)

DB 쿼리문을 가장 마지막에 작성해야 하는거 아닌가? 싶을 수도 있지만, 어떤 데이터를 어떤 형태로 넘겨주어 어떤 데이터를 가져오게 할건지를 파악하는 게 가장 우선이라고 생각해 쿼리문을 먼저 작성했다.

 

고려해야할 것은 ①파라미터, ②마이바티스로 동적 SQL을 만드는 방법 ③리턴타입이다.

 

- 각각의 컬럼별로 검색 키워드를 넣을 수 있고 모든 키워드를 포함하는 결과만을 출력하고 싶었다.  최종 쿼리문이 다음과 같이 예상되었다.

더보기
SELECT 컬럼1, [컬럼2, 컬럼3, ....] FROM 테이블WHERE 컬럼1 like '%키워드1%' [and 컬럼2 like '%키워드2%' ...]ORDER BY ...[페이징처리]

① 파라미터 : 컬럼과 키워드가 하나의 쌍으로, 여러 개의 세트를 파라미터로 넘겨주어야 한다.  

  • HashMap을 사용하는 것이 좋을 것 같다. 
  • 하나의 객체가 두 개의 key-value값을 갖도록 한다.
    • key1 : field(컬럼명) -  value1 : 'emp_name'
    • key2 : keyword(검색어) - value2 : '김'
    • 검색란이 8개이므로, 총 8개의 해쉬맵을 리스트로 가져와야 할 것이다.

② 동적 SQL : 파라미터가 해쉬맵의 리스트이기 때문에, 각각의 리스트에서 해쉬맵을 하나씩 꺼내 쿼리에 추가해야한다

  • foreach : collection과 item을 지정해준다.
    collection : 파라미터가 리스트이고,  리스트의 객체는 클래스가 아닌 해쉬맵이기 때문에 데이터 타입(List)가 들어간다.
                          List가 클래스를 가지는 경우에는 collection에 변수의 이름을 쓴다고 하는데, 이 부분 확실하지 않음
  • where태그와 if태그의 활용
    검색어를 입력하다가 지울수도 있고, 8개란 중 keyword가 null값을 가지는 경우가 허다하기 때문에 태그 안의 값이 존재할 때만 쿼리문을 추가시키는 where태그와 if태그를 활용

 리턴타입 : 검색 결과를 만족하는 직원정보의 리스트(다수의 record)가 필요하다. 
     각각의 컬럼명을 멤버로 가지는 memberList 클래스를 이미 구현한 상황이라 그대로 사용하기로 한다. List<MemberList>

그 외 controller-service-dao-mapper.xml로 이어지는 과정은 생략하겠다.

 

 

 

1. js(제이쿼리)를 통해 인풋태그(검색창)의 키워드를 가져오기

1) id값 부여 :

제이쿼리로 view단의 요소들을 제어하기 위해, 검색 input태그에 id값을 준다.

2) onkeyup 이벤트 :

검색어를 입력할때마다 자동으로 데이터를 실시간으로 받아오기 위해 onkeyup이벤트를 부여한다.

3) getMap() 메소드

  getMap 메소드 안에 제이쿼리로 인풋값 가져오기, 제이슨 객체로 변환, ajax통신까지 모두 들어가지만 일단은 상단부만 가져왔다.

  • $('.클래스명').each(function(){});
    검색란이 여러개이지만, 한번에 모든 검색어를 가져와 and절로 검색해야 하기 때문에

    클래스 이름을 부여해서 그 이름에 해당하는 요소를 모두 가져온다.
  • 가져온 요소들의 구조는 콘솔에 찍어가며 확인이 가능하다. (console.log 활용)
  • 인풋태그가 가지는 수많은 속성 중에 우리에게 필요한 값은 id(어떤 컬럼인지)와 value(어떤 검색어인지)이다.
  • each(function(){});
    선언한 뒤 요소 하나씩 id값과 value값만 추출해 두 값을 속성으로 가지는 객체를 만들고,
    만든 객체를 해당 메소드 밖에서 선언한 search 배열에 추가시킨다.
  •  클래스 네임으로 가져온 요소 리스트 중 하나의 아이템 (each)에서 추출하되, 반복문으로 돌림
    •   id 추출 : $(this).attr('id') - id는 요소의 속성명으로 사전에 정의된 이름, 개발자가 정의X
    •  value 추출 : $(this).val() - val()는 제이쿼리에서 어떤 요소의 value를 얻어오거나 변경하기 위한 문법
  • 여기까지 끝났다면 우리는 객체의 배열을 가지고 있다.
    • 각각의 객체는 'field' - 'field의 id값'(컬럼명), 'keyword' - 'keyword의 value'(검색어)라는 두개의 key-value 쌍을 가진다.
    • 여기서 고민이 된다.
      결국엔 Mapper에서 필요로하는 파라미터는 List타입인데, 우리가 만든 객체는 배열을 타입으로 가지기 때문이다.
      자바스크립트에서 List객체를 만드는 방법도 잘 모르겠고 이게 자바에서의 List와 같은지 확신이 서지 않았다.
      일단은 배열째로 컨트롤러로 넘겨주려는데, 구글링 좀 더 해보니 배열로 넘겨줘도 자바에서 List로 받을 수 있는 것 같았다.

 

function getMap(){
	let search = $('.search');
	search = [];
	console.log(search);
	$('.search').each(function(){
		console.log($(this).attr(('id')));*/
		let s ={
			field : $(this).attr('id'),
			keyword : $(this).val()
		};
		search.push(s);
	});
}    
window.onload = function(){
    getMap();
}

 

 

2. 자바스크립트 객체 - 제이슨 데이터타입 변환

이건 간단하다. JSON.Stringify(제이슨으로 변환할 js 객체)하면 끝이다.

JSON 문자열이 어떤 형태를 가지는지는 콘솔에 찍어가며 확인하고, 원하는 값이 얻어지면 이제 ajax를 통해 넘겨주면 된다.

역시나 getMap() 메소드 안에서 실행한다. 위의 코드 익명구현메소드(?)가 끝나고 바로 아래 코드를 추가해주면 된다.

	var jsonData = JSON.stringify(search);
	console.log(jsonData);

 

3. ajax를 통해 js에서 컨트롤러로 값 넘기기

1) $.ajax 메소드 작성

  검색해보면 사용하는 프레임워크에 따라, 라이브러리에 따라, ajax 수행부를 작성하는 방법이 제각각이다. 

  이것저것 보다가 공통적으로 존재하는 것들을 뽑아내고 실행해보았다.

  • url : 어떤 주소로 요청할 것인지 (나는 상대경로를 사용했다. 컨트롤러의 url mapping명과 맞춰준다)
  • type : get요청인지 post요청인지 (나는 넘겨줄 값이 많아 post로 요청)
  • succes : 서버와의 통신이 성공했을 경우 수행할 코드를 작성한다.
    • 실제로 서버에서 데이터를 받아와 사용할 예정이라면 function(data){ 받아온 데이터 작업}과 같은 형태
    • 여기서는 일단 컨트롤러로 값이 잘넘어가는지 확인하는 게 목적이라,  컨트롤러의 return값은 void로 주고 js에서는 console에 성공 여부만을 찍었다.
  • error : 통신이 실패할 경우 에러코드를 보여주게 했다.
  • 이 외에도 dataType :'json'등 옵션을 추가해야 할수도 있다.
    나는 제이슨 데이터를 넘겨주면, 서버에서 제이슨으로 인식할 수 있도록 하는 라이브러리(jackson)를 사용할 예정이기 때문에 추가하지 않았다. 
  • $.ajax({
    	url : "searchlist",
    	type:"post",
    	data : {
    		"search":jsonData
    		},
    	success : function(){ 
        		console.log("값 넘겨주기 성공");
    	},
    	error : function(xhr){
    		console.log(xhr.status);
    	}
    });

2) 컨트롤러 작성

@Responsebody : 메소드명 앞에 추가한다.

    사실 이건 자바에서 클라이언트로 객체를 전달할 때 문자열로 전달하기 위한 장치인데,

   순전히 값을 받기 위해서도 필요한지는 모르겠다. 그리고 나중에는 Responsebody를 대체할 예정

@RequestParam : 클라이언트에서 넘겨준 데이터를 받기 위해 추가

                                    경우에 따라서 String[]으로 받기도 하고 리스트로 받기도 하던데 내 경우엔 문자열로 받아 변한하는 게 유효했다.

	@PostMapping("searchlist")
	public void @ResponseBody searchResult(@RequestParam(value = "search") String search) 
		throws JsonMappingException, JsonProcessingException {
        System.out.println(search);
    }

키워드를 입력할 때마다

[{"field:컬럼명1", "key:키워드1" }, ... ,{"field:emp_name", "key:김" } ] 이런 형태로 값이 찍히면 성공

 

 

 

 

 

4. 컨트롤러에서 데이터 변환

제이슨 배열 객체를 문자열로 변환해 받았으니 문자열을 다시 객체로 변환해야 한다.

우리가 다시 얻고 싶은 데이터 타입은 사실 객체의 리스트이다. 

이것저것 찾아봤는데, jason-simple 등 여러 라이브러리가 있지만

나는 뉴렉처 강의에서 다룬 jackson라이브러리를 이용하기로 결정했다.

 

pom.xml에 의존성 주입

: 서버에서 객체를 반환하면 클라이언트에서 객체 값을 문자열로 변환해 받을 수 있게 함

<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-databind</artifactId>
	<version>2.11.2</version>
</dependency>

servlet-context.xml에 설정 추가

: 사실 이건 스프링이 각각의 클래스에서 어노테이션을 읽도록 하는 설정이라, 스프링을 통해 프로젝트를 구현하고 있다면 대부분 이미 설정을 추가해놨을 것이다.

<mvc:annotation-driven />

 

이제 데이터를 변환할 준비가 되었다.

ObjectMapper 객체를 사용하여 각 인덱스에 객체를 가지고 있는 배열 제이슨 문자열을 다시 객체로 변환한다.

이를 위해 내가 찾아본 방법으로는 두가지가 유효했다.

1) mapper.readValue(제이슨 문자열, new TypeReference<List<클래스>>)

2) mapper.readValue(제이슨 문자열, new TypeReference<List<Map<String, String>>>(){});

  나는 key와 value의 세트가 두개뿐인지라, key값을 필드로 가지는 객체를 따로 만들지 않고 두번째 맵을 사용했다.   

  주석은 변환 후에 값이 성공적으로 찍히는지를 알아보기 위한 흔적이다.

 

@PostMapping("searchlist")
	public List<MemberList> searchResult(@RequestParam(value = "search") String search) throws JsonMappingException, JsonProcessingException {
		
		ObjectMapper mapper = new ObjectMapper();
//		List<Search> list = mapper.readValue(search, new TypeReference<List<Search>>() {});
//		List<MemberList> model =service.searchList(list);
		List<Map<String, String>> mapList = mapper.readValue(search, new TypeReference<List<Map<String, String>>>(){});
		System.out.println(search);
//		System.out.println(mapList.get(0).get("field"));
//		System.out.println(mapList.get(0).get("key"));
//		System.out.println(mapList.get(1).get("key"));
//		System.out.println(mapList.get(1).get("field"));
		List<MemberList> model =service.searchList(mapList);
        return model;
	}

 

자바에서 사용 가능한 리스트 객체를 얻었다면 서비스함수를 호출하여 쿼리를 실행시킨다.

리턴값은 List<MemberList>이고, 메소드의 리턴타입도 void에서 변경해주었다.

5. 리턴 데이터를 ajax를 통해 js로 돌려주기

다시 js로 객체를 돌려주기 위해서는 json 문자열로 변환해야 한다.

대충 찾아봤는데 무슨 이스케이프문자도 고려해야하고 머리가 아팠다.

그래서 내 진짜 스승님 뉴렉처님의 말씀대로 @RestController 어노테이션을 이용하기로 했다.

 

해당 어노테이션은 @RequestBody를 대체하면서도, 우리가 그냥 객체를 리턴할 때 알아서 제이슨 문자열로 반환해서 js로 전달한다.

그런데 @RestController는 클래스 위에 붙이는 어노테이션이다보니, 나는 해당 요청을 처리하는 메소드를 따로 개별 클래스로 분리했다.

이제 js의 ajax success 블럭을 수정해야한다.

참고 : 2021.06.09 - [Servlet] - 뉴렉처 스프링 MVC

 

6. 받은 데이터를 뷰단에서(js 이용) 사용하기 (뷰단 갱신)

 

List<memberList>가 변환된 제이슨 문자열을 member이라는 변수명으로 받기로 했다.

다시 배열로 넘어오기 때문에 반복문 안에서 member[i].속성명으로 사용하면 되는데,

문제는 js로 어떻게 이미 존재하는 테이블에 값을 넣지?였다.

 

remove하고 insertRow(), insertColumn() 이런 메소드들을 사용하기도 하던데, 나는 자바스크립트가 아닌 제이쿼리 문법을 사용하고 싶었다.

 

여러 방법이 있겠지만 내 경우에 수정해야할 부분은 tbody였다. jsp 파일의 초기 코드는 다음과 같다.

			<tbody id="ajax">
			<c:forEach var="list" items="${list}">
				<tr>
					<td>${list.org_groupname}</td>
					<td>${list.emp_dept}</td>
					<td>${list.emp_id}</td>
					<td>${list.emp_name}</td>
					<td>${list.emp_posi}</td>
					<td>${list.emp_tel}</td>
					<td>${list.emp_email}</td>
					<c:if test="${list.emp_level=='ROLE_ADMIN'}">
						<td>관리자</td>
					</c:if>
					<c:if test="${list.emp_level=='ROLE_MEMBER'}">
						<td>직원</td>
					</c:if>
				</tr>
			</c:forEach>
			</tbody>

 

ajax가 아닌 mvc방식으로 데이터를 가져올 때의 jsp이다.

tbody부분만 수정하면 되기 때문에 tbody에 아이디를 부여하고, 안의 html을 새로 만들어 수정하면 된다.

html이라는 변수를 만들어 그 안에 내용(태그)을 문자열 연산으로 채워나간다. 

반복문이 끝나면 #ajax를 선택해 html(해당 내용 담긴변수) 함수로 그 안의 html을 수정할 수 있다.

 

	$.ajax({
		url : "searchlist",
		type:"post",
		data : {
			"search":jsonData
			},
		success : function(member){
			var html='';
			for(var i=0; i<member.length; i++){
				html += "<tr>"+
					"<td>"+member[i].org_groupname+"</td>"+
					"<td>"+member[i].emp_dept+"</td>" +
					"<td>"+member[i].emp_id+"</td>" +
					"<td>"+member[i].emp_name+"</td>" +
					"<td>"+member[i].emp_posi+"</td>" +
					"<td>"+member[i].emp_tel+"</td>" +
					"<td>"+member[i].emp_email+"</td>";
				if(member[i].emp_level == "ROLE_ADMIN")
					html +="<td>관리자</td>";
				if(member[i].emp_level == "ROLE_MEMBER")
					html +="<td>직원</td>";
					html+="</tr>";
			}
			$("#ajax").html(html);
			
		},
		error : function(xhr){
			console.log(xhr.status);
		}
	});

 

전체코드

더보기

JavaScript

function getMap(p){
	let search = $('.search');
	search = [];
	
	console.log(search);
	$('.search').each(function(){
		let s ={
			field : $(this).attr('id'),
			keyword : $(this).val()
		};
		search.push(s);
	});
	var jsonData = JSON.stringify(search);
	console.log(jsonData);
	
	$.ajax({
		url : "searchlist",
		type:"post",
//		dateType:'json', @ResponseBody를 담아놓았기 때문에 자동 처리
		data : {
			"search":jsonData
			},
		success : function(member){
//			$("#ajax").remove();
//			var html='<tbody id="ajax">';
			var html='';
			for(var i=0; i<member.length; i++){
				html += "<tr>"+
							"<td>"+member[i].org_groupname+"</td>"+
							"<td>"+member[i].emp_dept+"</td>" +
							"<td>"+member[i].emp_id+"</td>" +
							"<td>"+member[i].emp_name+"</td>" +
							"<td>"+member[i].emp_posi+"</td>" +
							"<td>"+member[i].emp_tel+"</td>" +
							"<td>"+member[i].emp_email+"</td>";
				if(member[i].emp_level == "ROLE_ADMIN")
					html +="<td>관리자</td>";
				if(member[i].emp_level == "ROLE_MEMBER")
					html +="<td>직원</td>";
					html+="</tr>";
			}
			$("#ajax").html(html);
			
		},
		error : function(xhr){
			console.log(xhr.status);
//			alert("Error code : " + xhr.status);
		}
	});
}


window.onload = function(){
    getMap();
}

 

Controller @RestController

@PostMapping("searchlist")
	public List<MemberList> searchResult(@RequestParam(value = "search") String search) throws JsonMappingException, JsonProcessingException {
		
		ObjectMapper mapper = new ObjectMapper();
		List<Map<String, String>> mapList = mapper.readValue(search, new TypeReference<List<Map<String, String>>>(){});
		System.out.println(search);
		List<MemberList> model =service.searchList(mapList);
        return model;
	}

 

VIEW

<table class="tbl-ex emp-tbl">
			<summary>총 직원수 : ${cnt}</summary>
			<thead>
				<tr class="search-tr">
					<th><input type="text" class="search" id="org_groupname" onkeyup="getMap(1)" placeholder="검색.."></th>
					<th><input type="text" class="search" id="org_teamname" onkeyup="getMap(1)" placeholder="검색.."></th>
					<th><input type="text" class="search" id="emp_id" onkeyup="getMap(1)" placeholder="검색.."></th>
					<th><input type="text" class="search" id="emp_name" onkeyup="getMap(1)" placeholder="검색.."></th>
					<th><input type="text" class="search" id="emp_posi" onkeyup="getMap(1)" placeholder="검색.."></th>
					<th><input type="text" class="search" id="emp_tel" onkeyup="getMap(1)" placeholder="검색.."></th>
					<th><input type="text" class="search" id="emp_email" onkeyup="getMap(1)" placeholder="검색.."></th>
					<th><input type="text" class="search" id="emp_level" onkeyup="getMap(1)" placeholder="검색.."></th>
				</tr>
				<tr class="title-tr">
					<th>본부</th>
					<th>부서</th>
					<th>사번</th>
					<th>이름</th>
					<th>직급</th>
					<th>내선번호</th>
					<th>이메일</th>
					<th>액세스권한</th>
				</tr>
			</thead>
			<tbody id="ajax">
			<c:forEach var="list" items="${list}">
				<tr>
					<td>${list.org_groupname}</td>
					<td>${list.emp_dept}</td>
					<td>${list.emp_id}</td>
					<td>${list.emp_name}</td>
					<td>${list.emp_posi}</td>
					<td>${list.emp_tel}</td>
					<td>${list.emp_email}</td>
					<c:if test="${list.emp_level=='ROLE_ADMIN'}">
						<td>관리자</td>
					</c:if>
					<c:if test="${list.emp_level=='ROLE_MEMBER'}">
						<td>직원</td>
					</c:if>
				</tr>
			</c:forEach>
			</tbody>
		</table>