74일차 내용 정리

채공부·2025년 9월 5일

RESTful API

HTTP 메서드별 역할과 특징

  1. GET /members
  • 회원 목록 요청 ➜ 응답 ➜ 회원 목록을 JSON 으로 응답 [{}, {}, {}]
    • 어떤 컨텐츠를 가지고 오는 것이 주 목적
  1. POST /members
  • 회원 추가 요청 ➜ 응답 ➜ 추가한 회원 정보를 응답 { }
    • 어떤 컨텐츠를 전송하는 것이 주 목적
    • 서버에 무언가를 저장하겠다

GETPOST 는 요청 경로는 같지만 요청 메소드가 다르기에
@GetMapping & @PostMapping 어노테이션으로 구분이 가능하다

  1. GET /members/1
  • 1번 회원 정보 요청 ➜ 응답 ➜ 1번 회원 정보를 응답 { }
  1. DELETE /members/1
  • 1번 회원 삭제 요청 ➜ 응답 ➜ 삭제한 회원 정보를 응답 { }
    • 어떤 컨텐츠를 삭제하기 위한 것이 주 목적
  1. PUT /members/1
  • 1번 회원 전체 필드 수정 요청 ➜ 응답 ➜ 수정한 회원 정보를 응답 { }
    • 어떤 컨텐츠를 수정하기 위한 것이 주 목적
  1. PATCH /members/1
  • 1번 회원 일부 필드 수정 요청 ➜ 응답 ➜ 수정한 회원 정보를 응답 { }

➜ 위에 4개도 요청 경로는 같지만 요청 메소드로 구분이 가능하다

RESTful API의 요청–응답 흐름

  • 클라이언트(웹/모바일)는 서버에 RESTful 방식으로 요청을 보내고
    서버는 요청 메소드에 따라 컨트롤러에서 해당 로직을 처리한 뒤 JSON 응답을 반환

Spring에서 REST API 구현

@RestController

컨트롤러에서 리턴한 List 나 dto 를 JSON 문자열로 변경해서 응답

JSON 변환 방식
1. List : [ ] 로 변경
2. Map or dto : { } 로 변경
3. List<dto> : [{ },{ },{ }...] 형식으로 변경

  • dto 의 필드 구성에 따라 좀 더 복잡한 형식의 { } 가 구성될 수도 있다
{
	"num" : 1,
	"name" : "kim",
	"hobby" : ["xxx", "yyy"],
	"info" : {  }
}
  • RestController 에 create 메소드 생성
    • 회원 추가 요청 처리
@PostMapping("/members")
public MemberDto create(MemberDto dto) {	
	return memberService.addMember(dto);
}
  • MemberService 인터페이스에 addMember & deleteMember 메소드 수정
public MemberDto addMember(MemberDto dto);
public MemberDto deleteMember(int num);
  • MemberServiceImpl 에 해당 오버라이드한 메소드 코드 수정
@Transactional
@Override
public MemberDto addMember(MemberDto dto) {
	// insert or update 된 entity 를 리턴해준다
	Member m = memberRepo.save(dto.toEntity());
    // 방금 추가한 회원의 정보를 리턴해준다
	return MemberDto.toDto(m);
}

@Transactional
@Override
public MemberDto deleteMember(int num) {
	// 만일 삭제할 Entity 가 존재하지 않으면
	if(!memberRepo.existsById(num)) {
		throw new IllegalArgumentException("삭제할 회원이 존재하지 않습니다 num = "+num);
	}
	Member m = memberRepo.findById(num).get();	
	// 번호를 이용해서 삭제 (실패 시 예외가 발생하지 않는다)
	memberRepo.deleteById(num);
		
	return MemberDto.toDto(m);
}
  • RestController 에 detail 메소드 추가
    • 회원 한 명의 정보 요청 처리
@GetMapping("/members/{num}")
public MemberDto detail(@PathVariable int num) {
	return memberService.getMember(num);
}
  • RestController 에 delete 메소드 추가
    • 회원 정보 삭제 요청 처리
