State (react의 핵심중의 핵심)

- 한글로 상태라는 뜻
- 리액트에서는 Component의 상태
- 리택트 Component에서 변경 가능한 데이터
- 렌더링이나 데이터 흐름에 사용되는 값만 state에 포함시켜야 함!
- State는 JavaScript 객체이다
- 직접 수정할 수 없다 (수정하면 안된다.)

- constructor (생성자)

 

 

 

 

Lifecycle

- 생명주기 라는 뜻
- 리액트 Component의 생명주기
- Component가 계속 존재하는 것이 아니라, 시간의 흐름에 따라 생성되고 업데이트 되다가 사라진다.

 

 

 

State 실습

import React from "react";

const styles = {
    wrapper: {
        margin: 8,
        padding: 8,
        display: "flex",
        flexDirection: "row",
        border: "1px solid grey",
        borderRadius: 16,
    },
    messageText: {
        color: "black",
        fontSize: 16,
    },
};

class Notification extends React.Component {
    constructor(props) {
        super(props);
        
        this.state = {};
    }

    render() {
        return (
            <div style={styles.wrapper}>
                <span style={styles.messageText}>{this.props.message}</span>
            </div>
        );
    }
}

export default Notification;
import React from "react";
import Notification from "./Notification";

const reservedNotifications = [
    {
        message: "안녕하세요 오늘의 일정을 알려드리겠습니다.",
    },
    {
        message: "점심식사 시간입니다.",
    },
    {
        message: "이제 곧 미팅이 시작됩니다.",
    },
];

var timer;

class NotificationList extends React.Component {
    constructor(props) {
        super(props);
        
        // 처음 빈 배열을 넣어 초기화
        this.state = {
            notifications: [],
        };
    }

    // setInterval을 이용하여 1초마다 정해진 작업
    // 미리 만들어둔 알림 데이터 배열 reservedNodification 로 부터
    // 알림 데이터를 하나씩 가져와서 state에 있는 notifications 배열에 넣고 업데이트
    componentDidMount() {
        const { notifications } = this.state;
        timer = setInterval(() => {
            if (notifications.length < reservedNotifications.length) {
                const index = notifications.length;
                notifications.push(reservedNotifications[index]);
                this.setState({ // state 업데이트를 위해 setState
                    notifications: notifications,
                });
            } else {
                clearInterval(timer);
            }
        }, 1000);
    }

    render() {
        return (
            <div>
                {this.state.notifications.map((notification) => {
                    return <Notification message={notification.message} />;
                })}
            </div>
        );
    }
}

export default NotificationList;

 

constructor는 일종의 생성자라고 보면 된다. 먼저 state()로 초기화 한 후, setInterval을 통해 배열에 원하는 데이터값을 하나씩 넣으면서 setState()로 업데이트를 한다. state()로는 업데이트를 하면 안된다.

 

 

 

1초에 하나씩 메세지가 생기는 걸 확인할 수 있다.

 

 

 

 

Lifecycle 실습

class Notification extends React.Component {
    constructor(props) {
        super(props);
        
        this.state = {};
    }

    componentDidMount() {
        console.log(`${this.props.id} componentDidMount() called.`);
    }

    componentDidUpdate() {
        console.log(`${this.props.id} componentDidUpdate() called.`);
    }

    componentWillUnmount() {
        console.log(`${this.props.id} componentWillUnmount() called.`);
    }

    render() {
        return (
            <div style={styles.wrapper}>
                <span style={styles.messageText}>{this.props.message}</span>
            </div>
        );
    }
}

생명 주기를 확인할 수 있는 함수를 작성한다. componentDidmount()는 마운트 되었을 시, componentDidUpdate()는 컴포넌트가 업데이트 되었을 시, componentWillUnmount()는 컴포넌트가 언마운트 되었을 시에 실행된다.

 

 

 

 

const reservedNotifications = [
    {
        id: 1,
        message: "안녕하세요 오늘의 일정을 알려드리겠습니다.",
    },
    {
        id: 2,
        message: "점심식사 시간입니다.",
    },
    {
        id: 3,
        message: "이제 곧 미팅이 시작됩니다.",
    },
];


