문지웅
문지웅님의 아티클
문지웅
QR 코드 생성기 만들기: Next.js와 TypeScript로 시작하기
개요
이 글에서는 QR 코드 생성기를 만드는 단계별 가이드를 제공합니다.
Next.js와 TypeScript를 사용하여 QR 코드를 입력하고 생성하며,
생성된 QR 코드를 다운로드할 수 있는 웹 애플리케이션을 개발하는 방법입니다.
목차
소개
QR 코드란?
프로젝트 개요
프로젝트 설정
프로젝트 초기 설정
UI 구성
전체 레이아웃 구성
입력 필드 추가
QR 코드 생성 버튼 추가
QR 코드 표시 영역 추가
QR 코드 생성
react-qr-code 라이브러리 설치
QR 코드 생성 로직 구현
QR 코드 저장
file-saver 라이브러리 설치
QR 코드 저장 기능 추가
QR 코드에 효과 추가
framer-motion 라이브러리 설치
QR 코드 div 수정
프로젝트 테스트
QR 코드 생성 및 저장 테스트
결론
구현 결과 및 평가
소개
QR 코드는 Quick Response Code의 약자로, 빠르고 쉽게 정보를 전달하는 데 사용되는 2차원 바코드입니다. 이번 프로젝트에서는 Next.js와 TypeScript를 사용하여 QR 코드 생성기를 만들어보려고 합니다. 사용자가 입력한 데이터를 기반으로 QR 코드를 생성하고, 생성된 코드를 이미지로 저장할 수 있는 웹 애플리케이션을 구현할 것입니다.
프로젝트 설정
Next.js 공식문서의 프로젝트 생성 가이드대로, 아래 명령어로 프로젝트를 생성합니다.
npx create-next-app@latestUI 구성
프로젝트의 UI를 구성하기 위해, 먼저, 제목 부분과, QR 생성기 부분으로 나눕니다.
QR 생성기에 입력 필드, QR 코드 생성 버튼, 그리고 QR 코드 표시 영역을 추가합니다.
// app/page.tsx
import QRGenerator from "@/components/QRGenerator";
import Title from "@/components/Title";
export default function Home() {
return (
<main>
<Title />
<QRGenerator />
</main>
);
}입력 필드 생성
QR 코드에 들어갈 내용을 입력하기 위한 입력 필드를 구현합니다.
interface QRCodeInputProps {
value: string;
onChange: (value: string) => void;
}
const QRCodeInput = ({ value, onChange }: QRCodeInputProps) => {
return (
<input
type="text"
value={value}
className="border w-full md:w-3/4 mx-auto px-4 rounded-lg text-lg mt-4"
placeholder="Enter Text"
onChange={(e) => onChange(e.target.value)}
autoFocus
/>
);
};
export default QRCodeInput;
QR 코드 생성
QR 코드 생성을 위해 react-qr-code 라이브러리를 사용하겠습니다.
이 라이브러리를 설치하고 QR 코드 생성 로직을 구현합니다.
npm install react-qr-code"use client"; // useState 사용을 위해, client component 사용
import { useState } from "react";
import saveAs from "file-saver";
import QRCode from "react-qr-code";
import { motion } from "framer-motion";
import QRCodeInput from "./QRCodeInput";
import Button from "./Button";
const QRGenerator = () => {
const [qrData, setQrData] = useState<string>("");
const [isGenerated, setIsGenerated] = useState<boolean>(false);
const handleInputChange = (value: string) => {
setQrData(value);
setIsGenerated(false);
};
const generateQRCode = () => {
if (qrData) {
setIsGenerated(true);
}
};
return (
<>
<QRCodeInput value={qrData} onChange={handleInputChange} />
<Button
onClick={generateQRCode}
disabled={qrData === ""}
title="Generate QR Code"
/>
{isGenerated && (
<>
<div className="border p-2">
<QRCode value={qrData} />
</div>
</>
)}
</>
);
};
export default QRGenerator;
QR 코드 저장
생성된 QR 코드를 이미지로 저장하기 위해 file-saver 라이브러리를 사용합니다. 이 라이브러리를 설치하고 QR 코드 저장 기능을 추가합니다. TypeScript 프로젝트이므로, @types/file-saver 도 필요합니다.
npm install file-saver @types/file-saver import saveAs from "file-saver";
// ...
const saveQRCode = () => {
if (isGenerated) {
const svgString = document.querySelector("svg")?.outerHTML;
if (!svgString) return;
const blob = new Blob([svgString], { type: "image/svg+xml" });
saveAs(blob, "qrcode.svg");
}
};
// ...※ 기능 추가
framer-motion을 추가하여, QR Code가 생성될 때, animation을 추가했습니다.
// ...
<motion.div
className="border p-2"
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
>
<QRCode value={qrData} />
</motion.div>
// ...완성된 QRGenerator 코드
"use client";
import { useState } from "react";
import saveAs from "file-saver";
import QRCode from "react-qr-code";
import { motion } from "framer-motion";
import QRCodeInput from "./QRCodeInput";
import Button from "./Button";
const QRGenerator = () => {
const [qrData, setQrData] = useState<string>("");
const [isGenerated, setIsGenerated] = useState<boolean>(false);
const handleInputChange = (value: string) => {
setQrData(value);
setIsGenerated(false);
};
const generateQRCode = () => {
if (qrData) {
setIsGenerated(true);
}
};
const saveQRCode = () => {
if (isGenerated) {
const svgString = document.querySelector("svg")?.outerHTML;
if (!svgString) return;
const blob = new Blob([svgString], { type: "image/svg+xml" });
saveAs(blob, "qrcode.svg");
}
};
return (
<>
<QRCodeInput value={qrData} onChange={handleInputChange} />
<Button
onClick={generateQRCode}
disabled={qrData === ""}
title="Generate QR Code"
/>
{isGenerated && (
<>
<motion.div
className="border p-2"
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
>
<QRCode value={qrData} />
</motion.div>
<Button
onClick={saveQRCode}
disabled={!isGenerated}
title="Save QR Code"
/>
</>
)}
</>
);
};
export default QRGenerator;프로젝트 테스트
전체 소스 코드를 다운 받고 npm run dev 로 실행할 수도 있고,
아래 링크에서 직접 테스트도 가능합니다.
웹 애플리케이션을 실행하고 실제로 QR 코드를 생성하고 저장해보시기 바랍니다.
결론
이미 만들어진 QR Code 라이브러리를 활용해서, 쉽고 빠르게 QR Code 생성기를 만들어 볼 수 있었습니다.
※ 참고 1) 전체 소스 코드
※ 참고 2) 프로젝트 이름 배경
QRQuasar:QRQuasar는 강력한 QR 코드 생성 도구로, 빠른 속도와 높은 효율성을 특징으로 합니다. (ChatGPT)
긴 글 읽어주셔서 감사합니다. 👍
문지웅
💡만들게 된 배경
블로그 작성 과정에서 필요한 섬네일 이미지 선택에 대한 고민이 생겨, 이 문제를 해결하기 위해 직접 섬네일 생성기를 개발하였습니다. 이 프로덕트는 '미니 픽(Mini Pick)'이라는 이름으로 명명되었는데, 이 이름은 ChatGPT로부터 제안 받은 10가지 이름 중에서 가장 적합하다고 판단하여 선택하였습니다.
⚒️ 기술 스택
Next.js , TypeScript, TailwindCSS, html-to-image , file-saver,Vercel
주요 기능
배경 설정 - 단색, 그라데이션 배경, 이미지 url 배경을 설정할 수 있습니다.
텍스트 설정 - 텍스트에 그림자를 주거나, 흰색 또는 검정색으로 설정할 수 있습니다.
반응형 디자인 - 반응형으로 설계되어, 저장하는 이미지의 넓이를 창의 크기에 따라 조절할 수 있습니다.
빠른 저장 - 저장하기 버튼 클릭했을 때, 바로 저장됩니다.
🖥️ 구현 과정
1. 프로젝트 생성
아래 명령어로 프로젝트를 생성합니다.
npx create-next-app@latest세부 설정은 아래와 같습니다.
What is your project named? image-generator
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use src/ directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias? No / Yes2. 프로젝트 구현
전체 프로젝트 구현 과정은 아래와 같습니다.
전체 레이아웃 구성
제목이 표시될 부분과 이미지 생성기가 표시될 부분을 구현했습니다.
제목 컴포넌트 구현
제목을 나타낼 Title 컴포넌트를 구현했습니다.
이미지 생성기 컴포넌트 구현
섬네일에 들어갈 문구를 입력 받는 부분을 구현했습니다.
배경 스타일을 설정하는 부분을 구현했습니다.
단색, 그라데이션, 이미지 배경이 가능합니다.
이미지 배경의 경우, 모달을 통해 입력받습니다.
텍스트 스타일을 설정하는 부분을 구현했습니다.
텍스트에 그림자를 추가할 수 있습니다.
텍스트 색상을 흰색 또는 검정색으로 설정할 수 있습니다.
이미지로 변환하기 전, 미리 보기할 부분을 구현했습니다.
넓이가 고정되어 있지 않아, 다양한 크기로 저장 가능합니다.
이미지를 저장할 저장하기 버튼을 구현했습니다.
file-saver를 이용해서, 버튼을 눌렀을 때, 바로 저장되도록 구현했습니다.
3. 리팩토링
이미지 생성기 컴포넌트 분할
초기에는 하나의 컴포넌트 안에서, 값 설정과 미리 보기를 함께 진행했습니다. 이를 두 개의 컴포넌트가 나누어 처리하도록 분리했습니다.
html2canvas 에서 html-to-image로 전환
초기에는 html2canvas를 이용해서, 미리 보기 div를 이미지로 저장하고자 했습니다. 해당 라이브러리 사용 시, url로 배경 이미지를 지정하면 이미지를 저장했을 때, 배경이 저장되지 않는 이슈가 생겼습니다. 이를 해결하기 위해, html2canvas 대신에, html-to-image로 전환했습니다.
구현 결과
아래 링크를 클릭하면 직접 확인하실 수 있습니다.
※ 전체 소스 코드는 여기서 확인할 수 있습니다.
감사합니다.
미니픽
미니멀한 블로그 썸네일 생성기
문지웅
Next.js와 ContentLayer로 MDX 블로그 만들기
Next.js와 ContentLayer를 활용해서, MDX 블로그 템플릿을 만들어보려고 합니다.
Next.js , TypeScript, TailwindCSS, ContentLayer 를 사용했습니다.
1. 프로젝트 생성하기
아래 명령어로 프로젝트를 생성합니다.
npx create-next-app@latest해당 명령어를 실행하면 터미널에서 아래와 같은 내용을 확인할 수 있습니다.
프로젝트 이름을 정하고, yes or no 를 선택하면 됩니다. 파란색 부분으로 나타나는 부분이 제가 선택한 부분입니다.
2. 프로젝트 실행하기
정상적으로 설치되었다면, 아래 명령어로 프로젝트를 실행할 수 있습니다.
npm run dev문제없이 실행된다면, 아래 이미지와 같습니다.
3. ContentLayer 세팅하기
먼저, ContentLayer를 설치해야 합니다. Next.js에서 사용할 것이므로, next-contentlayer 도 필요하고, 날짜 표시를 위해 date-fns도 필요합니다. 아래 명령어로 설치합니다.
npm install contentlayer next-contentlayer date-fns설치가 완료되면, ContentLayer를 사용하기 위해, next.config.js , tsconfig.json수정이 필요합니다.
아래와 같이 수정합니다.
1) next.config.js
import { withContentlayer } from "next-contentlayer";
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
};
module.exports = withContentlayer(nextConfig);2) tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"contentlayer/generated": ["./.contentlayer/generated"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".contentlayer/generated"
],
"exclude": ["node_modules"]
}또한, .contentlayer 폴더의 내용물은 GitHub에 올라갈 필요가 없으므로, .gitignore 파일에 아래 내용을 추가합니다.
# contentlayer
.contentlayer그리고, 루트 디렉토리에 contentlayer.config.ts 파일을 생성합니다.
contentlayer.config.ts 파일의 코드는 아래와 같습니다.
// contentlayer.config.ts
import { defineDocumentType, makeSource } from "contentlayer/source-files";
export const Post = defineDocumentType(() => ({
name: "Post",
filePathPattern: `**/*.mdx`,
contentType:"mdx",
fields: {
title: {
type: "string",
required: true,
},
date: {
type: "date",
required: true,
},
},
computedFields: {
url: {
type: "string",
resolve: (post) => `/posts/${post._raw.flattenedPath}`,
},
},
}));
export default makeSource({ contentDirPath: "posts", documentTypes: [Post] });이제 Contentlayer를 사용할 준비는 끝났습니다.
4. 게시글 작성하기
루트 디렉토리에 posts폴더를 생성하고, 게시글을 작성합니다. posts 폴더인 이유는 contentDirPath: "posts" 로 설정했기 때문입니다.
---
title: My First Post
date: 2023-08-10
---
This is My First Post!!날짜 순 정렬을 테스트하기 위해, date를 다르게 해서 세 개의 MDX파일을 생성합니다. ex) 2023-08-08, 2023-08-09, 2023-08-10
게시글까지 작성했으면, 게시글을 보여주기 위해 홈페이지(app 디렉토리의
page.tsx)를 아래와 같이 수정해야 합니다.
import { allPosts } from "@/.contentlayer/generated";
import { compareDesc } from "date-fns";
export default function Home() {
const posts = allPosts.sort((a, b) =>
compareDesc(new Date(a.date), new Date(b.date))
);
return (
<div>
{posts.map((post) => (
<h2 key={post._id}>{post.title}</h2>
))}
</div>
);
}수정하고 나면, 아래와 같이 게시글 제목들이 나오는 것을 확인할 수 있습니다.
이제 제목만 보여주는 대신 각 포스트를 보여줄 컴포넌트도 추가하고, 홈도 좀 더 꾸며보도록 하겠습니다.
1) PostCard.tsx
app 디렉토리에 components 폴더를 생성하고, PostCard.tsx 파일을 생성합니다.
import { Post } from "@/.contentlayer/generated";
import { format, parseISO } from "date-fns";
import Link from "next/link";
export default function PostCard(post: Post): React.ReactElement {
return (
<div className="mb-4 flex flex-col border-2 rounded-lg p-2">
<Link href={post.url} className="text-3xl mb-1 text-blue-500">
{post.title}
</Link>
<time dateTime={post.date}>
{format(parseISO(post.date), "LLLL d, yyyy")}
</time>
</div>
);
}2) 수정된 page.tsx
import { allPosts } from "@/.contentlayer/generated";
import { compareDesc } from "date-fns";
import PostCard from "./components/PostCard";
export default function Home() {
const posts = allPosts.sort((a, b) =>
compareDesc(new Date(a.date), new Date(b.date))
);
return (
<main className="mx-auto max-w-5xl">
<h1 className="my-8 text-center text-3xl font-bold">Next.js & ContentLayer Blog Example</h1>
{posts.map((post) => (
<PostCard key={post._id} {...post} />
))}
</main>
);
}위 코드와 같이 수정하면, 아래와 같은 결과물을 확인할 수 있습니다.
아직, 상세 페이지가 존재하지 않아서, 각 포스트를 클릭하면 404 페이지를 만나게 됩니다. 이를 해결하기 위해 상세 페이지를 생성합니다.
상세 페이지 파일의 위치는 app/posts/[slug]/page.tsx 가 됩니다.
상세 페이지의 코드는 다음과 같습니다.
import { allPosts } from "@/.contentlayer/generated";
import { notFound } from "next/navigation";
import type { MDXComponents } from "mdx/types";
import Link from "next/link";
import { useMDXComponent } from "next-contentlayer/hooks";
const mdxComponents: MDXComponents = {
a: ({ href, children }) => <Link href={href as string}>{children}</Link>,
};
export const generatedStaticParams = async () => {
allPosts.map((post) => ({ slug: post._raw.flattenedPath }));
};
export const generatedMetadata = ({ params }: { params: { slug: string } }) => {
const post = allPosts.find((post) => post._raw.flattenedPath === params.slug);
if (!post) notFound();
};
export default function Page({ params }: { params: { slug: string } }) {
const post = allPosts.find((post) => post._raw.flattenedPath === params.slug);
if (!post) notFound();
const MDXContent = useMDXComponent(post.body.code);
return (
<article className="mx-auto prose">
<div className="mb-8 text-center">
<time dateTime={post.date} className="mb-1 text-xs text-gray-600">
{new Intl.DateTimeFormat("en-US").format(new Date(post.date))}
</time>
<h1 className="text-3xl font-bold">{post.title}</h1>
</div>
<MDXContent components={mdxComponents} />
</article>
);
}기본적인 구현은 완료되었습니다.
추가적으로, not-found.tsx 파일을 추가해서, 잘못된 링크에 대한 404 페이지를 커스텀할 수도 있습니다.
또한, 상세페이지 article의 스타일링을 쉽게 하기 위해, @tailwindcss/typography 를 사용할 수 있습니다.
먼저, 아래 명령어로 설치합니다.
npm install -D @tailwindcss/typography설치가 완료되면, tailwind.config.js 에 해당 플러그인을 추가해준다.
module.exports = {
theme: {
// ...
},
plugins: [
require('@tailwindcss/typography'),
// ...
],
}완료되면 <article> 의 className에 prose를 추가해주면 됩니다!
<article className="mx-auto prose"> ... </article>prose 관련 추가 설정은 아래 링크에서 확인할 수 있습니다.
그 외에 remark나 rehype 플러그인을 추가할 수도 있습니다.
remarkGfm을 추가한다고 하면, 아래 명령어로 설치하고, contentlayer.config.ts를 수정해서 사용할 수 있습니다.
1) remarkGfm 설치
npm install remark-gfm2) contentlayer.config.ts 코드 수정
// contentlayer.config.ts
import { defineDocumentType, makeSource } from "contentlayer/source-files";
import remarkGfm from "remark-gfm";
export const Post = defineDocumentType(() => ({
name: "Post",
filePathPattern: `**/*.mdx`,
contentType: "mdx",
fields: {
title: {
type: "string",
required: true,
},
date: {
type: "date",
required: true,
},
},
computedFields: {
url: {
type: "string",
resolve: (post) => `/posts/${post._raw.flattenedPath}`,
},
},
}));
export default makeSource({
contentDirPath: "posts",
documentTypes: [Post],
mdx: {
remarkPlugins: [remarkGfm],
rehypePlugins: [],
},
});
그 외에도 직접 커스터마이징해서 사용할 수 있습니다.
긴 글 읽어주셔서 감사합니다 😀
전체 소스 코드는 아래 링크에서 확인할 수 있습니다! ^^