@DeleteMapping("/members/{num}")
public MemberDto delete(@PathVariable int num) {
	return memberService.deleteMember(num);
}
  • RestController 에 update 메소드 추가
    • 회원 정보 전체 수정 요청 처리
@PutMapping("/members/{num}")
public MemberDto update(@PathVariable int num, MemberDto dto) {
	// 수정할 회원의 번호를 dto 에 넣어준다
	dto.setNum(num);
	memberService.updateMember(dto);
	return dto;
}

JSON 요청 처리와 @RequestBody

<form method="post">
	<input type="text" name="name" />
	<input type="text" name="addr" />
</form>

Content Type = application/x-www-form-urlencoded 형식으로 전송된다

이런 형식의 전송은 input 요소에 입력한 내용이 query 문자열 로 변경되어서
요청의 body 에 들어간다

body 에 포함되는 query 문자열 : name=kimgura&addr=seoul

react 에서는 form 자체를 전송하지 않고 fetch() 혹은 axiosjavascript 로 페이지 전환없이 전송할 내용을 json 문자열로 만들어서 전송한다
그런 경우, Content-Type = application/json 이고 요청의 body 에 {"name":"kimgura", "addr":"seoul"} 형식의 json 문자열을 포함시켜서 전송

json 문자열을 전송하면 서버에서 request.getParameter() 로 추출 X
➜ 추출 방법이 다르다

spring boot 에서는 @RequestBody 어노테이션을 이용하면 쉽게 추출할 수 있다

  • RestController 에 create & update 메소드에 @RequestBody 어노테이션 추가
    • json 문자열이 전송되면 @RequestBody 어노테이션이 필요
// 회원 추가 요청 처리
@PostMapping("/members")
public MemberDto create(@RequestBody MemberDto dto) {
	return memberService.addMember(dto);
}

@PutMapping("/members/{num}")
public MemberDto update(@PathVariable int num, @RequestBody MemberDto dto) {
	// 수정할 회원의 번호를 dto 에 넣어준다
	dto.setNum(num);
	memberService.updateMember(dto);
	return dto;
}

Swagger(OpenAPI)로 API 문서 자동화

Swagger 설정하기

spring09.config 패키지 생성

  • pom.xml 에 swagger ui 를 사용하기 위한 dependency 코드 추가
    • (spring boot 3.0 이상 버전에서)
<dependency>
	<groupId>org.springdoc</groupId>
	<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
	<version>2.6.0</version>
</dependency>
  • SwaggerConfig 클래스 생성
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;

@Configuration
public class SwaggerConfig {
	@Bean
	public OpenAPI customOpenAPI() {
		Info info = new Info()
				.title("API 문서입니다")
				.version("v1")
				.description("API 공부를 시작합시다");
		
		return new OpenAPI().info(info);
	}
}

Swagger UI 사용하기

  • http://localhost:9000/swagger-ui/index.html 로 링크 가기

Swagger 어노테이션 활용

@Tag

API 그룹화

  • RestController 에 Tag 어노테이션 추가
    • 태그
@Tag(name = "Members", description = "회원 관리 API") // swagger ui 에서 제공할 정보

@Operation

API 설명 추가

  • RestController 에 list 메소드에 @Operation 어노테이션 추가
    • 동작에 대한 설명
@Operation(summary = "회원 목록 조회", description = "전체 회원을 페이징 없이 반환합니다")

@Schema

DTO 문서화

  • MemberDto 에 @Schema 어노테이션 추가
@Schema(name="MemberDto", description = "회원 정보 DTO")

React

배열 구조 분해 할당

배열의 구조를 분해해서 변수나 상수한테 할당하는 문법

const [state, setState] = useState([]);
  • App3.jsx 에 fetch 함수 추가
    • data : 회원 목록이 들어 있는 배열
    • spring boot 서버로부터 받아온 데이터로 상태값을 변경하면 ui 가 업데이트된다