/ .... /


    render() {
        return (
            <div>
                {this.state.notifications.map((notification) => {
                    return <Notification 
                            key={notification.id}
                            id={notification.id}
                            message={notification.message} />;
                })}
            </div>
        );
    }

헷깔리지 않도록 메세지에 id 값을 주고, 렌더링 시, Notification에 키와 아이디값을 요소에 설정한다.

 

 

그럼 다음과 같이 터미널의 콘솔창에 나타난다. 첫번째 메세지가 마운트되고, 2번째 메세지가 마운트되어 나타나면 첫번째 메세지도 업데이트 된다. 3번째 메세지가 마운트되어 나타나면, 1,2번 메세지 역시 업데이트된다.

 

 

 

 

    componentDidMount() {
        const { notifications } = this.state;
        timer = setInterval(() => {
            if (notifications.length < reservedNotifications.length) {
                const index = notifications.length;
                notifications.push(reservedNotifications[index]);
                this.setState({ // state 업데이트를 위해 setState
                    notifications: notifications,
                });
            } else {
                this.setState({
                    notifications: [],
                });
                clearInterval(timer);
            }
        }, 1000);
    }

but 컴포넌트가 언마운트되었을 시, componentWillUnmount() 함수를 실행하는데, 위의 작업은 componentWillUnmount()가 실행되지 않았다. 그 이유는 언마운트 된 컴포넌트가 없기 때문이다. 그래서 모든 업데이트가 끝난 후, 부분에 setState() 로 notifications 배열을 빈 배열로 만든다.

 

 

 

모든 작업 이후, notifications 배열이 빈 배열로 바뀌었기 때문에, 1,2,3번 메세지는 언마운트된다. 콘솔창에서 확인할 수 있다.

'React' 카테고리의 다른 글

[React] Hooks 실습  (0) 2024.08.07
[React] Hooks  (0) 2024.08.06
[React] Components 와 Props  (0) 2024.08.01
[React] Rendering Elements  (0) 2024.07.31
[React] JSX 의 장점과 사용법  (0) 2024.07.30

Components

작은 컴포넌트들이 모여서 하나의 컴포넌트를 구성하고 이런 컴포넌트들로 전체 페이지가 구성됨
Component가 붕어빵 틀이라면 Element는 붕어빵. 자바의 객체와도 같음

 

Props

prop은 리액트 Component의 property(속성)
붕어빵에 들어가는 재료라고 보면 됨

Props 의 특징
읽기 전용 - 값을 변경할 수 없다. (붕어빵 다 구워졌으니 속재료 변경 불가)
같은 Props에 대해서는 항상 같은 결과를 보여줌

Props 사용법
JSX 사용경우 (권장)

 


JSX 사용하지 않을 경우

 

 

 

Component 만들기

 

 

class component : React.Component를 상속받음

 


Component의 이름 짓는 법
- Component의 이름은 항상 대문자로 시작해야 한다!

Component 렌더링

 


Component 합성
- Component 안에 또 다른 Component


Component 추출
- 큰 Component 를 일부를 추출해서 새로운 Component를 만듦
- 재사용성 높아짐
- 개발속도 향상

 

 

실습 : 댓글 컴포넌트 만들기

import React from "react";

function Comment(props) {
    return (
        <div>
            <h1>제가 만든 첫 컴포넌트</h1>
        </div>
    );
}

export default Comment;
import React from "react";
import Comment from "./Comment";

function CommentList(props) {
    return (
        <div>
            <Comment />
        </div>
    );
}

export default CommentList;
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

import CommentList from './chapter_05/CommentList';

setInterval(() => {
  ReactDOM.render(
    <React.StrictMode>
    <CommentList />
    </React.StrictMode>,
    document.getElementById('root')
  );
}, 1000);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

댓글 리스트 컴포넌트 안에 댓글 컴포넌트가 포함되어 있는 형태이다.

 

 

 

컴포넌트에 스타일 입히기

import React from "react";

