상세 컨텐츠

본문 제목

프로그래머 도전기 89일차

프로그래머가 될거야!

by Choyee 2023. 12. 20. 21:06

본문

오늘은

 

오늘부터 백엔드의 스프링을 들어가기 위한 수업이 시작되었고

저는 따로 자바스크립트를 다시 공부를 하며

노드와  리액트를 위한 공부를 시작하였습니다

 

 

 

 

 

학원 수업

 

Servlet/Jsp -> Maven -> Spring Freamework
메이븐 프로젝트 - 상품관리(등록, 수정, 구매)
Market 프로젝트
Dynamic Web Project -> Maven Project로 변환

 

 

* maven에 필요한 dependency 가져오기

<dependencies>
    <!-- https://mvnrepository.com/artifact/javax.servlet/jstl -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.2.0</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
        <scope>provided</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/com.servlets/cos -->
    <dependency>
        <groupId>com.servlets</groupId>
        <artifactId>cos</artifactId>
        <version>09May2002</version>
    </dependency>
</dependencies>

 

 


<M(model)V(view)C(controller)패턴>

mvn 리포지터리
- 회면(view) - 템플릿언어 : jsp(jstl)
- DB연결(model) - mysql driver
- VO, DAO - 롬복(lombok)

 


** jsp **
- index.jsp  (mian.jsp)

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>KHMarket</title>
</head>
<body>
	<%-- <%@ include file="header.jsp" %> --%>
	<jsp:include page="header.jsp"/>
	<div class="container my-3">
		<h1 class="text-center">웹 마켓에 오신 것을 환영합니다</h1>
		<div class="text-center my-4">
			<img src="resources/images/main.jpg" alt="집이미지" style="width:500px" class="rounded-lg">
		</div>
	</div>
	<jsp:include page="footer.jsp"/>
	<%-- <%@ include file="footer.jsp" %> --%>
</body>
</html>

 

 

 

- product/list.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html>
<html>
<head>
'
<meta charset="UTF-8">
<title>상품 목록</title>
</head>
<body>
	<jsp:include page="../header.jsp" />
	<div class="container my-3">
		<h2 class="text-center">상품 목록</h2>
		<div class="row" align="center">
			<c:if test="${empty products}">
				<p>상품이 없습니다</p>
			</c:if>
			<c:if test="${not empty products}">
				<c:forEach items="${products}" var="product">
					<div class="col-4 my-5">
						<c:if test="${not empty product.pimage}">
							<img src="../upload/${product.pimage}" style="width: 300px; height:300px;">
						</c:if>	
						<h3>${product.pname}</h3>
						<p>${product.category}</p>
						<p>${product.price}원</p>
						<a href="/productinfo.do?pid=${product.pid}"
							class="btn btn-secondary">상세정보 &raquo;</a>
					</div>
				</c:forEach>
			</c:if>
		</div>
	</div>
	<jsp:include page="../footer.jsp" />
</body>
</html>

 

 


- product/pinfo.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>상품정보</title>
</head>
<body>
	<jsp:include page="../header.jsp" />
	<div class="container my-3">
		<h2>상품정보</h2>
		<div class="row">
			<div class="col-5">
				<img src="../upload/${product.pimage}" alt="" style="width:100%">
			</div>
			<div class="col-7">
				<h3>상품명 : ${product.pname}</h3>
				<p>상품설명 : ${product.description}</p>
				<p><b>상품코드</b> : <span class="badge bg-dark">${product.pid}</span></p>
				<p><b>분류</b> : ${product.category}</p>
				<p><b>재고수</b> : ${product.pstock}</p>
				<p><b>상태</b> : ${product.condition}</p>
				<p><b>가격</b> : ${product.price}</p>
				<form action="/addcart.do?pid=${product.pid}" name="addform" method="post">
					<!-- 상품 주문 버튼을 클릭하면 폼이 전송되어야 함 -->
					<a href="#" onclick="addToCart()" class="btn btn-success">상품 주문</a>
					<a href="/productlist.do" class="btn btn-secondary">상품 목록 &raquo;</a>
				</form>
			</div>
		</div>
	</div>
	<jsp:include page="../footer.jsp" />
	<script>
		let addToCart = function(){
			if(confirm("상품을 주문하시겠습니까?")){ // 확인, 취소
				document.addform.submit();
			}else {
				document.addform.reset();
			}
		}
	
	</script>
