React

Create a Url Shortener using NextJs, Tailwind CSS and Strapi

Asset Type
File Type
When to use
2022/03/18
Reference
Created time
2022/03/18 13:08
Created by
Last edited time
2022/05/07 13:13

Advantages of Using a Shortened URL

이 기사에 대해 자세히 설명하기 전에 URL Shortener의 기능에 대해 명확하게 이해할 필요가 있습니다. 다음과 같은 이점이 있습니다.
Aesthetic appeal : URL에 날짜나 장소를 포함하는 긴 링크가 아니라 링크에 이벤트 이름만 있는 이벤트 초대장을 보는 것은 멋진 일이 아닐까요? ( 클린 URL )
Analytics tracking : 여러 장소에 배포할 수 있는 어플리케이션으로서 다수의 고객 관리 담당자를 운용하는 비용을 삭감합니다.
Link Swapping : 대부분의 URL 단축 서비스는 실제 URL을 편집할 수 있기 때문에 링크에 접속하는 웹페이지에 유연하게 대응하면서 공유하는 링크에서 항상 일관성을 유지할 수 있습니다.
Easier to remember : 대부분의 단축 서비스가 있기 때문에 도메인은 bit.ly, TinyURL과 같이 짧습니다. URL이 공유되면 사람들이 URL을 불러오기 쉽습니다.

Prerequisites

튜토리얼 전에 다음 사항이 필요하다.
Node.js (v14+)
Strapi 기본적인 이해
Next.js 기본 지식
Tailwind CSS 기초 지식

What is Next Js

Next.js is an awesome React framework for building highly dynamic applications. It comes with pre-rendering, server-side rendering, automatic code-splitting amongst many other great features out of the box.
Next.js는 매우 역동적인 애플리케이션을 구축하기 위한 훌륭한 React 프레임워크입니다. 프리렌더링, 서버 사이드 렌더링, 자동 코드 분할 기능을 갖추고 있습니다.

What is Tailwind CSS

Tailwind CSS is a utility-first CSS framework for rapidly building custom user interfaces. With Tailwind CSS, we write our CSS directly in our HTML classes. This is quite useful as we don't need to import an external stylesheet or use a separate library for UI designs.
테일윈드 CSS는 커스텀 사용자 인터페이스를 신속하게 구축하기 위한 유틸리티 우선 CSS 프레임워크입니다. Tailwind CSS에서는 HTML 클래스에서 직접 CSS를 작성합니다. UI 디자인을 위해 외부 스타일시트를 가져오거나 별도의 라이브러리를 사용할 필요가 없으므로 매우 유용합니다.

What is Strapi

Strapi is a Node.js open-source headless CMS that allows us to develop APIs and manage content easily without the hassle of building out a project from scratch. It allows for customization and self-hosting as opposed to the rigid traditional CMS we are used to.
Strapi는 Node.js 오픈 소스 헤드리스 CMS로 프로젝트를 처음부터 구축하는 번거로움 없이 API를 개발하고 콘텐츠를 쉽게 관리할 수 있습니다. 기존의 엄격한 CMS와는 달리 커스터마이즈와 셀프호스팅이 가능합니다.
We can easily build out APIs faster and consume the contents via APIs using any REST API client or GraphQL.
REST API 클라이언트나 GraphQL을 사용하면 API를 통해 보다 빠르게 API를 구축하고 콘텐츠를 소비할 수 있습니다.

Scaffolding a Strapi Project

