Framework/React,Next.js

[토이프로젝트]대출이자계산기 만들어보기 - 2편 : Next.js로 웹 서비스 개발하기

  • -
반응형

웹 서비스 개발 마지막 장입니다.

오늘은 카카오톡 공유하기를 통해 계산 결과를 공유해보겠습니다.


2-5. 계산 결과 공유하기(with 카카오톡 공유하기)

카카오 개발자 설정

카카오 개발자센터에 접속해서 애플리케이션을 추가하겠습니다.

 

 

 

앱 키의 JavaScript 키가 카카오톡 공유하기 기능에 사용됩니다.

 

플랫폼 탭에서 Web 플랫폼을 등록합니다. 

두 개의 주소를 등록하는데 로컬(http://localhost:3000)과 프로덕션 주소(https://calculator.codedream.co.kr)입니다. 

 

 

카카오 SDK 추가

환경 변수를 관리하는 .env.local 파일을 추가합니다.

 

앞서 확인한 JavaScript 키를 추가합니다.

.env.local

NEXT_PUBLIC_KAKAO_KEY=b2f44b1ec8a7e244edf9da39f09a3090

.env.local 파일을 gitignore에 자동으로 등록되기 때문에 외부로 노출될 가능성이 없습니다.

키 값은 꼭 NEXT_PUBLIC_ 을 앞에 붙여 만들어주어야 합니다. 서버 컴포넌트 방식에서 env 변수 접근은 가능하지만 클라이언트 방식에서 env 변수에 접근하기 위해서는 NEXT_PUBLIC_ 접두사가 반드시 필요합니다.

 

 

app/layout.tsx

import type { Metadata } from "next";
import "./globals.css";
import "./font.css";
import Script from "next/script";

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

declare global {
  interface Window {
    Kakao: any;
  }
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ko">
      <body>
        {children}

        <div id="modal"></div>
      </body>
      <Script
        src="https://t1.kakaocdn.net/kakao_js_sdk/2.7.1/kakao.min.js"
        integrity="sha384-kDljxUXHaJ9xAb2AzRd59KxjrFjzHa5TAoFQ6GbYTCAG0bjM55XohjjDT7tDDC01"
        crossOrigin="anonymous"
        async
      />
    </html>
  );
}

 

 

공유 페이지 추가

 

계산 결과를 팝업을 통해 확인했습니다.

팝업에 있는 계산 결과를 어떻게 사용자에게 보여줄 수 있을까요? 팝업 형태로 보여줘야 한다면 아마 복잡한 작업들을 거쳐야할 것 같습니다. 사용자는 공유된 주소를 통해 접속하기 때문입니다.

 

현재 프로젝트에서 계산된 결과가 팝업에 조회될 때 주소는 변동되지 않는 것을 확인할 수 있습니다. 그래서 메인 주소 전달시에는 계산 결과를 확인하기 어려울 것입니다. 

 

 

계산 결과를 확인할 주소와 주소에 매핑되는 페이지가 필요합니다. 미리 만들어둔 그리드 컴포넌트는 입력값들을 props로 전달받고 있어 페이지를 추가하고 그리드 컴포넌트로 값만 넘겨주면 계산 결과를 출력할 화면을 쉽게 만들 수 있을 것 같습니다.

 

주소에는 쿼리 파라미터로 입력 값들을 전달받을 수 있게 만들 수 있을 것 같습니다. (/share?상환방식=A&대출원금=B&대출기간=C&연이자율=D)

 

app/share, app/share/form 디렉토리를 추가하겠습니다. 그리고 share/page.tsx 파일을 추가하고 share/form/form.tsx 파일을 추가합니다.

여기서 page.tsx는 서버 컴포넌트로 동작해야 합니다.

 

app/share/page.tsx

import ShareForm from "./form/form";

export default function Share() {
  return (
    <main>
      <div className="container" style={{ maxWidth: "720px" }}>
        <h1>계산 결과 공유하기</h1>

        <ShareForm />
      </div>
    </main>
  );
}

 