const styles = {
    wrapper: {
        margin: 8,
        padding: 8,
        display: "flex",
        flexDirection: "row",
        border: "1px solid grey",
        borderRadius: 16,
    },
    imageContainer: {},
    image : {
        width: 50,
        height: 50,
        borderRadius: 25,
    },
    contentContainer: {
        marginLeft: 8,
        display: "flex",
        flexDirection: "column",
        justifyContent: "center",
    },
    nameText: {
        color: "black",
        fontSize: 16,
        fontWeight: "bold",
    },
    commentText: {
        color: "black",
        fontSize: 16,
    },
};

function Comment(props) {
    return (
        <div style={styles.wrapper}>
            <div style={styles.imageContainer}>
                <img 
                    src="https://lh3.googleusercontent.com/proxy/fKYexLyW7D6DMKEsN_TZbTFFgn95uCZDwkBW8-YSORGtmw9KrzayhocMeyh__7sl0rW9XjR0Qlp2gC5-Txx9IXPzsKZxvdoXB9OCROAg4tzP_XBvrA"
                    style={styles.image} 
                />
            </div>

            <div style={styles.contentContainer}>
                <span style={styles.nameText}>김이박</span>
                <span style={styles.commentText}>
                    제가 쓴 첫 댓글
                </span>
            </div>
        </div>
    );
}

export default Comment;

 

다음과 같이 Comment.jsx를 수정한다. 

 

 

 

그럼 다음과 같이 댓글에 스타일이 적용된 것을 확인할 수 있다.

 

 

 

 

컴포넌트에 Props 추가하기

            <div style={styles.contentContainer}>
                <span style={styles.nameText}>{props.name}</span>
                <span style={styles.commentText}>
                    {props.comment}
                </span>
            </div>

 

우선 위에서 수정한 Comment 컴포넌트에서 닉네임과 댓글 부분을 다음과 같이 {props.name} 그리고 {props.comment}로 수정한다.

 

 

import React from "react";
import Comment from "./Comment";

function CommentList(props) {
    return (
        <div>
            <Comment name={"야야야"} comment={"hello world!"} />
            <Comment name={"좋아요"} comment={"ㅋㅋㅋㅋㅋㅋ"} />
        </div>
    );
}

export default CommentList;

 

CommentList 컴포넌트로 가서 Comment 컴포넌트들의 요소를 다음과 같이 설정하면...

 

올바르게 값들이 불려진 걸 확인할 수 있다.

 

 

 

Comment 데이터를 별도의 객체로 분리하기

import React from "react";
import Comment from "./Comment";

const comments = [
    {
        name: "손흥민",
        comment: "안녕하세요, 손흥민입니다.",
    },
    {
        name: "이강인",
        comment: "이강인입니다. 반갑습니다.",
    },
    {
        name: "김민재",
        comment: "제 이름은 김민재입니다. 반갑습니다.",
    },
];

function CommentList(props) {
    return (
        <div>
            {comments.map((comment) => {
                return (
                    <Comment name={comment.name} comment={comment.comment} />
                );
            })}
        </div>
    );
}

export default CommentList;

CommentList.jsx 에서 각 댓글 객체를 작성하고, 컴포넌트에서 map으로 각 댓글 객체 조회 후, Comment 컴포넌트 소환 후 각 요소들에 데이터들을 집어 넣는다.

 

 

댓글 객체의 데이터들이 불려져왔다.

'React' 카테고리의 다른 글

[React] Hooks  (0) 2024.08.06
[React] State and Lifecycle  (0) 2024.08.03
[React] Rendering Elements  (0) 2024.07.31
[React] JSX 의 장점과 사용법  (0) 2024.07.30
[React] create-react-app  (0) 2024.07.29

Elements 란?

어떤 물체를 구성하는 성분
리액트 앱을 구성하는 가장 작은 블록들

 

 

생김새

리액트 Elements는 자바스크립트 객체 형태로 존재
마음대로 생성되면 바꿀 수 없는 불변성

타입이 문자열일 경우

 

 

타입이 문자열이 아닐 경우

 

 

 

Element를 만드는 방식, type은 태그 이름, props가 속성, children이 자식 태그

 

 

 

 

Button Element가 ConfirmDialog Element에 속해 있다.

 

 

 

특징