</body>
</html>

 


- product/pfrom.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>상품등록</title>
</head>
<body>
	<jsp:include page="../header.jsp" />
	<div class="container my-3">
		<h2>상품 등록</h2>
		<div class="row">
			<form action="/insertproduct.do" method="post" enctype="multipart/form-data">
				<div class="form-group row my-3">
					<label class="col-2">상품 코드</label>
					<div class="col-3">
					<p>상품코드 <input type="text" name="pid" class="form-control"></p>
					</div>
				</div>
				<div class="form-group row my-3">
					<label class="col-2">상품명</label>
					<div class="col-3">
					<p>상품명 <input type="text" name="pname" class="form-control"></p>
					</div>
				</div>
				<div class="form-group row my-3">
					<label class="col-2">가격</label>
					<div class="col-3">
					<p>가격 <input type="text" name="price" class="form-control"></p>
					</div>
				</div>
				<div class="form-group row my-3">
					<label class="col-2">상품 설명</label>
					<div class="col-4">
					<p>상품설명 <textarea rows="3" cols="40" name="description" class="form-control"></textarea></p>
					</div>
				</div>
				<div class="form-group row my-3">
					<label class="col-2">카테고리</label>
					<div class="col-3">
					<p>카테고리 <input type="text" name="category" class="form-control"></p>
					</div>
				</div>
				<div class="form-group row my-3">
					<label class="col-2">재고 수</label>
					<div class="col-3">
					<p>재고 수 <input type="text" name="pstock" class="form-control"></p>
					</div>
				</div>
				<div class="form-group row my-3">
					<label class="col-2">상태</label>
					<div class="col-3">
					<p>
						<label><input type="radio" name="condition" value="New" checked>신상품</label>
						<label><input type="radio" name="condition" value="Old">중고품</label>
					</p>
					</div>
				</div>
				<div class="form-group row my-3">
					<label class="col-2">상품 이미지</label>
					<div class="col-3">
					<p>상품 이미지 <input type="file" name="pimage" class="form-control"></p>
					</div>
				</div>
				<div class="form-group row my-3">
					<div class="col-3">
						<p><input type="submit" value="등록" class="btn btn-success"></p>
					</div>
				</div>
			</form>
		</div>
	</div>
	<jsp:include page="../footer.jsp" />
</body>
</html>

 

 

 


** java **
- controller/MainController(servlet)

package controller;

import java.io.IOException;
import java.util.Enumeration;
import java.util.List;

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 com.oreilly.servlet.MultipartRequest;
import com.oreilly.servlet.multipart.DefaultFileRenamePolicy;

import model.Product;
import model.ProductDAO;

@WebServlet("*.do")
public class MainController extends HttpServlet {
	private static final long serialVersionUID = 1L;
	// 필드
	ProductDAO pdao;
	
    public MainController() {
    	pdao = new ProductDAO();
    }

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		doPost(request, response);
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// 한글 인코딩
		request.setCharacterEncoding("utf-8");
		
		// command 패턴 경로 설정
		String uri = request.getRequestURI();
		String command = uri.substring(uri.lastIndexOf("/"));
		String nextPage = "";
		
		if(command.equals("/main.do")) {
			nextPage = "/main.jsp";
		}else if(command.equals("/productlist.do")) {
			// 목록 보기 메서드 호출
			List<Product> productlist = pdao.getProductList();
			// 모델 생성하기
			request.setAttribute("products", productlist);
			// 페이지 이동
			nextPage = "/product/list.jsp";
			
		}else if(command.equals("/productform.do")) {
			nextPage = "/product/pform.jsp";
		}else if(command.equals("/insertproduct.do")) {
			String realFolder ="C:\\jspworks\\Market\\src\\main\\webapp\\upload";
		    int maxSize = 10*1024*1024; //10MB
			String encType = "utf-8";   //파일이름 한글 인코딩
			DefaultFileRenamePolicy policy = new DefaultFileRenamePolicy();
			//5가지 인자
			MultipartRequest multi = new MultipartRequest(request, realFolder, maxSize, encType, policy);
			
			// 입력폼의 데이터 받기
			String pid = multi.getParameter("pid");
			String pname = multi.getParameter("pname");
			int price = Integer.parseInt(multi.getParameter("price"));
			String description = multi.getParameter("description");
			String category = multi.getParameter("category");
			int pstock =  Integer.parseInt(multi.getParameter("pstock"));
			String condition = multi.getParameter("condition");
			
			// file 파라미터
			Enumeration<?> files = multi.getFileNames();
		    String pimage = "";
		    while(files.hasMoreElements()) { //파일이름이 있는 동안 반복
		       String userFilename = (String)files.nextElement();
		       
	           //실제 저장될 이름
	           pimage = multi.getFilesystemName(userFilename);
		    }
			
			// 상품 객체 1개 생성
			Product product = new Product();
			product.setPid(pid);
			product.setPname(pname);
			product.setPrice(price);
			product.setDescription(description);
			product.setCategory(category);
			product.setPstock(pstock);
			product.setCondition(condition);
			product.setPimage(pimage);
			
			// db에 등록할 메서드 호출
			pdao.insertProduct(product);
			
			// 경로로 설정해주어야 데이터가 들어감
			nextPage = "/productlist.do";
		}else if(command.equals("/productinfo.do")) {
			String pid = request.getParameter("pid");
				
			// 상세보기 메서드 호출
			Product product = pdao.getProduct(pid);
			
			// 모델 생성
			request.setAttribute("product", product);
			nextPage = "/product/pinfo.jsp";
		}
		