공유 페이지 안에 폼 컴포넌트를 클라이언트 방식으로 관리하고 useSearchParams() 함수를 통해 쿼리 파라미터를 전달받습니다. 파라미터는 searchParams.get("키값")과 같은 형태로 접근할 수 있습니다.

 

공식문서에 따르면 useSearchParams()를 사용하는 클라이언트 컴포넌트는 Suspense를 래핑하라고 안내합니다. 그렇지 않은 경우 빌드시 오류가 발생합니다.(더보기 참고)

 

app/share/form/form.tsx

"use client";

import MyGrid from "@/app/components/grid/grid";
import { useSearchParams } from "next/navigation";
import { Suspense } from "react";

function Query() {
  const searchParams = useSearchParams();

  return (
    <>
      <MyGrid
        params={{
          paymentMethod: searchParams.get("paymentMethod")!,
          amount: searchParams.get("amount")!,
          period: searchParams.get("period")!,
          interest: searchParams.get("interest")!,
        }}
      />
    </>
  );
}

export default function ShareForm() {
  return (
    <Suspense>
      <Query />
    </Suspense>
  );
}

 

여기까지 작업 결과를 확인해보겠습니다.

프로젝트 실행 후 아래 주소를 통해 접근해보겠습니다.

http://localhost:3000/share?paymentMethod=1&amount=100000000&period=1&interest=4.5

 

계산 결과가 조회되는 페이지가 추가되었습니다.

 

 

공유하기 컴포넌트 추가

계산 결과 페이지를 보여주기 위해 공유하기 컴포넌트를 만들어보겠습니다.

app/components/share 디렉토리를 추가합니다. share.tsx/share.module.scss 파일을 추가하겠습니다.

 

아래 카카오 이미지를 다운받아 public 경로에 추가해 주시고, 공유하기시 사용될 이미지도 추가해 주세요. 

kakaotalk_sharing_btn_medium.png
0.00MB

 

 

app/components/share/share.tsx

"use client";

import { useEffect } from "react";
import Image from "next/image";
import styles from "./share.module.scss";

type Props = {
  paymentMethod: string;
  amount: string;
  period: string;
  interest: string;
};

export default function MyShare({ paymentMethod, amount, period, interest }: Props) {
  useEffect(() => {
    if (window.Kakao) {
      const { Kakao } = window;

      if (!Kakao.isInitialized()) {
        Kakao.init(`${process.env.NEXT_PUBLIC_KAKAO_KEY}`);
      }

      const redirectUrl = `${window.location.origin}/share?paymentMethod=${paymentMethod}&amount=${amount}&period=${period}&interest=${interest}`;
      let methodName = "";

      switch (paymentMethod) {
        case "1":
          methodName = "원리금 균등 상환";
          break;
        case "2":
          methodName = "원금 균등 상환";
          break;
        case "3":
          methodName = "만기일시 상환";
          break;
      }

      Kakao.Share.createDefaultButton({
        container: "#kakao-link-btn",
        objectType: "feed",
        content: {
          title: "my-calculator",
          description: "계산 결과를 확인해 보세요!",
          imageUrl: `${window.location.origin}/favicon.png`,
          link: {
            mobileWebUrl: redirectUrl,
            webUrl: redirectUrl,
          },
        },
        buttons: [
          {
            title: "계산 결과 확인하기",
            link: {
              webUrl: redirectUrl,
              mobileWebUrl: redirectUrl,
            },
          },
        ],
      });
    }
  }, []);

  return (
    <div className={styles.share_image}>
      <Image
        id="kakao-link-btn"
        width={44}
        height={44}
        src={"/kakaotalk_sharing_btn_medium.png"}
        alt="카카오톡 공유 이미지"
      />
    </div>
  );
}

 

카카오 sdk 초기화(Kakao.init)후 createDefaultButton을 통해서 공유하기 버튼을 생성합니다.

Image 요소에 id로 매핑되어 클릭시 공유하기 창이 팝업됩니다. 공유하기시 사용되는 이미지 경로는 content - imageUrl 값으로 저장합니다.

 

현재 프로젝트에서 사용된 공유하기는 피드 템플릿을 사용했으며, 종류가 여러가지 있으니 직접 수정해서 사용해보셔도 좋습니다. 공식문서에 관련 코드가 잘 정리되어 있습니다.

 

