React

How to build a Marketing Site with React and Strapi

Asset Type
File Type
When to use
2022/03/24
Reference
Created time
2022/03/24 07:02
Created by
Last edited time
2022/05/07 13:12

Goal

이 글에서 우리는 form 기능을 가진 스타텁 사이트를 만들 겁니다. 새로운 집을 찾는 걸 돕는 부동산 사이트이고, strapi 백엔드, react 프론트엔드로 사용할 겁니다.
또한, 우리는 매월 자사 제품에 대한 이메일을 받는 것을 원하는 유저들의 뉴스레터 폼을 준비할 것입니다.
유저는 회원가입 후 회원가입이 성공했다는 확인 메일을 받을 것입니다.

An Introduction to Strapi

Strapi는 백엔드 API 서비스에 맞게 커스터마이즈 및 유연성을 제공할 수 있도록 자체 호스팅되는 오픈 소스 헤드리스 CMS 솔루션입니다.
Strapi의 장점 중 하나는 프런트엔드에서 마이크로 서비스로 사용할 수 있는 엔드포인트를 공개하기 위한 API의 scaffolding이 쉽다는 것입니다. Strapi는 RESTful API와 GraphQL API를 모두 지원합니다.

Scaffolding이란?

MVC 모델 기반 앱을 만드려 할 때 생산하는 복잡하고 많은 양의 코드를 앱이 제공하는 템플릿 기반으로 MVC에 관한 코드를 자동으로 생성해주는 기능입니다.

Prerequisites

이 기사를 계속 쓰기 전에 먼저 알아두셔야 합니다.
React: 이 문서는 React에 대한 소개가 아니라 백엔드 서비스에 Strapi를 사용하는 방법에 대해 설명합니다. Head over to React’s official documentation to get started with React.
Strapi의 CLI 설치 가이드에서는 컴퓨터에 다음 두 가지를 설치해야 합니다.
Nodejs: v14 이상 버전이 지원됩니다. v14를 사용하려면 명령어를 실행합니다. nvm ls // to see all versions available nvm use 14 // to use version 14 nvm alias default v14 // to make v14 your default version
Strapi는 npm의 v6만 지원합니다. v6으로 다운그레이드하려면 npm install npm@6을 실행합니다.
Postman: Strapi 엔드포인트를 FE에 연결하기 전에 Postman를 사용하여 테스트합니다.

Backend Setup

백엔드 세팅을 위해 Strapi를 사용할 것이므로 Strapi 프로젝트를 스캐폴드하고 설치할 겁니다.
주의 : 이 기사는 Strapi(v4.1.2) 최신 버전으로 써졌고 테스트했습니다.(Node16 and yarn)

Strapi Setup

Strapi 프로젝트를 스캐폴드/설치하기 위해, Marketing-Site 파일을 생성해서 cd로 들어간다음 아래를 실행하세요.
cd Marketing-Site npx create-strapi-app@latest backend --quickstart
Bash
복사
or
cd Marketing-Site yarn create strapi-app backend --quickstart
Bash
복사
위에서 Marketing-Site 폴더 내에 backend라는 프로젝트를 만들고 --quickstart 플래그를 사용하여 quickstart 설치 유형을 선택합니다.
설치 후 yarn develop를 실행하여 새 프로젝트를 시작합니다. 이 명령어는 브라우저에 새 관리자를 등록하기 위한 페이지가 있는 탭을 엽니다. 양식을 작성하여 제출하여 Strapi 대시보드에 로그인합니다.
등록 후 관리자 페이지로 이동하여 백엔드 콘텐츠와 API를 설정합니다.

Create a Collection

Estate 컬렉션 유형을 만듭니다. 관리자 홈페이지에서 Content-Type Builder를 클릭한 다음 Create a new collection type를 클릭합니다.
컨텐츠 타입을 작성하기 위해서, Content-Type의 name으로 Estate를 입력하도록 요구하는 메세지가 표시됩니다. Continue를 클릭하여 웹 사이트에서 원하는 필드를 추가합니다.
이 튜토리얼에서는 name, description, image and price of our houses만 있으면 됩니다.
필드를 선택한 후 Finish를 클릭하여 모드를 종료합니다.
페이지 오른쪽 상단에 저장 및 게시 버튼이 있습니다.
컬렉션을 저장하려면 저장을 클릭하고 컬렉션을 게시하려면 게시를 클릭합니다.
다음과 같은 이미지가 필요합니다.