		if(command.equals("/insertproduct.do")) {
			response.sendRedirect("/productlist.do");
		}else {
			// 페이지 이동(forward), 리다이렉트(redirect)
			RequestDispatcher dispatch = request.getRequestDispatcher(nextPage);
			dispatch.forward(request, response);
		}
	}
}

 

 


- common/JDBCTest, JDBCUtil

package common;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

// DB에 연결하고 종료하는 클래스
public class JDBCUtil {
	
	static String driverClass = "com.mysql.cj.jdbc.Driver";
	static String url = "jdbc:mysql://localhost:3306/jwebdb?serverTime=Asia/Seoul";
	static String user = "jweb";
	static String password = "pwjweb";
	
	Connection conn = null;
	PreparedStatement pstmt = null;
	ResultSet rs = null;
	
	// DB 연결 메서드
	public static Connection getConnection() {
		try {
			Class.forName(driverClass);
			// 연결 됐을 때의 return
			return DriverManager.getConnection(url, user, password);
		} catch (Exception e) {
			e.printStackTrace();
		}
		// 연결이 안됐을 때의 return
		return null;
	}
	
	// DB 종료 메서드(추가, 수정, 삭제)
	public static void close(Connection conn, PreparedStatement pstmt) {
		if(pstmt != null) {
			try {
				pstmt.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
		if(conn != null) {
			try {
				conn.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}
	
	// DB 종료 메서드(검색)
	public static void close(Connection conn, PreparedStatement pstmt, ResultSet rs) {
		if(rs != null) {
			try {
				rs.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
		
		if(pstmt != null) {
			try {
				pstmt.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
		
		if(conn != null) {
			try {
				conn.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}
}

 

 


- model/Product, ProductDAO

package model;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import common.JDBCUtil;

public class ProductDAO {
	
	Connection conn = null;
	PreparedStatement pstmt = null;
	ResultSet rs = null;
	
	// 목록 보기
	public List<Product> getProductList(){
		List<Product> productlist = new ArrayList<>();
		try {
			// db연결
			conn = JDBCUtil.getConnection();
			// sql처리
			String sql = "select * from product";
			pstmt = conn.prepareStatement(sql);
			rs = pstmt.executeQuery();
			while(rs.next()) {
				Product p = new Product();
				p.setPno(rs.getInt("p_no"));
				p.setPid(rs.getString("p_id"));
				p.setPname(rs.getString("p_name"));
				p.setPrice(rs.getInt("p_price"));
				p.setDescription(rs.getString("p_description"));
				p.setCategory(rs.getString("p_category"));
				p.setPstock(rs.getLong("p_stock"));
				p.setCondition(rs.getString("p_condition"));
				p.setPimage(rs.getString("p_image"));
				p.setRegDate(rs.getTimestamp("regdate"));
				p.setUpdateDate(rs.getTimestamp("updatedate"));
				
				productlist.add(p);  // 리스트에 객체 저장
				
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally { // db종료
			JDBCUtil.close(conn, pstmt, rs);
		}
		return productlist;
	}
	
	//상품 등록 메서드
	public void insertProduct(Product p) {
		try {
			// db연결
			conn = JDBCUtil.getConnection();
			// sql처리
			String sql = "insert into product(p_id, p_name, p_price, p_description, "
						+ "p_category, p_stock, p_condition, p_image) "
						+ "values (?, ?, ?, ?, ?, ?, ?, ? )";
			pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, p.getPid());
			pstmt.setString(2, p.getPname());
			pstmt.setInt(3, p.getPrice());
			pstmt.setString(4, p.getDescription());
			pstmt.setString(5, p.getCategory());
			pstmt.setLong(6, p.getPstock());
			pstmt.setString(7, p.getCondition());
			pstmt.setString(8, p.getPimage());
			// sql 실행
			pstmt.executeUpdate();
		} catch (SQLException e) {
			e.printStackTrace();
		} finally { // db종료
			JDBCUtil.close(conn, pstmt);
		}
	}

	public Product getProduct(String pid) {
		Product p = new Product();
		try {
			// db연결
			conn = JDBCUtil.getConnection();
			// sql처리
			String sql = "select * from product where p_id=?";
			pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, pid);
			rs = pstmt.executeQuery();
			if(rs.next()) {
				p.setPno(rs.getInt("p_no"));
				p.setPid(rs.getString("p_id"));
				p.setPname(rs.getString("p_name"));
				p.setPrice(rs.getInt("p_price"));
				p.setDescription(rs.getString("p_description"));
				p.setCategory(rs.getString("p_category"));
				p.setPstock(rs.getLong("p_stock"));
				p.setCondition(rs.getString("p_condition"));
				p.setPimage(rs.getString("p_image"));
				p.setRegDate(rs.getTimestamp("regdate"));
				p.setUpdateDate(rs.getTimestamp("updatedate"));
				
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally { // db종료
			JDBCUtil.close(conn, pstmt, rs);
		}
		return p;
	}
}

 

 

 


** db **
- table: product

use jwebdb;

create table product (
	p_no			int primary key auto_increment,  -- 일련번호
    p_id			varchar(10) unique,              -- 상품코드
	p_name			varchar(30) not null,			 -- 상품명	
    p_price			int not null,					 -- 상품 가격
    p_description	text not null,					 -- 상품 설명
    p_category		varchar(30),					 -- 상품 분류
    p_stock			long,					 		 -- 재고수
    p_condition		varchar(20),					 -- 신상품, 중고품
    p_image			varchar(30),					 -- 상품 이미지
    regdate			datetime default now(),			 -- 등록일
    upadatedate		datetime						 -- 수정일
);

select * from product;

insert into product(p_id, p_name, p_price, p_description, 
			p_category, p_stock, p_condition, p_image)
values ('p1234', 'Galaxy21', '1500000', '저장 용량 64GB, 화면 크기 6.2인치',
		'smart phone', '10000', '신상품', 'p1234.png');

 

 

 



** resources **
- css, js, images, sql

 

*js

let addToCart = function(){
    if(confirm("상품을 주문하시겠습니까?")){ // 확인, 취소
        document.addform.submit();
    }else {
        document.addform.reset();
    }
}

 

* css, js

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>

 

 

 

JavaScript 공부

 

<<콜백 탈출>>
콜백 지옥 = 연속되는 비동기 함수들을 처리할 때
    비동기 처리의 결과값을 사용하기 위해서 콜백이 깊어지는 현상

<promise 객체>
= javaScript의 비동기를 돕는 객체
   => 비동기처리의 결과값을 핸들링할 수 있는 코드를 비동기 함수로부터 분리할 수 있다

 

 

* 비동기 작업이 가질 수 있는 3가지 상태
1. Pending(대기 상태) = 비동기가 진행중이거나
                              시작할 수도 없는 문제가 있는 상태
2. resolve 해결 -> Fulfilled(성공) = 이행, 성공 상태
                                           의도한대로 비동기 작업이 정상적으로 완료된 상태
3. reject 거부 ->  Rejected(실패) = 거부, 실패 상태
                                            비동기 작업이 모종의 이유로 실패했음을 의미

 

 

* promise 객체와 내장 함수 .then(), .catch()

function isPositive(number, resolve, reject) {
    setTimeout(()=>{
        // 전달받은 number가 숫자형 타입이 아니라면 비동기 처리 -> 실패
        // 전달받은 number가 숫자형 타입이 맞다면 비동기 처리 ->성공
        if(typeof number == 'number'){
            // 성공 -> resolve
            resolve(number >= 0 ? "양수" : "음수");
        }else {
            // 실패 -> reject
            reject("주어진 값이 숫자형 값이 아닙니다");
        }

    },2000) // 2s
}   // 2초 뒤에 콜백함수를 실행 전달받은 인자를 판단해줌


// 함수 호출
// isPositive(
//     [], 
//     (res)=>{
//         console.log("성공적으로 수행됨 : ", res);
//     }, 
//     (err)=>{
//         console.log("실패 하였음 : ", err);
//     });


// function isPositiveP(){
//     const executor = ()=>{setTimeout(()=>{if(''){}else{}},2000)}
// }
function isPositiveP(number) {
    // 비동기 작업을 실질적으로 실행시켜 주는 함수 executor 생성
    const executor = (resolve,reject) => {   // 실행자
        setTimeout(()=>{
            if(typeof number == 'number'){
                // 성공 -> resolve
                console.log(number);
                resolve(number >= 0 ? "양수" : "음수");
            }else {
                // 실패 -> reject
                reject("주어진 값이 숫자형 값이 아닙니다");
            }
        },2000);
    };

    // 비동기 작업 자체인 Promise를 저장할 상수 asyncTask 생성
    // new 키워드를 사용 -> promise 객체 생성자로 비동기작업 실행자 함수를 넘겨줌
    const asyncTask = new Promise(executor);
    // executor를 바로 실행하게 된다
    return asyncTask;
    // isPositiveP의 반환값이 promise로 바뀌게 된다
    // 어떤 함수가 promise를 반환한다는 것
    //  = 이 함수는 비동기작업을 하고 그 작업의 결과를
    // promise 객체로 반환을 받아서 사용할 수 있는 함수라는 것
}

const res = isPositiveP(101);
// promise의 내장 함수 .then(), .catch()
// res.then(()=>{}).catch(()=>{});
res
    .then((res)=>{  // resolve
        console.long("작업 성공 : ", res);
    })
    .catch((err)=>{  // reject
        console.log("작업 실패 : ", err);
    });
    
// resolve(number >= 0 ? "양수" : "음수"); 에서 전달한 양수라는 값이
// console.long("작업 성공 : ", res); 콜백 함수에 들어와서 실행 됨

 

 

* promise 객체 사용

// function taskA(a,b,cb){
//     setTimeout(()=>{
//         const res = a+b;
//         cb(res);
//     }, 3000);
// }

// function taskB(a,cb) {
//     setTimeout(()=>{
//         const res = a * 2;
//         cb(res);
//     }, 1000);
// }

// function taskC(a,cb) {
//     setTimeout(()=>{
//         const res = a * -1;
//         cb(res);
//     }, 2000);
// }

// Callback_Hell
// taskA(3,4,(a_res)=>{
//     console.log("tesk A : ", a_res);
//     taskB(a_res,(b_res)=>{
//         console.log("task_B : ", b_res);
//         taskC(b_res,(c_res)=>{
//             console.log("task_C : ", c_res);
//         });
//     }); 
// });


// Promise() 객체사용 => .then(), .catch() 사용 가능
function taskA(a,b,){
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            const res = a + b;
            resolve(res);
;        }, 3000);

    });
}

function taskB(a) {
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            const res = a * 2;
            resolve(res);
        }, 1000);
    });
}

function taskC(a) {
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            const res = a * -1;
            resolve(res);
        }, 2000);
    });
}

// .then()의 틀린 사용방법
// taskA(5,1).then((a_res)=>{
//     console.log("A Result : ", a_res);
//     taskB(a_res).then((b_res)=>{
//         console.log("B Result : ", b_res);
//         taskC(b_res).then((c_res)=>{
//             console.log("C Result : ", c_res);
//         })
//     })
// })

// .then() chaining
taskA(5, 1).then((a_res)=>{
    console.log("A Result : ", a_res);
    return taskB(a_res);
}).then((b_res)=>{
    console.log("A Result : ", b_res);
    return taskC(b_res);
}).then((c_res)=>{
    console.log("A Result : ", c_res);
    // 콜백 함수 연결~
})
// promise()객체 사용시 =>
// 비동기 처리를 호출하는 코드와 
// 결과를 처리하는 코드르 분리할 수 있다
// = 가독성있고 깔끔한 비동기 처리를 도와준다

 

 

 

 

 

 

 

 

2023. 12.  20 (수)

관련글 더보기