불변성 - Elements 생성 후에는 children이나 attributes를 바꿀 수 없다!

 

 

 

렌더링하기

Root DOM Node

<div id="root"></div>


VirtureDOM에서 실제 DOM으로 랜더링되는 과정

const element = <h1>안녕, 리액트!</h1>;
ReactDOM.render(element, document.getElementById('root'));

 

 

 

실습 - 시계만들기

import React from "react";

function Clock(props) {
    return (
        <div>
            <h1>안녕, 리액트!!!!</h1>
            <h2>지금 시간 : {new Date().toLocaleTimeString()}</h2>
        </div>
    );
}

export default Clock;

 

다음과 같이 시계 엘레멘트를 만든다.

 

 

 

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

import Clock from './chapter_04/Clock';

setInterval(() => {
  ReactDOM.render(
    <React.StrictMode>
    <Clock />
    </React.StrictMode>,
    document.getElementById('root')
  );
}, 1000);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

index.js로 가서 setInterval() 함수를 사용하여 1초간 시간을 갱신하게끔 한다.

 

 

 

터미널에서 npm start를 치면 브라우저가 열러서 시계가 작동되는 것을 확인할 수 있다.

'React' 카테고리의 다른 글

[React] State and Lifecycle  (0) 2024.08.03
[React] Components 와 Props  (0) 2024.08.01
[React] JSX 의 장점과 사용법  (0) 2024.07.30
[React] create-react-app  (0) 2024.07.29
[React] React 란? & 연동하기  (0) 2024.07.29

'코딩 문제(JavaScript)' 카테고리의 다른 글

푼 문제 240226 (JAVA)  (0) 2024.08.28
240801~16 푼 문제  (0) 2024.08.17
240716~18 푼 문제  (0) 2024.07.16
240708~09 푼 문제  (0) 2024.07.09
240423~30 푼 문제  (0) 2024.04.23

JSX란?

자바스크립트(JS) 에 XML / HTML 기능을 추가한 것으로 A syntax extension to JavaScript (자바스크립트 확장 문법) 이라고 한다. 사실 JSX는 필수가 아니다. createElement() 라는 함수를 이용해서 구현을 할 수 있으나, 그래도 후술할 장점들 때문에 JSX가 많이 권장되는 편이다.

 

JSX 장점

- 간결한 코드

- 가독성 향상

- 버그 발견 쉬움

- Injection Attacks 해킹 방어

 

사용법

- 모든 자바스크립트 문법 사용 가능
- 태그의 속성에 값을 넣기 위해 큰 따옴표 사이에 문자열 넣거나
- 중괄호 사이에 자바스크립트 코드를 넣으면 됨!
- childeren 정의하는 법은 그냥 태그 안에 태그를 또 넣으면 됨

 

 

우선 지난 포스팅에서 create-react-app을 통해 만들어 놓은 애플리케이션 틀이 있을 것이다. 다음과 같이 파일과 폴더를 만든다.

 

 

import React from "react";

function Book(props) {
    return (
        <div>
            <h1>{`이 책의 이름은 ${props.name}입니다.`}</h1>
            <h2>{`이 책은 총 ${props.numOfPage}페이지로 이뤄져 있습니다.`}</h2>
        </div>
    );
}

export default Book;

Book.jsx

 

import React from "react";
import Book from "./Book";

function Library(props) {
    return (
        <div>
            <Book name="책이름 파이썬" numOfPage={300} />
            <Book name="책이름 AWS" numOfPage={400} />
            <Book name="책이름 리액트" numOfPage={500} />
        </div>
    );
}

export default Library;

Library.jsx

 

다음과 같이 Book 에 속한 태그들을 Library에서 설정한 것처럼 나열한다.

 

 

 

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

import Library from './chapter_03/Library';