fetch("/api/v1/members")
	.then(res=>res.json())
	.then(data=>{
		setMembers(data);
});

key

  • 반복문 출력 시 key 가 없는 경우 key 를 generate 해서 넣어줘야 한다
<tbody>
	{members.map(item=><tr key={item.num}>
		<td>{item.num}</td>
		<td>{item.name}</td>
		<td>{item.addr}</td>
	</tr>)}
</tbody>

useEffect

  • App4.jsx 생성
import React, { useState } from 'react';

function App4() {
    // 회원 목록을 상태값으로 관리하기
    const [members, setMembers] = useState([]);

    return (
        <div className='container'>
            <button onClick={()=>{
                fetch("/api/v1/members")
                .then(res=>res.json())
                .then(data=>{
                    // data 는 회원 목록이 들어 있는 배열
                    /// spring boot 서버로부터 받아온 데이터로 상태값을 변경하면 ui 가 업데이트된다
                    setMembers(data);
                });
            }}>받아오기</button>
            <h1>회원 목록</h1>
            <table>
                <thead>
                    <tr>
                        <th>번호</th>
                        <th>이름</th>
                        <th>주소</th>
                    </tr>
                </thead>
                <tbody>
                    {members.map(item=><tr key={item.num}>
                        <td>{item.num}</td>
                        <td>{item.name}</td>
                        <td>{item.addr}</td>
                    </tr>)}
                </tbody>
            </table>
        </div>
    );
}

export default App4;
  • App4.jsx 에 useEffect 코드 추가

    • useXXX() 형식을 react 에서 자주 사용하는 데 이걸 hook 이라고 부른다
    • hook : react 에서 사용하는 유틸리티라고 생각하면 된다
    • useEffect(()=>{}, [])
    • 두번째 매개변수에 빈 배열을 전달하면 첫번째 매개변수에 전달한 함수가 이 component(App4 component) 가 준비가 되었을 때 최초 1번 호출된다
    • 무언가 준비할 게 있으면 해당 함수 안에서 준비하면 된다
useEffect(()=>{
        // table 에 출력할 회원 목록을 받아와서 state 를 변경 시킨다
}, []);
  • fetch 함수 useEffect 안으로 이동
useEffect(()=>{
// table 에 출력할 회원 목록을 받아와서 state 를 변경 시킨다
        fetch("/api/v1/members")
        .then(res=>res.json())
         .then(data=>{
            setMembers(data);
        });
}, []);

useEffect() 실행 시점 정리

  1. 빨간색 구역
  • 처음 컴포넌트가 준비되는 시점 , 상태값이 변경되었을 때 빨간영역이 실행된다
  1. 노란색 구역
  • 컴포넌트 준비가 완벽하게 끝났을 때 호출
  1. 초록색 구역
  • 요청하고 난 후 서버가 정상적으로 응답할 때 실행

➜ 빈 배열 : 비어두었다면 컴포넌트가 호출되는 최초의 한 번에만 실행

회원 추가

  • form 코드 추가
<h1>회원 추가 양식</h1>
<form action="/api/v1/members" method="post">
	<input type="text" name="name" placeholder='이름 입력'/>
	<input type="text" name="addr" placeholder='주소 입력' />
	<button type="submit">추가</button>
</form>

onSubmit

  • onSubmit 이벤트 코드 추가
<h1>회원 추가 양식</h1>
<form action="/api/v1/members" method="post" onSubmit={(e) => {
    // 폼제출 막기
    e.preventDefault();
    // 폼에 입력한 내용을 담고 있는 FormData 객체를 얻어낸다
    const formData = new FormData(e.target);
    // 폼에 입력한 내용을 object 로 변환
    const obj = Object.fromEntries(formData);
    // object 를 JSON 문자열로 변환
    const json = JSON.stringify(obj);
    // fetch() 함수를 이용해서 전송하기
    fetch("/api/v1/members", {
        method: "post", // 요청 method
        headers: { "Content-Type": "application/json" }, // 요청 header
        body: json // 요청 body
    })
    .then(res => res.json())
    .then(data => {
        // data 는 방금 추가한 회원 정보
        console.log(data);
    });

}}>
    <input type="text" name="name" placeholder='이름 입력' />
    <input type="text" name="addr" placeholder='주소 입력' />
    <button type="submit">추가</button>