To set up a new Strapi Project is quite straightforward as running these few commands:
npx create-strapi-app strapi-tutorial-shortner --quickstart
TypeScript
복사
Change strapi-tutorial-shortner to the preferred name of your project.
This would install and create a Strapi project locally.
After installation, the browser would open a page on localhost:1337, which would prompt to set up the first admin account to proceed with Strapi.
strapi-tutorial-shortner를 프로젝트의 기본 이름으로 변경합니다.
그러면 Strapi 프로젝트가 로컬로 설치 및 생성됩니다.
설치 후 브라우저는 [localhost:1337](http://localhost:1337/) 페이지를 열어 Strapi를 진행하기 위한 첫 번째 관리자 계정을 설정하라는 메시지를 표시합니다.

Building the Shortener Collection

Next, we will create a new collection type that will store the details of each question and their respective answers.
Hence, we create a collection type called shortner that has these four fields fields: alias, url, visit, user.
다음으로 각 질문의 상세 내용과 답변을 저장하는 새로운 컬렉션 유형을 만듭니다.
따라서 "alias", "url", "visit", "user"의 4가지 필드를 가진 "shortner"라는 컬렉션 유형을 만듭니다.
Clicking “Continue” would bring up another screen to select the fields for this collection. Choose the “Text” field from the list and provide alias as its name.
"계속"을 클릭하면 이 컬렉션의 필드를 선택할 수 있는 다른 화면이 나타납니다. 목록에서 "Text" 필드를 선택하고 alias을 이름으로 지정합니다.
Next, we select the Short Text type in the Base Settings, as alias is meant to be a short string.
다음으로 alias 는 짧은 문자열이므로 기본 설정에서 Short Text 유형을 선택합니다.
Next, we proceed to the “Advanced settings” tab and check the “Required field” box to ensure this field is required. Also, we check the “Unique field” box to prevent having the same alias in our record.
다음으로 [Advanced settings]탭으로 이동하여 [Required field] 체크박스를 켜고 이 필드가 필수인지 확인합니다. 또한 "Unique field" 체크박스를 켜서 동일한 alias가 기록되지 않도록 합니다.
We click on the Add another field to add the answer field. Below is a table showing the properties for all the fields we need in this collection:
Add another field를 클릭하여 응답 필드를 추가합니다. 다음 표는 이 컬렉션에서 필요한 모든 필드의 속성을 보여줍니다.
Search
Field Name
Field Type
Required
Unique
Short text
Short text
Number (integer)
Number (integer)

Allowing Public access

By default, whenever you create an API, they’re all going to be restricted from public access. We need to tell Strapi that you’re okay with exposing these checked endpoints to the public. Go to Settings > Users & Permissions Plugin ** > Roles and click to edit the Public Role. Next, scroll down to Permissions > Shortner and tick the find checkbox.
기본적으로는 API를 만들 때마다 모든 API는 공개 액세스가 제한됩니다. Strapi에게 이 체크된 엔드포인트들을 일반에 공개해도 괜찮다고 말해야 해요 [ Settings ]> [ Users & Permissions Plugin ]> [ Roles ]순서로 선택하여 [Public Role]을 클릭하여 편집합니다. 다음으로 [Permissions]> [ Shortner ]까지 스크롤하여 [find]체크박스를 켜겠습니다
We would also be exposing some endpoints to the authenticated user. Click the “Go Back” button and then click edit the Authenticated Role. The image below shows the endpoints which would be exposed to the authenticated user: **
또한 authenticated user에게 일부 엔드포인트가 노출됩니다. [Go Back] 단추를 누른 후 [Authenticated Role]을 누릅니다. 다음 이미지는 authenticated user에게 노출되는 엔드포인트를 보여 줍니다.

Customizing the Shorter Controller

src/api/shortner/controllers/shortner.js에 있는 shortner 컨트롤러를 커스터마이즈합니다. 더 많은 기능을 추가하여 우리의 요구를 충족시킵니다.
「find」메서드의 경우는, 다음의 시나리오가 있습니다.
1. authenticated user가 호출한 경우 해당 사용자의 레코드만 표시합니다. 일반적으로 프론트 엔드에서 대시보드에 레코드를 표시하려는 경우 호출됩니다.
2. unauthenticated user에 의해 호출된 경우 제공된 쿼리에 따라 필터링합니다. 일반적으로 프론트 엔드에서 호출되어 레코드에 alias가 존재하는지 여부를 확인합니다. 발견된 경우 shortner 컬렉션의 visit 필드도 증가하여 visit을 추적합니다.
create 메서드의 경우 shortner 컬렉션의 user 필드를 authenticated user ID에 할당하고 새 레코드를 만들기 위해 사용합니다. 따라서 authenticated user만 이 엔드포인트에 액세스할 수 있습니다.
delete 메서드의 경우 shortner 컬렉션에서 레코드를 삭제할 때 사용합니다. 레코드를 작성한 사용자만이 삭제할 수 있습니다. 이는 authenticated user만 이 엔드포인트에 액세스할 수 있음을 의미합니다.
따라서 파일 코드를 다음 코드로 바꿉니다.
'use strict'; /** * shortner controller */ const { createCoreController } = require('@strapi/strapi').factories; module.exports = createCoreController('api::shortner.shortner', ({ strapi }) => ({ async find(ctx) { let { query } = ctx; const user = ctx.state.user; let entity; if (user) { query = { user: { '$eq': user.id } } entity = await strapi.service('api::shortner.shortner').find({ filters: query }); } else { query = { alias: { '$eq': query.alias } } entity = await strapi.service('api::shortner.shortner').find({ filters: query }); if (entity.results.length !== 0) { let id = entity.results[0].id let visit = Number(entity.results[0].visit) + 1 await strapi.service('api::shortner.shortner').update(id, { data: { visit } }); } } const sanitizedEntity = await this.sanitizeOutput(entity, ctx); return this.transformResponse(sanitizedEntity); }, async create(ctx) { const { data } = ctx.request.body; const user = ctx.state.user; let entity; data.user = user.id entity = await strapi.service('api::shortner.shortner').create({ data }); const sanitizedEntity = await this.sanitizeOutput(entity, ctx); return this.transformResponse(sanitizedEntity); }, async delete(ctx) { let { id } = ctx.params; const user = ctx.state.user; let entity; let query = { user: { '$eq': user.id }, id: { '$eq': id } } entity = await strapi.service('api::shortner.shortner').find({ filters: query }); if (entity.results.length === 0) { return ctx.badRequest(null, [{ messages: [{ id: 'You can delete someone else content' }] }]); } entity = await strapi.service('api::shortner.shortner').delete(id); const sanitizedEntity = await this.sanitizeOutput(entity, ctx); return this.transformResponse(sanitizedEntity); }, }));
JavaScript
복사

Scaffolding a Next.js project

Creating a Next.js app
To create a Next.js app, open your terminal, cd into the directory you’d like to create the app in, and run the following command:
npx create-next-app -e with-tailwindcss nextjs-shortner
Plain Text
복사
This would also configure Tailwind CSS with the project.
Running the Next.js Development Server
Next, we cd into the newly created directory, in our case that would be nextjs-``shortner:
cd nextjs-shortner
Plain Text
복사
After which we start up the development server by running this command:
npm run dev
Plain Text
복사
If everything was set up fine, the Next.js server should now be running on localhost:3000 and we should see the following page on our browser:

Building Next.js Components

Next, we open up any text editor of our choice to write code for the rest of the application. Open up the installed project and we should have a folder structure such as this:
다음으로, 어플리케이션의 나머지 부분에서 코드를 작성하기 위해 선택한 텍스트 에디터를 엽니다. 설치된 프로젝트를 열면 다음과 같은 폴더 구조가 생성됩니다.
To begin the design of the interface, we would remove all of the code in the index.js file and add the code below:
인터페이스 설계를 시작하려면 index.js 파일의 모든 코드를 삭제하고 다음 코드를 추가합니다.
import React, { useContext, useEffect } from 'react'; import MyContext from '../lib/context'; import { useRouter } from "next/router"; export default function Home() { const { isLoggedIn, user } = useContext(MyContext) const router = useRouter() useEffect(() => { if (isLoggedIn) { return router.push("/dashboard"); } return router.push("/login"); }, [isLoggedIn]) return null }
TypeScript
복사
useEffect에서 뭔가 리턴하고 있는데 이동하고 함수를 리턴시키고 싶어서인 것 같다. 에러가 난다. 안하면 된다. return 제거.
The above code makes use of React Context API to check if the user is authenticated. This determines which page gets shown to the user.
As can also be seen, we are importing a context file from the lib folder. We need to create this file. Go to the root of the project and create a folder called lib, then create a file called context.js in it.
Inside this context.js, we create the context, and also assign the default value of false to isLoggedIn.
위 코드는 React Context API를 사용하여 사용자가 인증되었는지 확인합니다. 이것은 사용자에게 표시할 페이지를 결정합니다.
마찬가지로 lib 폴더에서 context 파일을 Import하고 있습니다. 이 파일을 작성해야 합니다. 프로젝트의 루트로 이동하여 lib라는 폴더를 만든 다음 context.js라는 파일을 만듭니다.
이 context.js에서는 context를 작성하고 기본값인 falseisLogged에 할당합니다.
import React from 'react'; const MyContext = React.createContext({ isLoggedIn: false }); export default MyContext;
TypeScript
복사
Next, we head straight to create the two files we would conditionally be redirecting to the Login and Register files.
다음으로 조건부로 리다이렉트하는 2개의 파일을 작성하기 위해서, Login파일과 Register파일로 이동합니다.
Next.js creates routes for files under the pages directory. The route points to the files themselves, their documentation explains it quite well. This means if we created a file called dashboard.js in the pages directory, we can access it by visiting localhost:3000/dashboard without needing to create an additional routing mechanism. Great right?
Next.js는 pages 디렉토리 아래에 파일의 루트를 만듭니다. 루트가 파일 자체를 가리키고 있기 때문에, https://nextjs.org/docs/routing/introduction의 설명에 매우 적합합니다. 즉, pages 디렉토리에 dashboard.js라는 파일을 작성했다면 별도의 라우팅 메커니즘을 만들지 않고도 localhost:3000/dashboard를 방문하여 액세스할 수 있습니다. 대단하죠?
So, we simply create the two files (Login and Register) in this pages directory.
따라서 이 페이지 디렉토리에 2개의 파일(Login과 Register)을 작성하기만 하면 됩니다.
Login
However, before we dive into these two pages, we would need to first update the content of the _app.js page.
그러나 이 두 페이지를 자세히 살펴보기 전에 먼저 _app.js 페이지의 내용을 업데이트해야 합니다.
This page is used by Next.js to initialize other pages, so we could use it to achieve persistent layout between pages, custom error handling, and in our case, keeping a global state among pages. Read more about this page here.
이 페이지는 Next.js에서 다른 페이지를 초기화하기 위해 사용됩니다.따라서 페이지 간 persistent layout, 커스텀에러 처리, 페이지 간 global state를 유지하기 위해 사용할 수 있습니다. 이 페이지의 [여기](https://nextjs.org/docs/advanced-features/custom-app))에 대한 자세한 내용은 이쪽을 참조하십시오.
Create an _app.js file if it doesn't exist in the pages directory. Remove everything in it and replace its code with the code below:
pages 디렉터리에 존재하지 않는 경우 '_app.js' 파일을 만듭니다. 안에 있는 모든 것을 제거하고 코드를 다음 코드로 바꿉니다.
import React, { useState, useEffect } from 'react'; import MyContext from '../lib/context'; import Cookie from "js-cookie"; import 'tailwindcss/tailwind.css' export default function _App({ Component, pageProps }) { const [user, setUser] = useState(null) const [urls, setUrls] = useState([]) useEffect(() => { const jwt = Cookie.get("jwt"); if (jwt) { fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/users/me`, { headers: { Authorization: `Bearer ${jwt}`, }, }).then(async (res) => { if (!res.ok) { Cookie.remove("jwt"); setUser(null); } const user = await res.json(); setUser(user); }); } }, []) return ( <MyContext.Provider value={{ user: user, isLoggedIn: !!user, setUser, setUrls, urls }} > <Component {...pageProps} /> </MyContext.Provider> ) }
TypeScript
복사
The above code simply wraps itself around all pages and handles the global state by using React Context API.
위의 코드는 단순히 모든 페이지를 감싸고 React Context API를 사용하여 global state를 처리합니다.
We also use the js-cookie npm package to store our token, to persist a session even when the user refreshes the page.
또한 "js-cookie" npm 패키지를 사용하여 토큰을 저장하고 사용자가 페이지를 새로 고친 경우에도 세션을 유지합니다.
To get it installed, we run the npm i js-cookie command.
이를 설치하려면 npm i js-cookie 명령을 실행합니다.
(_app.tsx일 때)
Then we import it into our file.
그런 다음 파일에 Import합니다.
import Cookie from "js-cookie";
TypeScript
복사
We make use of the useEffect hook to check if there is a stored token (meaning the user is logged in). If a token is found, we make a request to the Strapi API to get the details of this user. If there are no errors, we store the user in the user state, else we delete the token and assign null to the user state.
useEffect 후크를 사용하여 저장된 토큰(사용자가 로그인하고 있음을 의미)이 있는지 확인합니다. 토큰이 발견되면 Strapi API에 요청하여 해당 사용자의 세부 정보를 얻습니다. 오류가 없으면 사용자를 user 상태로 저장합니다. 그렇지 않으면 토큰을 삭제하고 nulluser 상태에 할당합니다.
useEffect(() => { const jwt = Cookie.get("jwt"); if (jwt) { fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/users/me`, { headers: { Authorization: `Bearer ${jwt}`, }, }).then(async (res) => { if (!res.ok) { Cookie.remove("jwt"); setUser(null); } const user = await res.json(); setUser(user); }); } }, [])
TypeScript
복사
As can also be seen we have two states, user and urls, created using the useState hook. We have seen the use of the user state already, we use the urls state to store the array of shorteners that we got from the Strapi API.
또, useState 후크를 사용해 작성한 userurls의 2개의 states가 있습니다. user 상태의 사용은 이미 확인되었으며 Strapi API에서 얻은 shorteners 배열을 저장하기 위해 urls 상태를 사용합니다.
Lastly, we wrap the Component with the Context API provider, similar to how we do it in Redux. Next, we set the values of the Context API to our state variables as well as functions such as setUrls, setUser so that other pages/components would be able to access them.
마지막으로 https://redux.js.org/의 방법과 마찬가지로 ComponentContext API 프로바이더로 래핑합니다. 다음으로 Context API의 값을 상태 변수뿐만 아니라 setUrls, setUser 등의 함수로 설정하여 다른 pages/components가 액세스할 수 있도록 합니다.
Finally, we create a new variable called isLoggedIn, this would be used to check if there exists an authenticated user.
마지막으로 isLogged라는 새로운 변수를 만듭니다.그럼 인증된 사용자가 있는지 확인하기 위해 사용합니다.
return ( <MyContext.Provider value={{ user: user, isLoggedIn: !!user, setUser, setUrls, urls }} > <Component {...pageProps} /> </MyContext.Provider> )
TypeScript
복사
Now, we would go on to create the Register file. Add the content below to the newly created pages/register.js file:
이제 Register 파일을 만듭니다. 새로 작성한 pages/register.js 파일에 다음 내용을 추가합니다.
import Head from 'next/head' import Link from 'next/link' import React, { useState, useContext, useEffect } from 'react'; import MyContext from '../lib/context'; import { register } from '../lib/auth' import { useRouter } from "next/router"; export default function Register() { const { isLoggedIn, setUser } = useContext(MyContext) const router = useRouter() let [username, setUsername] = useState(""); let [email, setEmail] = useState(""); let [password, setPassword] = useState("") const [loading, setLoading] = useState(false); const [errors, setErrors] = useState({}); useEffect( () => { if (isLoggedIn) { return router.push("/dashboard"); } }, [isLoggedIn]) const submit = async () => { if(!username.trim()) return setErrors({ username: "Username must not be empty"}) if(!email) return setErrors({ email: "Email must not be empty"}) if(!password) return setErrors({ password: "Password must not be empty"}) setLoading(true); const reg = await (register(username, email, password)) setLoading(false); if(reg.jwt){ setUser(reg.user); router.push('/dashboard') }else{ setErrors({ server: reg?.error?.message || 'Error from server' }); } } return ( <div className="flex flex-col items-center justify-center min-h-screen py-2"> <Head> <title>Create Next App</title> <link rel="icon" href="/favicon.ico" /> </Head> <main className="flex flex-col items-center justify-center w-full flex-1 px-20 text-center"> <h1 className="text-6xl font-bold text-blue-600"> Url Shortener </h1> <div className="flex flex-wrap items-center justify-around max-w-4xl mt-6 sm:w-full"> <form className="w-full max-w-lg mt-8" onSubmit={(e) => { e.preventDefault(); submit() }}> <div className="flex flex-wrap -mx-3 mb-2"> <div className="w-full px-3 mb-6 md:mb-0"> <input onChange={ (e) => setUsername(e.target.value)} placeholder="Enter username" className={`appearance-none block w-full text-gray-700 mb-4 border rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500 ${errors.username ? "border-red-500" : "border-gray-200"}`} id="grid-username" type="text" /> {errors.username ? ( <p className="text-red-500 text-xs italic">{errors.username}</p> ) : ''} </div> </div> <div className="flex flex-wrap -mx-3 mb-2"> <div className="w-full px-3 mb-6 md:mb-0"> <input onChange={ (e) => setEmail(e.target.value)} placeholder="Enter email" className={`appearance-none block w-full text-gray-700 mb-4 border rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500 ${errors.email ? "border-red-500" : "border-gray-200"}`} id="grid-email" type="email" /> {errors.email ? ( <p className="text-red-500 text-xs italic">{errors.email}</p> ) : ''} </div> </div> <div className="flex flex-wrap -mx-3 mb-6"> <div className="w-full px-3"> <span className={`w-full inline-flex items-center rounded border border-r-1 text-gray-700 mb-2 text-sm focus:outline-none focus:bg-white focus:border-gray-500 ${errors.password ? "border-red-500 " : " border-gray-200"}`}> <input onChange={ (e) => setPassword(e.target.value)} placeholder="******************" className="appearance-none block rounded w-full py-3 px-4 leading-tight" id="grid-password" type='password' /> </span> {errors.password ? ( <p className="text-red-500 text-xs italic">{errors.password}</p> ) : ''} </div> </div> {errors.server ? ( <p className="text-red-500 text-xs italic">{errors.server}</p> ) : ''} <div className="flex flex-row flex-wrap justify-between"> <span className="text-blue-600 hover:text-gray-600 pt-2 md:p-6"> <Link href="/login">Back to Login?</Link></span> <button disabled={loading} className={`w-full md:w-1/2 mt-3 flex justify-center hover:bg-gray-200 hover:text-gray-900 rounded-md px-3 py-3 uppercase ${loading ? "bg-gray-200 text-black cursor-not-allowed" : "bg-gray-900 text-white cursor-pointer"}`}> {loading ? ( <> loading &nbsp;... </> ) : 'Register'} </button> </div> </form> </div> </main> </div> ) }
TypeScript
복사
The above code registers users to the platform, allowing us to create a secured page later for people to come in, create, manage and track their shortened URLs.
위의 코드는 사용자를 플랫폼에 등록하기 때문에 나중에 사용자가 들어와서 shortened URL을 작성, 관리 및 추적할 수 있는 안전한 페이지를 만들 수 있습니다.
We also use the useContext hook to get our state values and functions:
또한 useContext 후크를 사용하여 상태 값과 함수를 가져옵니다.
import React, { useState, useContext, useEffect } from 'react'; import MyContext from '../lib/context'; const { isLoggedIn, setUser } = useContext(MyContext)
TypeScript
복사
Also, we use the useEffect hook to apply middleware on the page, so that only the unauthenticated user can access the page. We achieve this using the isLoggedIn state:
또한 인증되지 않은 사용자만 페이지에 액세스할 수 있도록 useEffect 후크를 사용하여 페이지에 미들웨어를 적용합니다. isLogged 상태를 사용하여 이를 실현합니다.
import React, { useState, useContext, useEffect } from 'react'; useEffect( () => { if (isLoggedIn) { return router.push("/dashboard"); } }, [isLoggedIn])
TypeScript
복사
If a user is authenticated, we redirect them back to their dashboard.
사용자가 인증되면 해당 사용자가 대시보드로 다시 리디렉션됩니다.
The submit method handles user registration, validates and sets the user state to the signed user if successful and then redirects the user to their dashboard:
submit 메서드는 사용자 등록을 처리하고, 성공한 경우 서명된 사용자에게 user 상태를 확인하고 설정한 후 사용자를 대시보드로 리디렉션합니다.
const submit = async () => { if(!username.trim()) return setErrors({ username: "Username must not be empty"}) if(!email) return setErrors({ email: "Email must not be empty"}) if(!password) return setErrors({ password: "Password must not be empty"}) setLoading(true); const reg = await (register(username, email, password)) setLoading(false); if (reg.jwt) { setUser(reg.user); router.push('/dashboard') } else{ setErrors({ server: reg?.error?.message || 'Error from server' }); } }
TypeScript
복사
As can be seen, we make use of a function called register, which handles the sending of a request to the Strapi API:
보시는 바와 같이 Strapi API에 대한 요청 전송을 처리하는 register라는 함수를 사용하고 있습니다.
import { register } from '../lib/auth' const reg = await register(username, email, password)
TypeScript
복사
We proceed to create this file (auth.js) in the lib folder. This file makes authenticated requests to our API and handles other auth-related functions like logout. Add the content below into the file:
이 파일(auth.js)을 lib 폴더에 작성합니다. 이 파일은 당사의 API에 인증된 요청을 하고 로그아웃과 같은 다른 인증 관련 기능을 처리합니다. 다음 내용을 파일에 추가합니다.
import Cookie from "js-cookie"; const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:1337"; export const register = async (username, email, password) => { try { let response = await fetch(`${API_URL}/api/auth/local/register`, { method: 'POST', body: JSON.stringify({ username, email, password }), headers: { 'Content-Type': 'application/json' }, }); response = await response.json(); if (response) { Cookie.set("jwt", response.jwt); } return response } catch (e) { return { error: 'An error occured' } } }; export const login = async (identifier, password) => { try { let response = await fetch(`${API_URL}/api/auth/local`, { method: 'POST', body: JSON.stringify({ identifier, password }), headers: { 'Content-Type': 'application/json' }, }); response = await response.json(); if (response) { Cookie.set("jwt", response.jwt); } return response } catch (e) { return { error: 'An error occured' } } }; export const logout = () => { Cookie.remove("jwt"); };
TypeScript
복사
As can be seen, we use the js-cookie package to assign the jwt once a user is logged in or registered, as well as delete this token once the user logs out.
이처럼 js-cookie 패키지를 사용하여 사용자가 로그인 또는 등록되면 jwt를 할당하고 사용자가 로그아웃하면 이 토큰을 삭제합니다.
This also leads us to create a .env at the root of our project. Inside it, we would have:
이를 통해 프로젝트의 루트에 .env를 만들 수 있습니다. 그 안에는 다음과 같은 것이 있습니다.
NEXT_PUBLIC_API_URL=http://localhost:1337
TypeScript
복사
Now, we would go on to create the Login file. Add the content below to the newly create pages/login.js file:
import Head from 'next/head' import React, { useState, useEffect, useContext } from 'react'; import MyContext from '../lib/context'; import { useRouter } from "next/router"; import { login } from '../lib/auth' import Link from 'next/link' export default function Login() { let [email, setEmail] = useState(""); let [password, setPassword] = useState("") const [loading, setLoading] = useState(false); const [errors, setErrors] = useState({}); const { isLoggedIn, setUser } = useContext(MyContext) const router = useRouter() const signIn = async () => { if(!email) return setErrors({ email: "Email must not be empty"}) if(!password) return setErrors({ password: "Password must not be empty"}) setLoading(true); const reg = await (login(email, password)) setLoading(false); if(reg.jwt){ setUser(reg.user); router.push('/') }else{ setErrors({ server: reg?.error?.message || 'Error from server' }); } } useEffect( () => { if (isLoggedIn) { return router.push("/dashboard"); } }, [isLoggedIn]) return ( <div className="flex flex-col items-center justify-center min-h-screen py-2"> <Head> <title>Create Next App</title> <link rel="icon" href="/favicon.ico" /> </Head> <main className="flex flex-col items-center justify-center w-full flex-1 px-20 text-center"> <h1 className="text-6xl font-bold text-blue-600"> Url Shortener </h1> <div className="flex flex-wrap items-center justify-around max-w-4xl mt-6 sm:w-full"> <form className="w-full max-w-lg mt-8" onSubmit={(e) => { e.preventDefault(); signIn(email, password) }}> <div className="flex flex-wrap -mx-3 mb-2"> <div className="w-full px-3 mb-6 md:mb-0"> <input onChange={ (e) => setEmail(e.target.value)} placeholder="Enter email..." className={`appearance-none block w-full text-gray-700 mb-4 border rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500 ${errors.email ? "border-red-500" : "border-gray-200"}`} id="grid-email" type="email" /> {errors.email ? ( <p className="text-red-500 text-xs italic">{errors.email}</p> ) : ''} </div> </div> <div className="flex flex-wrap -mx-3 mb-6"> <div className="w-full px-3"> <span className={`w-full inline-flex items-center rounded border border-r-1 text-gray-700 mb-2 text-sm focus:outline-none focus:bg-white focus:border-gray-500 ${errors.password ? "border-red-500 " : " border-gray-200"}`}> <input onChange={ (e) => setPassword(e.target.value)} placeholder="******************" className="appearance-none block rounded w-full py-3 px-4 leading-tight" id="grid-password" type='password' /> </span> {errors.password ? ( <p className="text-red-500 text-xs italic">{errors.password}</p> ) : ''} </div> </div> {errors.server ? ( <p className="text-red-500 text-xs italic">{errors.server}</p> ) : ''} <div className="flex flex-row flex-wrap justify-between"> <button disabled={loading} className={`w-full md:w-1/2 mt-3 flex justify-center align-center hover:bg-gray-200 hover:text-gray-900 rounded-md px-2 py-3 uppercase ${loading ? "bg-gray-200 text-black cursor-not-allowed" : "bg-gray-900 text-white cursor-pointer"}`}> {loading ? ( <> loading &nbsp;... </> ) : 'LOG IN'} </button> <span className="text-blue-600 hover:text-gray-600 pt-2 md:p-6"> <Link href="/register">Register</Link></span> </div> </form> </div> </main> </div> ) }
TypeScript
복사
The above code allows users to login and get access to the secured dashboard. It is similar to the register, only that it doesn't create users but checks their existence in the record and authenticates them.
위의 코드를 사용하면 사용자는 로그인하여 보안 대시보드에 액세스할 수 있습니다. register와 유사하지만 사용자를 생성하지 않고 레코드의 존재 여부를 확인하고 인증합니다.
This also makes use of the lib/auth.js file which we have seen already.
또, 이미 본 lib/auth.js파일을 사용합니다.
The remaining pages we would be looking at now are the:
그 외의 페이지는, 다음과 같습니다.
1.
Dashboard page: We would use this to handle the deletion and viewing of the shortened URLs.
2.
Add Url page: This is used to add a shortened URL.
3.
Alias page: This is used to redirect to the URL if the alias is found in our record.
1. 대시보드 페이지: 단축 URL 삭제 및 표시에 사용합니다.
2. Url 추가 페이지: 단축 URL을 추가하기 위해 사용합니다.
3. Alias 페이지: 이것은, alias가 레코드에서 검출되었을 경우에 URL 로 리다이렉트 하기 위해서 사용됩니다.

Building the Dashboard Page

As discussed earlier, this page shows all created records, as well as enables the user to test them and delete them.
앞서 설명한 바와 같이 이 페이지에는 작성된 모든 레코드가 표시되며 사용자는 레코드를 테스트하고 삭제할 수 있습니다.
Proceed to create a file called dashboard.js in the pages folder pages/dashboard.js. Insert the code below as its content:
페이지 폴더 pages/dashboard.jsdashboard.js라는 이름의 파일을 작성합니다. 아래 코드를 내용으로 삽입하십시오.
import Head from 'next/head' import React, { useEffect, useContext, useState } from 'react'; import MyContext from '../lib/context'; import { useRouter } from "next/router"; import Link from 'next/link'; import { logout } from '../lib/auth' import { get, deleteAlias } from '../lib/shortener' export default function Dashboard() { const { isLoggedIn, setUser, user, setUrls, urls } = useContext(MyContext) const router = useRouter() const getAll = async () => { let short = await get() if (!short) return setUrls(short?.data?.attributes?.results || null) } const deleteShort = async (id) => { if (!id) return let deleted = await deleteAlias(id) if (deleted.data && !deleted.error) { await getAll() } } useEffect(() => { if (!isLoggedIn) { return router.push("/login"); } getAll() }, [urls.length]) const signOut = () => { logout() setUser(null) router.push('/login') } return ( <div className="flex flex-col items-center justify-center min-h-screen py-2"> <Head> <title>Dashboard</title> <link rel="icon" href="/favicon.ico" /> </Head> <header className="flex justify-between align-center p-4 h-32 w-full text-6xl font-bold text-blue-600"> <h1 className="text-6xl font-bold text-blue-600"> Url Shortener </h1> <span className="text-sm font-bold text-red-600 cursor-pointer" onClick={() => signOut()}>Logout</span> </header> <main className="flex flex-col items-center w-full mt-0 flex-1 px-8 text-center"> <p className="flex flex-wrap w-full text-lg font-bold"> Welcome {user?.username || ""} </p> <div className="flex flex-wrap items-center justify-around max-w-4xl mt-6 sm:w-full"> <div className="shadow border-b w-full overflow-hidden border-gray-200 sm:rounded-lg"> <table className="min-w-full divide-y divide-gray-200"> <thead> <tr> <th scope="col" className="px-6 py-3 bg-gray-50 text-center text-xs font-medium text-gray-500 uppercase tracking-wider"> Url </th> <th scope="col" className="px-6 py-3 bg-gray-50 text-center text-xs font-medium text-gray-500 uppercase tracking-wider"> Alias/Shortned </th> <th scope="col" className="px-6 py-3 bg-gray-50 text-center text-xs font-medium text-gray-500 uppercase tracking-wider"> No of hits </th> <th scope="col" className="px-6 py-3 bg-gray-50"> <span className="sr-only">Remove</span> </th> </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {(!urls || urls.length == 0) && ( <tr> <td colSpan="3" className="px-2 py-4 whitespace-nowrap cursor-pointer"> No record found </td> </tr> )} {urls && urls.map(short => ( <tr className="hover:bg-gray-200" key={short.id}> <td className="px-2 py-4 whitespace-nowrap cursor-pointer" title = "Open Url" onClick={() => { window.open(`${short.url}`, 'blank') }}> <div className="text-sm text-gray-900">{short?.url || 'N/A'}</div> </td> <td className="px-2 py-4 whitespace-nowrap cursor-pointer" title = "Test Alias" onClick={() => { window.open(`/${short.alias}`, 'blank') }}> <div className="text-sm text-gray-900">{short?.alias || 'N/A'}</div> </td> <td className="px-2 py-4 whitespace-nowrap cursor-pointer"> <span className="px-2 text-xs leading-5 font-semibold rounded-full "> <div className="text-sm text-gray-500"> {short?.visit || 0} </div> </span> </td> <td className="px-2 py-2 whitespace-nowrap text-center text-sm font-medium"> <button onClick={() => deleteShort(short.id)} className="text-red-600 hover:text-red-900 mx-1">Delete</button> </td> </tr> ) )} </tbody> </table> </div> </div> </main> <Link href="/addUrl"> <button className="absolute rounded-full text-white font-bold text-lg p-2 bg-blue-800 w-12 h-12 m-4 right-0 bottom-0 hover:bg-blue-400"> + </button> </Link> </div> ) }
TypeScript
복사
In a nutshell, we use this to show users their shortened URLs. As can be seen, we use the useEffect hook to help prevent unauthenticated users from accessing the page.
Also, we have functions to handle deleting a record, getting all records, and logout users.
The functions that handle the delete and get, call a central shortener helper file called shortener.js:
간단히 말하면, 이 기능을 사용하여 사용자에게 단축된 URL을 표시합니다. 보시는 바와 같이 인증되지 않은 사용자가 페이지에 액세스하지 못하도록 하기 위해 useEffect 후크를 사용합니다.
또, 레코드 삭제, 모든 레코드 취득, 유저 로그아웃을 처리하는 기능도 있습니다.
deleteget을 처리하는 함수는 shortener.js라는 이름의 중앙 shortener.js helper 파일을 호출합니다.
import { get, deleteAlias } from '../lib/shortener'
TypeScript
복사
We use this file to handle all shortener related functionalities. Hence, we proceed to create this file inside the lib folder, lib/shortener.js, and add the code below as its content:
이 파일을 사용하여 shortener와 관련된 모든 기능을 처리합니다. 따라서 이 파일을 lib/shortener.js 폴더 내에 작성하고 다음 코드를 콘텐츠로 추가합니다.
import Cookie from "js-cookie"; const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:1337"; export const get = async () => { const token = Cookie.get("jwt"); try { let response = await fetch(`${API_URL}/api/shortners`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, }); response = await response.json(); return response } catch (e) { return { error: 'An error occured' } } }; export const getSingle = async (alias) => { try { let response = await fetch(`${API_URL}/api/shortners?alias=${alias}`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }); response = await response.json(); return response } catch (e) { return { error: 'An error occured' } } } export const create = async (url, alias) => { const token = Cookie.get("jwt"); try { let response = await fetch(`${API_URL}/api/shortners`, { method: 'POST', body: JSON.stringify({ data: { url, alias } }), headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, }); response = await response.json(); return response } catch (e) { return { error: 'An error occured' } } }; export const deleteAlias = async (id) => { const token = Cookie.get("jwt"); try { let response = await fetch(`${API_URL}/api/shortners/${id}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, }); response = await response.json(); return response } catch (e) { return { error: 'An error occured' } } };
TypeScript
복사

Building the Add URL Page

As discussed earlier, this page handles the creation of shortened URLs. Proceed to create a file called addUrl.js inside the pages folder, pages/addUrl.js.
앞서 설명한 바와 같이, 이 페이지는 단축 URL의 작성을 처리합니다.페이지 폴더 내에 addUrl.js라는 파일 pages/addUrl.js를 작성합니다.
Next, add the content below as its new content:
그런 다음 아래 내용을 새 콘텐츠로 추가합니다.
import Head from 'next/head'; import Link from 'next/link'; import React, { useEffect, useContext, useState } from 'react'; import MyContext from '../lib/context'; import { useRouter } from "next/router"; import { logout } from '../lib/auth'; import { create } from '../lib/shortener'; export default function AddUrl() { const { isLoggedIn, setUser } = useContext(MyContext) const [url, setUrl] = useState(""); const [alias, setAlias] = useState(""); const [loading, setLoading] = useState(false); const [errors, setErrors] = useState({}); const router = useRouter(); useEffect(() => { if (!isLoggedIn) { return router.push("/login"); } }, [isLoggedIn]); const shorten = async () => { if (!url) return setErrors({ url: "Url must not be empty" }) if (!alias) return setErrors({ alias: "Alias must not be empty" }) setLoading(true); const short = await(create(url, alias)) setLoading(false); if (short.data && !short.error) { router.push('/dashboard') } else { setErrors({ server: short?.error?.message || 'Error from server' }); } } const signOut = () => { logout(); setUser(null); router.push('/login'); } return ( <div className="flex flex-col items-center justify-center min-h-screen py-2"> <Head> <title>Add Url</title> <link rel="icon" href="/favicon.ico" /> </Head> <header className="flex justify-between align-center p-4 h-32 w-full text-6xl font-bold text-blue-600"> <h1 className="text-6xl font-bold text-blue-600"> Url Shortener </h1> <span className="text-sm font-bold text-red-600 cursor-pointer" onClick={() => signOut()}>Logout</span> </header> <main className="flex flex-col items-center w-full mt-0 flex-1 px-8 text-center"> <p className="flex flex-wrap w-full text-lg font-bold"> Fill the form </p> <div className="flex flex-wrap items-center justify-around max-w-4xl mt-6 sm:w-full"> <form className="w-full max-w-lg mt-8" onSubmit={(e) => { e.preventDefault(); shorten() }}> <div className="flex flex-wrap -mx-3 mb-2"> <div className="w-full px-3 mb-6 md:mb-0"> <input onChange={(e) => setUrl(e.target.value)} placeholder="Enter url" className={`appearance-none block w-full text-gray-700 mb-4 border rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500 ${errors.url ? "border-red-500" : "border-gray-200"}`} id="grid-url" type="text" /> {errors.url ? ( <p className="text-red-500 text-xs italic">{errors.url}</p> ) : ''} </div> </div> <div className="flex flex-wrap -mx-3 mb-2"> <div className="w-full px-3 mb-6 md:mb-0"> <input onChange={(e) => setAlias(e.target.value)} placeholder="Enter alias" className={`appearance-none block w-full text-gray-700 mb-4 border rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500 ${errors.alias ? "border-red-500" : "border-gray-200"}`} id="grid-alias" type="text" /> {errors.alias ? ( <p className="text-red-500 text-xs italic">{errors.alias}</p> ) : ''} </div> </div> {errors.server ? ( <p className="text-red-500 text-xs italic">{errors.server}</p> ) : ''} <div className="flex flex-row flex-wrap justify-between"> <span className="text-blue-600 hover:text-gray-600 pt-2 md:p-6"> <Link href="/dashboard"> Back to Dashboard</Link></span> <button disabled={loading} className={`w-full md:w-1/2 mt-3 flex justify-center hover:bg-gray-200 hover:text-gray-900 rounded-md px-3 py-3 uppercase ${loading ? "bg-gray-200 text-black cursor-not-allowed" : "bg-gray-900 text-white cursor-pointer"}`}> {loading ? ( <> loading &nbsp;... </> ) : 'Shorten'} </button> </div> </form> </div> </main> </div> ) }
TypeScript
복사
This is quite straightforward to understand, we simply make use of the shortener file in the lib folder to make a request to our Strapi API to add the record.
이것은 이해하기 매우 간단합니다.우리는 단순히 lib 폴더에 있는 단축기 파일을 사용하여 Strapi API에 레코드 추가를 요청합니다.
We also make use of the useEffect hook to prevent unauthenticated users from accessing the page.
또, 인증되지 않은 유저가 페이지에 액세스 하는 것을 막기 위해서, use Effect를 쓴다.
useEffect로 로그인 상태인지 확인해서 login페이지로 리디렉션한다. 하지만 아래와 같이 처음에는 기본값인 false가 들어온다. 그러면 들어올 때마다 false부터 인식해서 바로 login로 이동되어 버린다.
리액트였다면 정상적으로 작동했을 것이다.
첫 렌더링에서는 왜 로그인 상태인데도 기본값을 먼저 주는 것인가? Context API보다 리덕스를 많이 사용해와서 이상하다. Nextjs에서 주로 발생하는 window is not defined과는 연관이 없다.,
cookie에 jwt가 존재하므로 조건을 변경시켜서 해결.

Building the Alias Page

This page is the one which is in charge of checking if the alias exists in our record and redirecting the user accordingly.
이 페이지는 당사의 기록에 alias가 존재하는지 확인하고 그에 따라 사용자를 리다이렉트하는 페이지입니다.
Subsequently, if an alias is found in our record, the Strapi API records that as a visit to the alia, giving us the ability to see analytics ofa particular alias.
그 후, 당사의 기록에서 alias가 발견되면 Strapi API는 그 alias를 방문으로 기록하여 특정 alias의 분석을 볼 수 있습니다.
We proceed to create a file called [alias].js in the pages folder, pages/[alias].js. If this looks strange, check how to build pages with dynamic routes in the Next.js.
alias.js라고 하는 이름의 파일을 작성합니다. 페이지 폴더의 pages/[alias].js에 있습니다. 이상할 경우 Next.js에서 dynamic routes를 사용하여 페이지를 작성하는 방법을 확인합니다.
Next, insert the content below as the content of this file:
다음으로 다음 내용을 이 파일의 내용으로 삽입합니다.
import { useRouter } from "next/router"; import { useEffect } from "react"; import { getSingle } from "../lib/shortener"; const AliasView = ({ error }) => { const router = useRouter() useEffect(() => { if (error) { return router.push('/') } }, []) return null }; export async function getServerSideProps({ params }) { const url = await getSingle(params.alias) if (url.data && (url.data?.attributes?.results[0] || false) && !url.error) { return { redirect: { destination: url.data.attributes.results[0].url, permanent: false, }, } } return { props: { error: "error" } } } export default AliasView; As can be seen, we use the `getServerSideProps` to check if the alias exists in our record, if so we redirect to the actual URL. export async function getServerSideProps({ params }) { const url = await getSingle(params.alias) if (url.data && (url.data?.attributes?.results[0] || false) && !url.error) { return { redirect: { destination: url.data.attributes.results[0].url, permanent: false, }, } } return { props: { error: "error" } } } If we can’t find it, we pass the `error` prop to the actual component: return { props: { error: "error" } } Then in our component, we redirect the user to the home page since the alias isn't in our record. const AliasView = ({ error }) => { const router = useRouter() useEffect(() => { if (error) { return router.push('/') } }, []) return null };
TypeScript
복사
If the user is authenticated, they would get redirected to the Dashboard page, else they would get redirected to the Login page. Did we implement this feature on the Index.js page? Yes, we did!
사용자가 인증되면 Dashboard 페이지로 리디렉션되고 그렇지 않으면 Login 페이지로 리디렉션됩니다. Index.js 페이지에서 이 기능을 구현했습니까? 네, 했어요!
And that's it for the code section of the frontend part of this tutorial. If you have come this far, I must say you are doing great!
이 튜토리얼의 프런트 엔드 부분의 코드 섹션은 여기까지입니다. 여기까지 왔으면 잘 하고 있다고 말할 수 있겠군!
Enough for all this technical stuff, in the next section, we would be seeing a demo of the finished app.
이 모든 기술적인 부분에 대해서는 다음 섹션에서 완성된 앱의 데모를 볼 수 있습니다.

Testing Finished App

The finished app looks like this:

Conclusion

The benefits a URL Shortener brings can’t be over-emphasized. This is seen in the rapid outbreak of companies playing in this space. You can go ahead to add more features to this project, by simply forking the repo (found at beginning of this tutorial) and getting your hands dirty. It's best left to the imagination what you can achieve!
URL 단축 서비스가 가져오는 이점은 아무리 강조해도 지나치지 않습니다. 이것은, 이 공간에서 활약하는 기업의 급속한 발생에서 볼 수 있다. 레포(이 튜토리얼의 선두에 있는 것)를 포킹하고 손을 더럽히는 것만으로, 이 프로젝트에 기능을 추가할 수 있습니다. 무엇을 달성할 수 있을지는 상상에 맡기는 것이 최선입니다!
This tutorial demonstrated how easily you can build a URL Shortener Service in about 20 minutes using technologies like Next.js and Strapi. Once again, Strapi has shown us that it is equal to the task when it comes to creating great APIs!
이 튜토리얼에서는 Next.js 및 Strapi 등의 기술을 사용하여 URL 단축 서비스를 약 20분 만에 쉽게 구축할 수 있음을 시연했습니다. 다시 한 번 [Strapi](https://strapi.io/))를 통해 훌륭한 API를 만들 수 있다는 것을 알 수 있었습니다!

url을 만들고 /ds에 요청하는 경우 넘어오는 값이 이상하다.

넘어온 results가 []이다. 제대로 만들어지지 않은 것 같다. adadurl을 고친다.