Spring_JWT 서버 실행
react-basic 에서 npm run dev
handleSubmit 함수 추가const handleSubmit = (e) => {
e.preventDefault();
};
onSubmit 이벤트 추가<form onSubmit={handleSubmit} action="/v1/comments" method="post">
onSubmit 이벤트 추가<form onSubmit={handleSubmit} action="/v1/comments" method="post">
handleSubmit 함수 안에 변환 후 DB에 데이터를 보내는 코드 추가form 데이터 를 넣은 경우 num=1&name="kim" 이런식으로query string 문자열 이 전달object 데이터 를 넣은 경우 {"num":1, "name":"kim"} 이런식으로json 문자열 이 전달e.preventDefault();
//submit 이벤트가 발행한 form 의 참조값
const form=e.target;
//폼에 입력한 내용을 FormData 객체로 얻어내서
const formData=new FormData(form);
//Object 로 변환
const obj = Object.fromEntries(formData);
try{
await api.post("/v1/comments", obj);
//댓글 목록을 refresh 한다
onRefresh();
}catch(err){
console.log(err);
}
disabled 상태 관리 코드 추가<button disabled={userInfo.userName ? false : true} type="submit" className="btn btn-success">등록</button>
disabled 상태 관리 코드 추가<textarea id="commentContent" name="content" rows="5" className="form-control"
placeholder={userInfo.userName ?'댓글을 입력하세요' : '댓글 작성을 위해 로그인이 필요합니다'}></textarea>
disabled 상태 관리 추가<textarea name="content" className="form-control mb-2" rows="2"
placeholder={userInfo.userName ?'댓글을 입력하세요' : '댓글 작성을 위해 로그인이 필요합니다'}
disabled={userInfo.userName ? false : true}></textarea>
<button disabled={userInfo.userName ? false : true} type="submit" className="btn btn-sm btn-success">등록</button>
<button disabled={userInfo.userName ? false : true} type="reset" className="btn btn-sm btn-secondary cancel-reply-btn">취소</button>
savecomment 메소드 생성@PostMapping("/comments")
public ResponseEntity<Void> savecomment(@RequestBody CommentDto dto) {
commentService.createComment(dto);
// 204 빈 컨텐츠 응답
return ResponseEntity.noContent().build();
}
handleRefresh 추가const handleRefresh = () => {
// 댓글 목록을 다시 받아온다
api.get(`/v1/board/${num}/comments`)
.then(res => setCommentList(res.data))
.catch(err => console.log(err));
};
Comment 에 onRefresh 함수 사용<Comment category="board"
parentNum={dto.num}
parentWriter={dto.writer}
list={commentList}
onRefresh={handleRefresh}/>
onRefresh props 추가function Comment({category, parentNum, parentWriter, list, onRefresh}) {
handleSubmit 함수 안에 try 문 에 onRefresh 함수 호출onRefresh();
defaultValue 속성 추가<textarea name="content" className="form-control mb-2" rows="2" defaultValue={item.content}></textarea>

onClick 이벤트 추가<button onClick={() => alert("hello electron")}>클릭해 보세요</button>

// import { createRequire } from 'node:module'
// const require = createRequire(import.meta.url)
hello-electron 폴더로 이동cd C:\playground\react\hello-electron
npm run build 코드 작성C:\playground\react\hello-electron>npm run build
createWindow
app.whenReady().then(createWindow)1
app.whenReady 함수 안에 코드 추가app.whenReady().then(() => {
createWindow();
if(!app.isPackaged){
win?.webContents.openDevTools();
}
// 5초 후에 "greet" 이벤트를 발생시키면서 renderer 에 인삿말 전달하기
setTimeout(() => {
win?.webContents.send("greet", "즐거운 하루 시작 React 공부 시작");
}, 5000);
})
type 코드 추가type MemberDto = {
num : number;
name : string;
addr : string;
};
inteface 코드 추가interface Member{
num : number;
name : string;
addr : string;
}
const m1:MemberDto = {num:1, name:"유재석", addr:"압구정"};
console.log(m1);
const m2:Member = {num:2, name:"박명수", addr:"이태원"};
console.log(m2);

useEffect 함수 추가window api 에 greet 이라는 이벤트가 발생하면 실행할 함수 등록useEffect(() => {
window.api.onGreet(()=>{});
}, []);
ApiType 타입 선언 코드 추가interface ApiType{
onGreet:(callback:(msg:string)=>void)=>void
}
declare 코드 추가declare global{
interface Window{
api:ApiType
}
}
➜ onGreet:() => void : object 를 만드는 걸까? 아니면 type 을 선언하는 걸까?

useState 함수 추가const [msg, setMsg] = useState("");
useEffect 함수 안에 setMsg 함수 사용useEffect(() => {
window.api.onGreet((msg)=>{
setMsg(msg);
});
}, []);
<p>main 프로세스에서 전달한 메세지 : <strong>{msg}</strong></p>
"greet" 이벤트에 귀를 기울이기import { ipcRenderer, contextBridge } from 'electron'
// --------- Expose some API to the Renderer process ---------
contextBridge.exposeInMainWorld('api', {
onGreet:(callback:(msg:string)=>void)=>{
console.log("preload.ts 에 onGreet() 함수가 호출됨");
ipcRenderer.on("greet", (_event, msg)=> {
console.log("ipcRenderer 에 greet 이벤트가 발생함");
callback(msg);
});
}
})

main.ts (Main Process)preload.ts (Preload Script)App.tsx (Renderer Process)preload.ts
.exposeInMainWorld("api", {
함수명1 : 함수,
함수명2 : 함수,
.
.
이런 구조로 함수를 계속 추가할 수 있다
});
window.api.함수명1(전달한 데이터);
contextBridge.exposeInMainWorld 함수 안에 toMain 함수 코드 추가"from-react" 라는 이벤트를 발생시키면서 문자열 전달하기toMain:(msg:string)=>{
ipcRenderer.send("from-react", msg);
}
'pretter/pretter':[]
ipcMain 코드 추가"from-react" 이벤트에 귀를 기울이기ipc : 프로세스와의 통신ipcMain.on("from-react", (_event, msg:string)=>{
console.log(msg);
});
➜ App을 실행하는 터미널에 해당 콘솔이 출력되야 한다
<input type="text" placeholder="main 프로세스에 할 말 입력..."/>
<button>전송</button>
handleSend 함수 추가const handleSend = () => {
};
ref 속성 추가<input ref={inputRef} type="text" placeholder="main 프로세스에 할 말 입력..."/>
onClick 이벤트 추가<button onClick={handleSend}>전송</button>
useRef 함수 추가const inputRef = useRef<HTMLInputElement>(null);
toMain 타입 추가ApiType : api 에서 호출할 함수의 모양을 미리 정의해 놓고 밑에서 사용toMain:(msg:string)=>void;
handleSend 함수 안에 입력한 문자열에 대한 처리 코드 추가// 입력한 문자열 읽어오기
const msg = inputRef.current!.value;
// api 에서 제공하는 함수를 호출하면서 전달하기
window.api.toMain(msg);


main(ipcMain) 와 renderer(ipcRenderer)
on ⟹ handle
send ⟹ invoke
➜ invoke 는 Promise 를 리턴

main.ts 에 whenReady 함수 안에 코드 3줄 주석 처리
// if(!app.isPackaged){
// win?.webContents.openDevTools();
// }
browser-window-focus 함수 추가app.on("browser-window-focus", ()=>{
// ctrl + t 를 눌렀을 때 개발 tool 이 열리도록 한다
globalShortcut.register("Control+t", ()=>{
win?.webContents.openDevTools();
})
});
➜ ctrl + t 를 해야 개발 창이
browser-window-blur 함수 추가app.on("browser-window-blur", ()=>{
globalShortcut.unregisterAll();
});
will-quit 함수 추가app.on("will-quit", ()=>{
globalShortcut.unregisterAll();
});
<button>1번 회원의 정보 받아오기</button>
handleClick 함수 추가const handleClick = (num:number) => {
};
onClick 이벤트 추가<button onClick={() => handleClick(1)}>1번 회원의 정보 받아오기</button>
handleClick 함수에 async, await 코드 추가 및 Member 테이블의 값을 가져올 코드 추가getMember 의 type : (num:number) => Promise<Member>const handleClick = async (num:number) => {
// api 의 getMember() 함수는 Promise<Member> 를 리턴하는 함수여야 한다
const m:Member = await window.api.getMember(num);
};
ApiType 함수 안에 getMember 타입 선언getMember:(num:number)=>Promise<Member>;
contextBridge.exposeInMainWorld 함수 안에 getMember 함수 추가.invoke() 함수는 Promise 리턴한다.invoke() 에 대해서 .handle() 로 처리한다getMember:(num:number)=>{
return ipcRenderer.invoke("get-member", num);
}
Member 라는 인터페이스 타입을 선언export interface Member{
num : number;
name : string;
addr : string;
};
whenReady 함수 안에 ipcMain 코드 추가Promise 가 resolve 된다import { Member } from './types/type'
ipcMain.handle("get-member", (_event, num:number)=>{
const m1:Member = {num:num, name:"유재석", addr:"압구정"};
return m1;
});
useState 함수 추가const [dto, setDto] =useState<Member>();
handleClick 함수 안에 setDto 함수 사용setDto(m);
undefined 의 가능성이 존재하기에 ? 를 붙여준다<p>번호 : <strong>{dto?.num}</strong></p>
<p>이름 : <strong>{dto?.name}</strong></p>
<p>주소 : <strong>{dto?.addr}</strong></p>

<button>좋아하는 음식 목록 받아오기</button>
<ul>
</ul>
ApiType 인터페이스 안에 getFoods 타입 선언getFoods:()=>Promise<string[]>;
contextBridge.exposeInMainWorld 함수 안에 getFoods 함수 추가getFoods:() => ipcRenderer.invoke("get-foods")
whenReady 함수 안에 icpMain.handle 함수 추가ipcMain.handle("get-foods", (_event)=>{
const foods:string[] = ["김밥", "떡볶이", "튀김"];
return foods;
});
useState 함수 추가const [foods, setFoods] = useState<string[]>();
handleClick2 함수 추가const handleClick2 = async () => {
const foods = await window.api.getFoods();
setFoods(foods);
}
onClick 이벤트 추가<button onClick={handleClick2}>좋아하는 음식 목록 받아오기</button>
<ul>
{foods?.map((item, i) => <li key={i}>{item}</li>)}
</ul>

playground 폴더 안에 electron_js 폴더 생성
해당 폴더에서 cmd 실행
npm create electron-vite@latest
memo-appProject name: » memo-app
Project template: » React
cd memo-app
npm install
npm run dev


dependencies 안에 코드 추가"@toast-ui/editor": "^3.2.2",
"axios": "^1.11.0",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"classnames": "^2.5.1",
"jwt-decode": "^4.0.0",
"react-bootstrap": "^2.10.10",
"react-redux": "^9.2.0",
"react-router-dom": "^7.8.2",
"redux": "^5.0.1"
npm install
function App() {
return <>
<div className="container-fluid">
</div>
</>
}
main.tsx 에 App.css import 코드 지우기
App.tsx 에 bootstrap 관련 import 코드 추가
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-icons/font/bootstrap-icons.css'
'pretter/pretter':[]
ApiType 인터페이스 및 declare 코드 추가interface ApiType{
}
declare global{
interface Window{
api:ApiType
}
}
<h1>메모장</h1>
<textarea rows={10} className="form-control"></textarea>
<button className="btn btn-success mt-3">저장</button>

saveMemo 라는 함수 타입 추가saveMemo:(content:string)=>void
useRef 함수 추가const textRef = useRef<HTMLTextAreaElement>(null);
handleSave 함수 추가const handleSave = () => {
};
ref 속성 & 버튼에 onClick 이벤트 추가<textarea ref={textRef} rows={10} className="form-control"></textarea>
<button onClick={handleSave} className="btn btn-success mt-3">저장</button>
handleSave 함수 안에 입력한 내용과 관련한 코드 추가// 입력한 내용 읽어오기
const content = textRef.current!.value;
// api 의 함수 호출하면서 전달하기
window.api.saveMemo(content);
api 로 수정 & saveMemo 코드 추가contextBridge.exposeInMainWorld('api', {
saveMemo:(content:string)=>{
// 매개변수로 전달된 저장할 문자열을 main 프로세스에 전달한다
ipcRenderer.send("save-memo", content);
}
})
⭐ window.api.saveMemo() 가 renderer에서 호출되면?
➤ 그 내부에서 content를 main 프로세스에 전달하는 구조
➤ content는 ipcRenderer.send()에 의해 main 프로세스로 전송
icpMain 코드 추가ipcMain.on("save-memo", (_event, content:string)=>{
console.log("save-memo!");
// node.js 에서 __dirname 은 위에 코드에서 얻어낸 현재 실행 경로 이다
console.log(__dirname);
});

fs importimport fs from 'node:fs'
main.ts 에 save-memo 이벤트에 입력한 내용 파일에 저장하는 코드 추가
const filePath = path.join(__dirname, '/files/myMemo.txt');
recursive:true 는 해당 겨올의 폴더가 존재하지 않으면 직접 만든다fs.mkdirSync(path.dirname(filePath), {recursive:true});
.writeFileSync(경로 내용, 인코딩);fs.writeFileSync(filePath, content, "utf-8");
.showMessageBox(알림을 띄울 window 객체, 옵션)dialog.showMessageBox(win!, {
type:"info",
buttons:["확인", "취소"],
title:"알림",
message:"저장 완료"
});