ReactDOM.render(
  <React.StrictMode>
    <Library />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

index.js에 와서 chapter 폴더 속 Library.jsx를 불러와서 ReactDOM에 랜더링해서 화면에 보여주게 한다.

터미널에서 npm start를 치면 브라우저가 열리고 localhost:3000 주소로 페이지를 보여준다.

(단 주의해야 할 점이 터미널에서 npm start 명령어를 칠 경우 package-json 파일이 있는 폴더의 상위 페이지에서 입력해야 한다. 만약 다른 경로에 있을 경우 cd 명령어로 경로를 설정해주면 된다.)

 

 

랜더링한 화면을 볼 수 있다.

'React' 카테고리의 다른 글

[React] State and Lifecycle  (0) 2024.08.03
[React] Components 와 Props  (0) 2024.08.01
[React] Rendering Elements  (0) 2024.07.31
[React] create-react-app  (0) 2024.07.29
[React] React 란? & 연동하기  (0) 2024.07.29

create-react-app

초기 환경을 설정해놓지 않고도, 리액트 프로젝트를 실행할 수 있도록 셋업을 완료해놓은 틀

 

 

터미널에 가서 리액트 애플리캐이션을 생성하기 위해 아래 처럼 명령어를 친다.

PS C:\Users\user\Documents\코리아IT\react> npx create-react-app my-app

뒤에 my-app은 내가 이름지은 프로젝트 명이다. 만약 프로젝트 명이 my-blog 라면 my-blog를 쓴다.

 

 

PS C:\Users\user\Documents\코리아IT\react> npm uninstall -g create-react-app

만약 안된다면 다음 명령어를 터미널에 입력 후, 다시 위의 명령어를 입력한다.

 

 

  cd my-app
  npm start

cd 명령어로 경로 변경 후 그리고 리액트 애플리케이션 실행을 위해 npm start를 치면

 

 

 

로컬호스트 3000 주소로 다음과 같은 창이 뜬다.

'React' 카테고리의 다른 글

[React] State and Lifecycle  (0) 2024.08.03
[React] Components 와 Props  (0) 2024.08.01
[React] Rendering Elements  (0) 2024.07.31
[React] JSX 의 장점과 사용법  (0) 2024.07.30
[React] React 란? & 연동하기  (0) 2024.07.29

React 란??

메타에서 개발한 오픈소스 자바스크립트 라이브러리이다. 랜더링이 잦은 동적인 모던 웹에서 빠른 퍼포먼스를 낼 수 있도록 한다.

 

 

 

연동

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>내 블로그</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>내 블로그에 오신 분들 환영합니다.</h1>

    <div id="root"></div>

    <!-- 리액트 가져오기 -->
    <script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
    
    <!-- 리액트 컴포넌트 가져오기-->
    <script src="MyButton.js"></script>
</body>
</html>

 

우선 HTML에 다음과 같이 작성한다. root div 태그는 domContainer 역할을 한다. 리액트를 가져오고 리액트 컴포넌트를 가져온다.

 

 

 

// 리액트 컴포넌트
// 간단한 리액트의 함수 컴포넌트
function MyButton(props) {
    const [isClicked, setIsClicked] = React.useState(false);

    return React.createElement(
        'button',
        { onClick: () => setIsClicked(true) },
        isClicked ? 'Clicked!' : 'Click here!'
    )
}

// 리액트 컴포넌트를 DOM 컴포넌트에 랜더링, 컴포넌트를 화면에 보여지게 함
const domContainer = document.querySelector('#root');
ReactDOM.render(React.createElement(MyButton), domContainer);

 

자바스크립트로 작성한 리액트 컴포넌트이다. 간단하게 작성 후, DOM 컴포넌트에 랜더링해서 화면에 보여지게 할 것이다.

 

 

DOM 컴포넌트에 랜더링 하면 다음과 같은 버튼이 생기는데, 클릭하면 'Clicked!' 로 버튼 이름이 변한다.

'React' 카테고리의 다른 글

[React] State and Lifecycle  (0) 2024.08.03
[React] Components 와 Props  (0) 2024.08.01
[React] Rendering Elements  (0) 2024.07.31
[React] JSX 의 장점과 사용법  (0) 2024.07.30
[React] create-react-app  (0) 2024.07.29

'코딩 문제(JavaScript)' 카테고리의 다른 글

240801~16 푼 문제  (0) 2024.08.17
240729~31 푼 문제  (0) 2024.07.31
240708~09 푼 문제  (0) 2024.07.09
240423~30 푼 문제  (0) 2024.04.23
240422 푼 문제 (JavaScript)  (0) 2024.04.22

이상형 월드컵, 굿즈 판매 개인 프로젝트를 진행하는 도중, 굿즈 결제 부분은 포트원 카카오페이 API를 활용하기로 했다.

 

https://portone.io/korea/ko

 

포트원 | 온라인 비즈니스 성장을 돕는 기업

포트원이 제공하는 단 한 줄의 코드로 세상의 모든 결제를 손쉽게 연동해보세요. PG사 통합결제 연동, 해외결제, 파트너 정산 관리, 결제 애널리틱스, 수수료 혜택까지, 포트원의 맞춤 컨설팅을

portone.io

우선 포트원 사이트에 가서 회원가입을 하고 로그인을 한다.

 

 

연동 정보에서 식별코드 탭에 가면 본인의 식별코드, 시크릿 키 등을 확인할 수 있다. 이 코드들은 유출되지 않도록 주의한다.

 

 

 

그리고 채널 관리 탭에 가서 사용할 결제 대행사를 선택 후, 등록을 한다. 단순 테스트를 하고 싶으면 테스트, 실제로 연동을 하고 싶으면 실연동으로. 내 프로젝트는 그저 포트폴리오용이니까 당연히 테스트로

 

 

 

 

<!-- jQuery CDN 추가 -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<!-- 아임포트 스크립트 추가 -->
<script type="text/javascript" src="https://cdn.iamport.kr/js/iamport.payment-1.2.0.js"></script>

 

해당 API를 사용할 HTML의 헤드 부분에 추가한다.

 

 

 

 

 

<input class="purchase-button" onclick="handlePayment('kakaopay', 'card')" value="구매하기" type="button">
// 아임포트 코드
var impCode = '본인 아임포트 코드';

function handlePayment(pg, payMethod) {
    const checkAgree = document.querySelector('.check-agree');
    if (!checkAgree.checked) {
        checkAgree.nextElementSibling.style.color = 'red';
        return;
    }
    const url = new URL(window.location.href);  // url 객체

    const urlParams = url.searchParams;         // 해당 주소에서 파라미터들 가져오기
    const merchantUid = urlParams.get("index"); // 주문 고유 index

    console.log("handlePayment");
    console.log(pg);
    console.log(payMethod);


    // 결제하기 버튼 클릭 시 결제 요청
    IMP.init(impCode);
    IMP.request_pay({
        pg: pg,
        pay_method: payMethod,
        merchant_uid: merchantUid, // 주문번호 생성
        name: document.querySelector('.product-name').value,        // 제품 이름
        amount: document.querySelector('.price').value,             // 결제 가격
        buyer_name: document.querySelector('.buyerName').value      // 주문한 사람
    }, function(rsp) {
        if (rsp.success) {
            // 결제 성공 시
            console.log(rsp.imp_uid);
            $.ajax({
                type: 'POST',
                url: '/api/v1/payment/validation/' + rsp.imp_uid
            }).done(function(data) {
                console.log(data);
                // 결제 금액 일치. 결제 성공 처리
                $.ajax({
                    url: "/api/v1/payment/check/" + merchantUid,
                    method: "post",
                    contentType: "application/json"
                }).then(function(res) {
                    // saveOrder(merchantUid);
                    alert("[결제 완료] 제품을 기다려주십시오.");
                    location.href = '/store/';
                }).catch(function(error) {
                    // 금액이 맞지 않아 결제 실패시 환불처리 및 주문취소
                    $.ajax({
                        url: "/api/v1/payment/cancel/" + rsp.imp_uid,
                        method: "post",
                        contentType: "application/json"
                    }).done(function(data) {
                        cancelOrder('no');
                    }).catch(function(error) {
                        alert('환불에 실패하였습니다.');
                    });
                });
            }).catch(function(error) {
                alert('결제에 실패하였습니다. ' + rsp.error_msg);
            });
        } else {
            alert(rsp.error_msg);
        }
    });
}

 

자바스크립트 로직. 일단 결제를 성공한 후, 만약 유저가 악의적으로 HTML로 금액을 조작한 후, 결제한 것이라면 바로 결제가 취소되고 주문이 취소되도록 하였다.

 

 

 

 

<repositories>
    <repository>
        <id>jitpack.io</id>
        <url>https://jitpack.io</url>
    </repository>
</repositories>
<dependency>
    <groupId>com.github.iamport</groupId>
    <artifactId>iamport-rest-client-java</artifactId>
    <version>0.2.22</version>
</dependency>

 

자바에서 아임포트 클라이언트를 활용하기 위해 pom.xml에 레포시토리와 의존성을 추가하였다.

 

 

 

 

#iamport
imp.code=본인 아임포트 코드
imp.api.key=본인 api 키
imp.api.secretkey=본인 시크릿키

 

애플리케이션 프로퍼티로 가서 위에서 발급받은 본인의 아임포트 코드, api 키, 시크릿키를 작성한다. 자바에서 그대로 사용하면 유출의 위험이 있기 때문이다.

 

 

 

@Service
@Slf4j
@RequiredArgsConstructor
public class PaymentService {
    private final AdminMapper adminMapper;
    private final StoreMapper storeMapper;
    private IamportClient iamportClient;

    @Value("${imp.api.key}")
    private String apiKey;

    @Value("${imp.api.secretkey}")
    private String secretKey;

    @PostConstruct
    public void init() {
        this.iamportClient = new IamportClient(apiKey, secretKey);
    }

    /**
     * 아임포트 서버로부터 결제 정보를 검증
     * @param imp_uid
     */
    public IamportResponse<Payment> validateIamport(String imp_uid, HttpSession session) {
        try {
            IamportResponse<Payment> payment = iamportClient.paymentByImpUid(imp_uid);
            log.info("결제 요청 응답. 결제 내역 - 주문 번호: {}", payment.getResponse());

            // 내가 결제한 정보들을 객체에 집어넣기
            GoodsOrderDto userOrder = new GoodsOrderDto();
            userOrder.setIndex(Integer.parseInt(payment.getResponse().getMerchantUid()));
            userOrder.setTitle(payment.getResponse().getName());
            userOrder.setUserEmail(payment.getResponse().getBuyerName());
            userOrder.setPrice(payment.getResponse().getAmount().intValue());
            session.setAttribute("userOrder", userOrder);   // 세션에 등록
            return payment;
        } catch (Exception e) {
            log.info(e.getMessage());
            return null;
        }
    }

    /**
     * 아임포트 서버로부터 결제 취소 요청
     *
     * @param imp_uid
     * @return
     */
    public IamportResponse<Payment> cancelPayment(String imp_uid) {
        try {
            CancelData cancelData = new CancelData(imp_uid, true);
            IamportResponse<Payment> payment = iamportClient.cancelPaymentByImpUid(cancelData);
            return payment;
        } catch (Exception e) {
            log.info(e.getMessage());
            return null;
        }
    }

    /**
     * 금액 맞는지 아닌지 체크
     */
    public Result check(int goodsOrderId, HttpSession session){
        GoodsOrderDto dbGoodsOrder = this.adminMapper.selectGoodsOrderByIndex(goodsOrderId);    // DB에 있는 값
        GoodsOrderDto userGoodsOrder = (GoodsOrderDto) session.getAttribute("userOrder");       // 유저 결제 내용
        UserEntity user = (UserEntity) session.getAttribute("user");                            // 유저
        if (dbGoodsOrder.getPrice() - dbGoodsOrder.getPrice() * dbGoodsOrder.getDiscount()/100 != userGoodsOrder.getPrice()) {
            System.out.println("가격 문제");
            return CommonResult.FAILURE;
        }
        if (goodsOrderId != userGoodsOrder.getIndex()) {
            System.out.println("인덱스 문제");
            return CommonResult.FAILURE;
        }
        if (!dbGoodsOrder.getTitle().equals(userGoodsOrder.getTitle())) {
            System.out.println("굿즈 이름 문제");
            return CommonResult.FAILURE;
        }
        if (!dbGoodsOrder.getUserEmail().equals(user.getEmail())) {
            System.out.println("유저 이름 문제");
            return CommonResult.FAILURE;
        }
        if (dbGoodsOrder.isPaid()) {
            System.out.println("이미 결제");
            return CommonResult.FAILURE;
        }
        dbGoodsOrder.setPaid(true);
        this.storeMapper.updateGoodsOrder(dbGoodsOrder);
        return CommonResult.SUCCESS;
    }

}
@RestController
@RequestMapping("/api/v1/payment")
@RequiredArgsConstructor
@Slf4j
public class PaymentController {
    private final PaymentService paymentService;

    // 결제하기
    @PostMapping("/validation/{imp_uid}")
    public IamportResponse<Payment> validateIamport(
            @PathVariable String imp_uid,
            HttpSession session
    ) throws IamportResponseException, IOException {
        log.info("imp_uid: {}", imp_uid);
        log.info("validateIamport");
        return paymentService.validateIamport(imp_uid, session);
    }

    // 금액 맞는지 체크
    @PostMapping("/check/{goodsOrderId}")
    public ResponseEntity<String> processOrder(
            @PathVariable int goodsOrderId,
            Exception ex,
            HttpSession session
    ) {
        Result result = this.paymentService.check(goodsOrderId, session);
        if (result == CommonResult.FAILURE) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
        }
        return ResponseEntity.ok("성공");
    }

    // 결제 취소(환불)
    @PostMapping("/cancel/{imp_uid}")
    public IamportResponse<Payment> cancelPayment(@PathVariable String imp_uid) throws IamportResponseException, IOException {
        return paymentService.cancelPayment(imp_uid);
    }
}

 