</form>
  • getMembers 함수 생성 후 그 안에 fetch 함수 이동
useEffect(() => {
    getMembers();
}, []);

// 회원 목록을 요청해서 상태값을 변경해주는 함수
const getMembers = () => {
    // table 에 출력할 회원 목록을 받아와서 state 를 변경 시킨다
    fetch("/api/v1/members")
        .then(res => res.json())
        .then(data => {
            setMembers(data);
            getMembers();
        });
};
  • form 안에 있는 e.target 코드 수정
<form action="/api/v1/members" method="post" onSubmit={(e) => {
    // 폼제출 막기
    e.preventDefault();
    // 이벤트가 일어난 그 요소(form)
    const form = e.target;
    // 폼에 입력한 내용을 담고 있는 FormData 객체를 얻어낸다
    const formData = new FormData(form);
    // 폼에 입력한 내용을 object 로 변환
    const obj = Object.fromEntries(formData);
    // object 를 JSON 문자열로 변환
    const json = JSON.stringify(obj);
    // fetch() 함수를 이용해서 전송하기
    fetch(form.action, {
        method: form.method, // 요청 method
        headers: { "Content-Type": "application/json" }, // 요청 header
        body: json // 요청 body
    })
    .then(res => res.json())
    .then(data => {
        // data 는 방금 추가한 회원 정보
        console.log(data);
        getMembers();
    });
}}>

회원 삭제

  • 삭제 칼럼과 삭제 버튼 추가
<th>삭제</th>

<td>
	<button>x</button>
</td>
  • 삭제 버튼을 눌렀을 때 호출되는 함수 추가
const handleDelete = (num) => {
    const isDelete = confirm(num + " 번 회원을 삭제 하시겠습니까?");
    if (isDelete) {
        fetch(`/api/v1/members/${num}`, {
            method: "delete"
        })
        .then(res => res.json())
        .then(data => {
            // 삭제 성공 시 여기가 호출된다
            getMembers();
        });
    }
};
  • 삭제 버튼에 이벤트 함수 추가
<button onClick={()=>{handleDelete(item.num)}}>x</button>

회원 정보 수정

  • 수정 칼럼과 수정 버튼 추가
<th>수정</th>

<td>
	<button>Edit</button>
</td>
  • 회원 수정 양식 코드 추가
    • class = className
    • for = htmlFor
<div>
    <h1><span>x</span> 번 회원의 수정 양식</h1>
    <form action="">
        <div>
            <label htmlFor="name">이름</label>
            <input type="text" name="name" id="name" value="x" />
        </div>
        <div>
            <label htmlFor="addr">주소</label>
            <input type="text" name="addr" id="addr" value="x" />
        </div>
        <button type="submit">수정 확인</button>
        <button type="reset">취소</button>
    </form>
</div>
  • true & false 상태값으로 관리
{true && <p>true 입니다</p>}
{false && <p>false 입니다</p>}
  • 회원 정보를 수정 중인지 여부를 상태값으로 관리하는 코드 추가
const [editing, setEditing] = useState(false);
  • 회원 수정 양식 조건부 렌더링
{editing && (
    <div>
        <h1><span>x</span> 번 회원의 수정 양식</h1>
        <form action="">
            <div>
                <label htmlFor="name">이름</label>
                <input type="text" name="name" id="name" value="x" />
            </div>
            <div>
                <label htmlFor="addr">주소</label>
                <input type="text" name="addr" id="addr" value="x" />
            </div>
            <button type="submit">수정 확인</button>
            <button type="reset">취소</button>
        </form>
    </div>
)}
  • 수정 버튼 클릭시 수정 폼이 나오게 하는 코드 추가