Populate the Collection

admin 페이지의 왼쪽 상단에 있는 Content Manager를 클릭합니다.
그러면 데이터베이스를 채울 페이지로 이동합니다.
다른 집을 추가하려면 Add new entry를 클릭하십시오. 우리는 name, description, pictures and price of the house이 필요하므로, 계속해서 collection에 당신의 데이터를 입력하세요.
그런 다음 페이지 오른쪽 상단에 있는 publish 버튼을 클릭하여 컬렉션을 게시합니다.
이제 리액트 프런트엔드에서 사용할 수 있는 부동산 경로를 만들어 보겠습니다.
Settings을 클릭하여 Settings 페이지로 이동한 후 Users and Permissions 아래에 있는 Roles을 클릭합니다.
다음으로 Public을 클릭하여 Estate 드롭다운에서 findfindOne 옵션을 선택합니다.
이것은 이 두 개의 부동산 경로를 공개적으로 이용할 수 있게 합니다.

Test the Routes with Postman

Postman에서 경로를 테스트하여 Strapi API가 어떻게 반환되는지 확인할 수 있습니다.
다음은 http://localhost:1337/api/estates가 반환하는 내용입니다.

Frontend Setup

React를 사용하여 프런트엔드를 만들고 axios and react router packages를 설치하여 Strapi에 연결합니다.
React 앱을 회전시키려면 React 앱을 만들기 전에 먼저 백엔드 폴더에서 나와서 이동하거나 새 터미널을 열고 Marketing-Site 폴더 내에서 다음 명령을 실행합니다.
npx create-react-app frontend cd frontend npm i axios react-router-dom --save npm start
Plain Text
복사
아래는 우리의 폴더 구조입니다. 헤매지 않도록, 페이지도 명확하게 기술하고 설명하겠습니다.
폴더 구조에 따라 아래 코드를 작성했습니다.
따라서 복사하여 붙여넣을 수 있습니다.
// frontend/src/hooks/useFetch.js import { useState, useEffect } from 'react'; import axios from 'axios'; export default function useFetch(url) { const [ estate, setEstate ] = useState(null); const [ error, setError ] = useState(null); const [ loading, setLoading ] = useState(true); useEffect( () => { const fetchData = async () => { setLoading(true); try { const res = await axios.get(url); setEstate(res.data.data); setLoading(false); } catch (error) { setError(error); setLoading(false); } }; fetchData(); }, [ url ] ); return { estate, error, loading }; }
TypeScript
복사
// frontend/src/pages/about/About.js import React from 'react'; import { useParams, Link } from 'react-router-dom'; import classes from './about.module.css'; import useFetch from '../../hooks/useFetch'; function AboutPage() { const { id } = useParams(); const { loading, error, estate } = useFetch(`http://localhost:1337/api/estates/${id}?populate=*`); if (loading) return <p> Loading... </p>; if (error) return <p> Error :( </p>; return ( <article className={classes.aboutPage}> <h2>More Description</h2> <hr /> <section className={classes.aboutBoard}> <h2>{estate.attributes.name}</h2> <div className={classes.aboutDescription}> <div className={classes.aboutImgContainer}> {estate.attributes.image.data ? ( estate.attributes.image.data.map((pic) => ( <img src={`http://localhost:1337${pic.attributes.url}`} alt="img" key={pic.attributes.id} /> )) ) : ( <img src={`http://localhost:1337${estate.attributes.image.data.attributes.url}`} alt="img" /> )} </div> <div> <h3>{estate.attributes.price}</h3> <p>{estate.attributes.description}</p> <Link to={'/'} style={{ textDecoration: 'none', background: 'black', color: 'white', border: '1px solid black', padding: '5px 10px' }} > {'< Back to Home'} </Link> </div> </div> </section> </article> ); } export default AboutPage;
TypeScript
복사
// frontend/src/pages/estates/Estates.js import React from 'react'; import { Link } from 'react-router-dom'; import useFetch from '../../hooks/useFetch'; import classes from './estates.module.css'; export default function Estatepage() { const { estate, error, loading } = useFetch('http://localhost:1337/api/estates?populate=*'); if (loading) return <p> Loading... </p>; if (error) return <p> Error :( </p>; return ( <div className={classes['estates']}> <section> <h2>Available Houses</h2> <hr className={classes['horizontal-rule']} /> {estate.map((house) => ( <article className={classes['article']} key={house.id}> <h2>{house.attributes.name}</h2> <section className={classes['article-description']}> <img src={`http://localhost:1337${house.attributes.image.data[0].attributes.url}`} alt="img" /> <div> <p>{house.attributes.price}</p> <p>{house.attributes.description}</p> <Link to={`${house.id}`}>See More...</Link> </div> </section> </article> ))} </section> </div> ); }
TypeScript
복사
// frontend/src/pages/home/Home.js import React from 'react'; import { Link } from 'react-router-dom'; import useFetch from '../../hooks/useFetch'; import classes from './home.module.css'; export default function Homepage() { const { estate, error, loading } = useFetch('http://localhost:1337/api/estates?populate=*'); if (loading) return <p> Loading... </p>; if (error) return <p> Error :( </p>; return ( <div className={classes['home']}> <section> <h2>Welcome to our Estate</h2> <hr className={classes['horizontal-rule']} /> <p>We help you find your new home</p> <form className={classes["home-form"]}> <h5>Interested in joining our Newsletter</h5> <h6>Sign up with your email below</h6> <label htmlFor="email"> Email Address: <input type="email" /> </label> <button>Signup</button> </form> {estate.splice(0, 2).map((house) => ( <article className={classes['home-article']} key={house.id}> <h2>{house.attributes.name}</h2> <section className={classes['home-article-description']}> <img src={`http://localhost:1337${house.attributes.image.data[0].attributes.url}`} alt="img" /> <div> <p>{house.attributes.price}</p> <p>{house.attributes.description}</p> <Link to={`estates/${house.id}`}>See More...</Link> </div> </section> </article> ))} </section> </div> ); }
TypeScript
복사
// frontend/src/pages/nav/Nav.js import React from 'react'; import { Link } from 'react-router-dom'; import classes from './nav.module.css'; export default function NavHeader() { return ( <div className={classes.navBar}> <h1>My Estate</h1> <nav className={classes.navLink}> <ul> <Link to="/" style={{ textDecoration: 'none' }}> <li>Home</li> </Link> <Link to="estates" style={{ textDecoration: 'none' }}> <li>Estates</li> </Link> </ul> </nav> </div> ); }
TypeScript
복사
// frontend/src/App.js import React, { Suspense } from 'react'; import { Routes, Route } from 'react-router-dom'; import Nav from './pages/nav/Nav'; import Home from './pages/home/Home'; const About = React.lazy(() => import('./pages/about/About')); const Estates = React.lazy(() => import('./pages/estates/Estates')); export default function App() { return ( <div> <Nav /> <Routes> <Route path="/" element={<Home />} /> <Route path="estates" element={ <Suspense fallback={<p>Loading...</p>}> <Estates /> </Suspense> } /> <Route path="estates/:id" element={ <Suspense fallback={<p>Loading...</p>}> <About /> </Suspense> } /> </Routes> </div> ); }
TypeScript
복사
useFetch를 axios로 하게 되면 데이터가 이상하게 들어오는데 fetch는 정상으로 옴. 다시 axios로 변경하니 잘 들어온다. data로 접근하면 정상인데, data 안의 data를 직접 접근하려고 하니 들어오지 않는다.
위의 App.js 파일 안에 Suspense API라고 불리는 React 18 feature를 구현했습니다.
React’s official page에 따르면 "<Suspense>는 데이터를 포함한 다른 모든 것을 선언적으로 "wait"할 수 있는 새로운 기능입니다.
데이터 페칭 라이브러리가 컴포넌트가 읽고 있는 **data가 아직 준비되지 않았음을 React에 전달하는 메커니즘입니다.
그런 다음 준비가 될 때까지 기다렸다가 UI를 업데이트할 수 있습니다."
// frontend/src/index.js import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; import App from './App'; ReactDOM.render( <React.StrictMode> <BrowserRouter> <App /> </BrowserRouter> </React.StrictMode>, document.getElementById('root') );
TypeScript
복사
위의 파일들은 다음과 같이 동작합니다.
1. fetch hook을 useFetch.js 파일에 분리하였습니다. 이렇게 하면 매번 같은 로직을 쓸 필요가 없어집니다. 대신 필요한 컴포넌트에서 호출합니다.
2. 델의 Home 컴포넌트와 Estates 컴포넌트에 Import했습니다. Axios로 fetch 성공 후 데이터를 표시하기 위해 반환된 배열을 매핑했습니다. 각 데이터를 클릭하면 About 페이지로 리다이렉트 됩니다. 이 페이지에는 다양한 pictures, prices 등과 함께 집의 전체 설명이 표시됩니다.
3. Nav.js에는 당사 웹사이트의 이름이 저장된 정적 nav header와 Estate 및 Home 앵커 링크가 포함되어 있습니다.
4. 또, Home.js에는 폼 요소가 있습니다. 이 폼은 이 기사의 주요 내용 중 하나인 뉴스레터 등록에 사용됩니다.
5. React 앱을 실행하여 결과를 보려면 터미널을 열고 다음 명령 중 하나를 실행합니다.
// Yarn yarn start
Plain Text
복사
//Npm npm start
Plain Text
복사
현재 Collection Types을 편집하거나 새 entries를 만드는 것만으로 Strapi를 사용하여 콘텐츠를 쉽게 변경하고 추가할 수 있습니다.
프런트엔드 셋업과 통합을 완료했습니다. 다음으로 뉴스레터 통합에 대해 알아보겠습니다.