전체 서비스와 컨트롤러. 우선 해당 결제을 식별하는 주문 번호를 카테고리로 가져와서 결제를 진행한다. 그러면 결제 완료.

 

하지만 결제 부분만 완성한다면, 어떤 유저가 악의적으로 금액을 수정해서 결제하는 것을 막을 수 없다. 특히 내 프로젝트는 장바구니 시스템이 곧 주문이다. 그러므로 상대적으로 금액이 낮은 제품을 우선 주문하고, 그 후 금액 높은 제품을 다시 주문하여 그 제품의 정보들을 이미 등록한 금액 낮은 제품의 정보로 등록하여 결제하는 편법을 쓸 수 있다.

 

그래서 우선 결제 부분에서 내가 주문한 정보들을 세션에 집어넣고, 검토 부분에서 내 주문의 정보들과 DB에 등록된 주문 정보가 맞는지 확인하는 컨트롤러 작성 후, ResponseEntity 방식으로 금액이 맞다면 ok, 틀리다면 일부러 오류를 내서 환불처리 POST를 진행하도록 한다. 위와 같은 편법을 쓸 경우 금액 높은 제품이 아닌 편법으로 등록해놓았던 금액 낮은 제품이 대신 결제완료 된다.

 

 

 

 

 

 

시연 결과 제대로 작동하는 것을 확인하였다.

 

약간 아쉬운 부분은, 포트원 api의 구현방식을 염두해 두지 않고 주문 테이블을 만들었기에, 고유 주문번호를 랜덤 숫자문자 16자리가 아닌 그냥 인덱스 식별값으로 집어넣은 점이 아쉽고, 이미 주문하기 부분을 구현해 놓은 상태였기에 포트원 api에서 권장하는 주문하기 기능을 활용하지 못한 점이 아쉽다.

 

포트원의 KG 이니시스 방식도 이 처럼 구현하는 방식이 같기 때문에 나중에 똑같은 방식으로 구현하면 좋을 것 같다.

'API' 카테고리의 다른 글

[API] 카카오 로그인 API 구현  (0) 2024.05.13
[API] 카카오 지도 API 추가하기  (0) 2024.03.25
[API] 다음 주소 찾기 API  (0) 2024.01.24

'코딩 문제(JavaScript)' 카테고리의 다른 글

240801~16 푼 문제  (0) 2024.08.17
240729~31 푼 문제  (0) 2024.07.31
240716~18 푼 문제  (0) 2024.07.16
240423~30 푼 문제  (0) 2024.04.23
240422 푼 문제 (JavaScript)  (0) 2024.04.22

+ Recent posts