app/components/share/share.module.scss

.share_image {
  display: flex;
  justify-content: center;
  margin-top: 40px;

  img {
    border-radius: 8px;
    background-color: #f9e101;
    padding: 2px;
    cursor: pointer;
  }
}

 

 

이제 공유하기 컴포넌트를 그리드 컴포넌트에 추가하겠습니다.

app/components/grid/grid.tsx

import { useEffect, useState } from "react";
import styles from "./grid.module.scss";
import MyShare from "../share/share";

export type GridType = {
  paymentMethod: string;
  amount: string;
  period: string;
  interest: string;
};

type ResultType = {
  round: number;
  principalPayment: string;
  interest: string;
  monthlyPayment: string;
  remainPayment: string;
};

type Props = {
  params: GridType | undefined;
};

const MyGrid = ({ params }: Props) => {
  const [result, setResult] = useState<ResultType[]>();

  /**
   * 원리금 균등 분할 상환
   * 계산식 : M = P * r * (1 + r)^n / ((1 + r)^n - 1)
   * @param amount
   * @param monthlyRate
   * @param month
   */
  const formula1 = (amount: number, monthlyRate: number, month: number) => {
    const newResults = [];
    let remainPayment = amount;
    let index = 1;

    while (index <= month) {
      // 총 납부액
      const monthlyPayment =
        (amount * monthlyRate * Math.pow(1 + monthlyRate, month)) / (Math.pow(1 + monthlyRate, month) - 1);
      // 이자액 : 잔여원금 * 월 이자율
      const interest = remainPayment * monthlyRate;
      // 상환원금 : 총 납부액 - 이자액
      const principalPayment = monthlyPayment - interest;
      // 잔여원금 : 잔여원금 - 상환원금
      remainPayment = remainPayment - principalPayment;

      //   console.log(`-- ${index}회차 --`);
      //   console.log(`총납부액 : ${monthlyPayment)}`);
      //   console.log(`이자액 : ${interest}`);
      //   console.log(`상환원금 : ${principalPayment}`);
      //   console.log(`잔여원금 : ${remainPayment}`);

      newResults.push({
        round: index,
        principalPayment: Math.round(principalPayment).toLocaleString(),
        interest: Math.round(interest).toLocaleString(),
        monthlyPayment: Math.round(monthlyPayment).toLocaleString(),
        remainPayment: Math.abs(Math.round(remainPayment)).toLocaleString(),
      });
      index++;
    }

    setResult(newResults);
  };

  /**
   * 원금 균등 분할 상환
   * 계산식 : M = P / n + (P * r)
   * @param amount
   * @param monthlyRate
   * @param month
   */
  const formula2 = (amount: number, monthlyRate: number, month: number) => {
    const newResults = [];
    let remainPayment = amount;
    let index = 1;
    let principalPayment = 0;

    while (index <= month) {
      // 이자액 : 잔여원금 * 월 이자율
      const interest = remainPayment * monthlyRate;
      // 총 납부액
      const monthlyPayment = amount / month + interest;

      // 상환원금 : 총 납부액 - 이자액
      if (index === 1) {
        principalPayment = monthlyPayment - interest;
      }

      // 잔여원금 : 잔여원금 - 상환원금
      remainPayment = remainPayment - principalPayment;

      //   console.log(`-- ${index}회차 --`);
      // console.log(`총납부액 : ${monthlyPayment}`);
      //   console.log(`이자액 : ${interest}`);
      //   console.log(`상환원금 : ${principalPayment}`);
      //   console.log(`잔여원금 : ${remainPayment}`);

      newResults.push({
        round: index,
        principalPayment: Math.round(principalPayment).toLocaleString(),
        interest: Math.round(interest).toLocaleString(),
        monthlyPayment: Math.round(monthlyPayment).toLocaleString(),
        remainPayment: Math.abs(Math.round(remainPayment)).toLocaleString(),
      });
      index++;
    }

    setResult(newResults);
  };

  /**
   * 만기일시 상환
   * 계산식 : M = P * r
   * @param amount
   * @param monthlyRate
   * @param month
   */
  const formula3 = (amount: number, monthlyRate: number, month: number) => {
    const newResults = [];
    let index = 1;
    let principalPayment = 0;

    while (index <= month) {
      // 이자액 : 잔여원금 * 월 이자율
      const interest = amount * monthlyRate;

      // 상환원금 : 마지막 회차에 납부
      if (index === month) {
        principalPayment = amount;
      }

      // 총 납부액
      const monthlyPayment = interest + principalPayment;

      // 잔여원금
      const remainPayment = amount - principalPayment;

      //   console.log(`-- ${index}회차 --`);
      //   console.log(`총납부액 : ${monthlyPayment}`);
      //   console.log(`이자액 : ${interest}`);
      //   console.log(`상환원금 : ${principalPayment}`);
      //   console.log(`잔여원금 : ${remainPayment}`);

      newResults.push({
        round: index,
        principalPayment: Math.round(principalPayment).toLocaleString(),
        interest: Math.round(interest).toLocaleString(),
        monthlyPayment: Math.round(monthlyPayment).toLocaleString(),
        remainPayment: Math.abs(Math.round(remainPayment)).toLocaleString(),
      });
      index++;
    }

    setResult(newResults);
  };

  useEffect(() => {
    const method = params?.paymentMethod;
    // 대출원금
    const amount = Number(params?.amount);
    // 월 이자율 : 대출 연이율 / 12 / 100
    const monthlyRate = Number(params!.interest) / 12 / 100;
    // 대출기간(월 변환)
    const month = Number(params?.period!) * 12;

    switch (method) {
      case "1":
        formula1(amount, monthlyRate, month);
        break;
      case "2":
        formula2(amount, monthlyRate, month);
        break;
      case "3":
        formula3(amount, monthlyRate, month);
        break;
    }
  }, []);

  return (
    <div className={styles.grid_container}>
      <ul className={styles.grid_header}>
        <li style={{ width: "10%" }}>회차</li>
        <li style={{ width: "22%" }}>상환원금</li>
        <li style={{ width: "22%" }}>이자액</li>
        <li style={{ width: "23%" }}>총납부액</li>
        <li style={{ width: "23%" }}>잔여원금</li>
      </ul>

      {result?.map((obj, index) => {
        return (
          <ul className={styles.grid_body} key={index}>
            <li style={{ width: "10%", textAlign: "center" }}>{obj.round}</li>
            <li style={{ width: "22%", textAlign: "right" }}>{obj.principalPayment}</li>
            <li style={{ width: "22%", textAlign: "right" }}>{obj.interest}</li>
            <li style={{ width: "23%", textAlign: "right" }}>{obj.monthlyPayment}</li>
            <li style={{ width: "23%", textAlign: "right" }}>{obj.remainPayment}</li>
          </ul>
        );
      })}

      <div>
        <MyShare
          paymentMethod={params?.paymentMethod!}
          amount={params?.amount!}
          period={params?.period!}
          interest={params?.interest!}
        />
      </div>
    </div>
  );
};

export default MyGrid;

 

프로젝트 실행 후 계산하기를 통해 공유하기 컴포넌트가 잘 추가되었는지 확인해보겠습니다. 추가한 카카오톡 이미지대로 공유 버튼이 생성되었습니다.

 

버튼을 누르면 카카오톡 로그인 후 카카오톡 공유 기능이 실행됩니다.

공유 메세지를 보내보겠습니다.

 

 

카카오톡으로 메세지를 받아볼 수 있고 버튼 클릭시 계산 결과 페이지로 이동됩니다. 기존에 추가된 반응형 웹 디자인으로 모바일 환경에서도 화면이 깨지지 않는 것을 확인할 수 있습니다.

 

계산 결과 페이지

 

개발 과정이 모두 마무리되었습니다.

다음 편에서 이 프로젝트가 실제 웹 서비스로 사용될 수 있도록 배포를 해보겠습니다.

전체 코드는 GitHub에서 확인하실 수 있습니다.

 

참고문서
 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.