SendGrid Setup

Mailchimp, MailerLite, Sendinblue와 같은 이메일 프로바이더가 많이 있습니다.
그러나 이 기사에서는 SendGrid라는 이메일 공급자를 사용합니다.
SendGrid 서비스를 설정하려면 먼저 SendGrid 계정을 만듭니다. SendGrid API를 통해 SendGrid를 Strapi에 연결해야 하기 때문입니다. Head over to SendGrid to sign up and create your account.(https://signup.sendgrid.com/)
After logging into your dashboard, click on the S**ettings dropdown on the left side of the dashboard and click on Sender Authentication. Proceed to create a new sender and make sure to verify the email address too.
Next, we will create our API key. On the left side of the dashboard, click on the Settings dropdown again and click on API keys. Click on create API key, give it a name and copy your API key.
Note: Make sure to copy your API key and store it somewhere safe because SendGrid won’t show it to you again. You can always create a new key though if you loose or forget where you stored it.
Next, we will go into our backend folder and run any of the commands below to download strapi email provider plugin.
// using yarn yarn add @strapi/provider-email-sendgrid --save // using npm npm install @strapi/provider-email-sendgrid --save
Plain Text
복사
After we have successfully downloaded the plugin, we’ll set it up in our backend folder. In the config folder, create a new file called plugins.js and paste the code below:
// config/plugins.js module.exports = ({ env }) => ({ email: { provider: 'sendgrid', providerOptions: { apiKey: env('SENDGRID_API_KEY') }, settings: { defaultFrom: 'myemail@protonmail.com', defaultReplyTo: 'myemail@protonmail.com' } } });
TypeScript
복사
Replace the settings default emails with your SendGrid verified email. Also, in your .env file, add your SendGrid API key.
SENDGRID_API_KEY=SG.5hoLikrVQXudcUtgaV6n6g.aKttCp***********************************
Plain Text
복사
After that, head over to the api folder inside src folder and create a new folder called subscribe. Inside our subscribe folder, we will also create two extra folders: config and controllers. In our config folder, create a new routes.json file and add the code below.
// src/api/subscribe/config/routes.json { "routes": [ { "method": "POST", "path": "/email", "handler": "email.send", "config": { "policies": [] } } ] }
TypeScript
복사
Then, in our controllers folder create an email.js file, and add the following code
// src/api/subscribe/controllers/email.js module.exports = { send: async (ctx) => { let options = ctx.request.body; await strapi.plugins.email.services.email.send({ to: options.to, from: 'yourmail@gmail.com', replyTo: 'yourmail@gmail.com', subject: options.subject, text: options.html }); ctx.send('Email sent!'); } };
TypeScript
복사
We will now test our configuration in Postman and see what we get. Before that, make sure you make the email route publicly available in your Strapi admin settings.
(Settings > Users and Permissions Plugin > Roles > Public > Email)
Then, in our postman, let’s test our API to see if it works.
We can see we got a status of 200 meaning the request was sent successfully. Log in to your email account to see the test message.
Finally, We will now integrate our Strapi subscribe functionality into our React app.
Head over to your frontend folder. Under the hooks folder where we created our useFetch.js file, create a new file called usePost.js. We will be putting our POST logic here; then, we will import it into our Home file.
// frontend/src/hooks/usePost.js import { useState } from 'react'; import axios from 'axios'; const usePost = (url) => { const [ signup, setSignup ] = useState(''); const [ signupError, setError ] = useState(null); const [ signupMsg, setSignupMsg ] = useState(''); const [ signupLoading, setSignupLoading ] = useState(true); const handleChange = (e) => { setSignup(e.target.value); }; const handleSignup = (e) => { e.preventDefault(); let userData = { to: signup, from: 'chimezieinnocent39@gmail.com', replyTo: 'chimezieinnocent39@gmail.com', subject: 'Thanks for signing up', html: "<h3>Hi!,</h3> <p>You've been subscribed to our primary newsletter. You can expect to receive an email from us every few weeks, sharing the new things that we've published and new houses to check out. Occasionally, We'll share unique newsletter-only content as well</p><p>Thanks for choosing us!</p>" }; axios .post(url, userData) .then((res) => { setSignup(res); setSignupMsg(true); setSignupLoading(false); }) .catch((signupError) => { setError(signupError); setSignupLoading(false); }); }; return { signup, signupError, signupMsg, signupLoading, handleChange, handleSignup }; }; export default usePost;
TypeScript
복사
Let us import it in our Home file below:
// frontend/src/pages/home/Home.js import React from 'react'; import { Link } from 'react-router-dom'; import useFetch from '../../hooks/useFetch'; import usePost from '../../hooks/usePost'; import classes from './home.module.css'; export default function Homepage() { const { estate, error, loading } = useFetch('http://localhost:1337/api/estates?populate=*'); const { signup, signupError, signupMsg, signupLoading, handleChange, handleSignup } = usePost( 'http://localhost:1337/api/email' ); if (loading && signupLoading) return <p> Loading... </p>; if (error) return <p> Error :( </p>; return ( <div className={classes['home']}> <section> <h2>Welcome to our Estate</h2> <hr className={classes['horizontal-rule']} /> <p>We help you find your new home</p> <form className={classes['home-form']} onSubmit={handleSignup}> <h5>Interested in joining our Newsletter</h5> <h6>Sign up with your email below</h6> <label htmlFor="email"> {signupError ? <p> {signupError} </p> : null} Email Address: <input type="email" name="email" value={signup} onChange={handleChange} /> {signupMsg ? <p> Thanks for signing up!</p> : null} </label> <button>Signup</button> </form> {estate.splice(0, 2).map((house) => ( <article className={classes['home-article']} key={house.id}> <h2>{house.attributes.name}</h2> <section className={classes['home-article-description']}> <img src={`http://localhost:1337${house.attributes.image.data[0].attributes.url}`} alt="img" /> <div> <p>{house.attributes.price}</p> <p>{house.attributes.description}</p> <Link to={`estates/${house.id}`}>See More...</Link> </div> </section> </article> ))} </section> </div> ); }
TypeScript
복사
Go ahead and test your app.

Conclusion

We have seen how to use Strapi with React to build a startup website. We’ve also seen how to Integrate SendGrid with Strapi and React to create a newsletter email form.
Lastly, we have seen how to implement one of React’s new features— React Suspense — and what it does. I hope you understood what we did and can now implement same in your projects.