<button onClick={()=>{
	setEditing(true);
}}>Edit</button>
  • 선택한 회원의 정보를 상태값으로 관리하는 코드 추가
const [member, setMember] = useState({num:0, name:"", addr:""});
  • 상태값으로 관리 & 선택한 회원 정보 뜨게 하기
{editing && (
    <div>
        <h1><span>{member.num}</span> 번 회원의 수정 양식</h1>
        <form action="">
            <div>
                <label htmlFor="name">이름</label>
                <input type="text" name="name" id="name" value={member.name} />
            </div>
            <div>
                <label htmlFor="addr">주소</label>
                <input type="text" name="addr" id="addr" value={member.addr} />
            </div>
            <button type="submit">수정 확인</button>
            <button type="reset">취소</button>
        </form>
    </div>
)}
  • 수정할 회원의 정보를 받아와서 상태값을 변경하는 코드 추가
fetch(`/api/v1/members/${item.num}`)
.then(res=>res.json())
.then(data=>setMember(data));
  • 이름과 주소를 변경했을 때 실행할 함수
    • change 이벤트 발생 시 현재 입력한 내용으로 상태값을 변경해야 한다
const handleChange = (e) => {

};
  • 수정 폼에서 text 박스에 onChange 함수 추가
<input onChange={handleChange} type="text" name="name" id="name" value={member.name}/>
<input onChange={handleChange} type="text" name="addr" id="addr" value={member.addr}/>
  • handleChange 함수 안에 코드 추가
// 이벤트가 일어난 요소의 name 속성의 값 "name" or "addr"
const name = e.target.name;
// 이벤트가 일어난 요소에 입력한 값 "입력한 문자열"
const value = e.target.value;
// 상태값 변경하기
setMember({
	...member, // 기존의 object 안에 있는 값을 펼쳐놓고
	[name]:value // 수정한 내용의 value 만 덮어쓰기 한다
});
  • CMD 창 열어서 node 실행
    • 오브젝트의 방 이름을 참조되는 key 값으로 결정
C:\Users\USER>node

> const m1 = {num:1, name:"kim"};
undefined
> const m2 = {["num"]:1, ["name"]:"kim"};
undefined
> const a = "num";
undefined
> const b = "name";
undefined
> const m3 = {[a]:1, [b]:"kim"};
undefined
> m1
{ num: 1, name: 'kim' }
> m2
{ num: 1, name: 'kim' }
> m3
{ num: 1, name: 'kim' }
>
  • 수정폼에 action & onSubmit 코드 추가
    • form 의 action은 post 와 get 만 가능
<form onSubmit={handleUpdateSubmit} action={`/api/v1/members/${member.num}`}>
  • 회원 정보 수정 form 에 submit 이벤트가 일어났을 때 실행할 함수 추가
const handleUpdateSubmit = (e) => {
    // 폼 전송 막기
    e.preventDefault();
    const form = e.target; // 수정 form 요소의 참조값

    fetch(form.action, {
        method: "put", // put 방식
        headers: { "Content-Type": "application/json" }, // json 문자열을 보낸다고 알림
        body: JSON.stringify(member) // 상태값으로 관리하는 member object 를 json 문자열로 변경
    })
    .then(res => res.json())
    .then(data => {
        alert(`${data.num} 번 회원의 정보 수정 완료`);
        setEditing(false);
        setMember({
            num: 0, name: "", addr: ""
        });
        getMembers();
    })
}
  • 취소 버튼 클릭 시 수정 양식 사라지게 하는 이벤트 코드 추가
<button type="reset" onClick={()=>{
    	setEditing(false);}}>취소</button>

React 상태 변경과 렌더링 흐름

실습 숙제 Book 테이블로도 API 해보기

profile
학원 공부 내용 정리

0개의 댓글