๐Ÿ”Ž Algolia ๊ฒ€์ƒ‰์„ NextJS ๋ฐ ๋งˆํฌ๋‹ค์šด ํŒŒ์ผ๊ณผ ์–ด๋–ป๊ฒŒ ํ†ตํ•ฉํ–ˆ์Šต๋‹ˆ๊นŒ?

7382 ๋‹จ์–ด devlognextjs
2019๋…„๋ถ€ํ„ฐ 1์ธ ๊ฐœ๋ฐœ์ž๋กœ SaaS for translation management์„ ๊ตฌ์ถ•ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ฒ˜์Œ๋ถ€ํ„ฐ ์ฝ”๋“œ ์ƒ˜ํ”Œ์„ ๊ฒŒ์‹œํ•˜๊ณ  ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ํ†ตํ•ฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‚˜ SimpleLocalize CLI๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๊ณ  ๋‹ค์šด๋กœ๋“œํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋Š” ์žฅ์†Œ๊ฐ€ ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค. SimpleLocalize documentation์—์„œ Algolia ๊ฒ€์ƒ‰์ด ์ž‘๋™ํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.



์ฒ˜์Œ์—๋Š” GitBook ์†Œํ”„ํŠธ์›จ์–ด๊ฐ€ ๊ทธ ๋‹น์‹œ ๋น›๋‚˜๊ณ  ํŠธ๋ Œ๋””ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉํ•˜๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ๊ฒƒ์„ ์‹œ์ž‘ํ•  ๋•Œ ๋ชจ๋“  ๋‹จ๊ณ„์—์„œ ์ฒ˜์Œ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๊ณ  ์‹ถ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ข‹์€ ์กฐ์น˜์˜€์Šต๋‹ˆ๋‹ค. ์ฃผ์š” ๋ชฉํ‘œ๋Š” ๊ณ ๊ฐ์—๊ฒŒ ์ข‹์€ ํ’ˆ์งˆ์˜ ์ฝ”๋“œ์™€ ๊ฐ€์น˜๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์‹œ๊ฐ„์„ ์ ˆ์•ฝํ•˜๊ธฐ ์œ„ํ•ด ๊ตฌ๋งคํ•  ์ˆ˜ ์žˆ๋Š”(๋˜๋Š” ๊ตฌ๋งคํ•ด์•ผ ํ•˜๋Š”) ๋‹ค๋ฅธ ๋ชจ๋“  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ฐ„๋‹จํžˆ ๋งํ•ด ๋ช‡ ๋‹ฌ ํ›„ GitBook์ด ๋งˆ์Œ์— ๋“ค์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋‚ด ํ•„์š”์— ๋งž์ง€ ์•Š์•˜๊ณ  ์Šคํƒ€์ผ์ด ๋งˆ์Œ์— ๋“ค์ง€ ์•Š์•˜๊ณ  ๋งํฌ๋ฅผ ์กฐ์ •ํ•˜๊ณ  ์ˆ˜์ •ํ•˜๋Š” ๋ฐ ๋„ˆ๋ฌด ๋งŽ์€ ์‹œ๊ฐ„์„ ์†Œ๋น„ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์—. ๋งž์ถคํ˜• ๋ฌธ์„œ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๊ณ  ๋†€๋ž๊ฒŒ๋„ ๋ชจ๋“  ๊ฒƒ์ด ์ˆœ์กฐ๋กญ๊ฒŒ ์ง„ํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ์œ ์ผํ•œ ์ฝ˜์„œํŠธ๋Š” ๋ชจ๋“  ๋ฌธ์„œํ™” ์†”๋ฃจ์…˜์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์šฐ์ˆ˜ํ•œ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์ด ๋ถ€์กฑํ•˜๊ฑฐ๋‚˜ open-source Docusaurus .

์„ค์น˜



Algolia ๊ฒ€์ƒ‰ ๊ตฌ์„ฑ ์š”์†Œ ๋ฐ Algolia ํด๋ผ์ด์–ธํŠธ ์„ค์น˜

npm install react-instantsearch-dom algoliasearch --save



gray-matter Markdown ํŒŒ์„œ ๋ฐ globby ํŒŒ์ผ ์ฐพ๊ธฐ

npm install gray-matter globby --saveDev



๊ตฌ์„ฑ




import algoliasearch from 'algoliasearch/lite';
import {connectStateResults, Hits, InstantSearch, SearchBox} from 'react-instantsearch-dom';

const searchClient = algoliasearch(
  'APP_ID',
  'SEARCH_API_KEY'
);



๊ฒฐ๊ณผ ๊ตฌ์„ฑ ์š”์†Œ



๊ธฐ๋ณธ์ ์œผ๋กœ Algolia<Hits/> ๊ตฌ์„ฑ ์š”์†Œ๋Š” ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ๊ฐ€ ๋น„์–ด ์žˆ์„ ๋•Œ ๋ชจ๋“  ๊ฒฐ๊ณผ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ๊ฐ€ ์žˆ๊ณ  ๋ฐ์ดํ„ฐ๊ฐ€ ๋กœ๋“œ๋œ ๊ฒฝ์šฐ์—๋งŒ ๊ฒฐ๊ณผ๊ฐ€ ํ‘œ์‹œ๋˜๋„๋ก ์ฝ”๋“œ๋ฅผ ์กฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

const Results = connectStateResults(({searchState, searchResults, searching}) => {
  const hasQuery = searchState && searchState.query;
  const hasResults = (searchResults?.hits ?? []).length > 0;
  const isSearching = searching;
  if (hasQuery && hasResults) {
    return <Hits hitComponent={Hit}/>;
  }
  if (hasQuery && !hasResults && !isSearching) {
    return <div>No results ๐Ÿ˜”</div>
  }
  return null;
}
);



์ ์ค‘ ๊ตฌ์„ฑ ์š”์†Œ



์ ์ค‘ ๊ตฌ์„ฑ ์š”์†Œ๋Š” ํ•˜๋‚˜์˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์ผ ๋ฟ์ž…๋‹ˆ๋‹ค. ๋ณต์‚ฌ-๋ถ™์—ฌ๋„ฃ๊ธฐ๊ฐ€ ๋” ์‰ฝ๋„๋ก ์Šคํƒ€์ผ์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ˜„

import React from "react";

function Hit(props: any) {
  const content = props?.hit?.content ?? "";
  const words = content.split(" ").length;
  return (<a href={props.hit.slug}>
    <div>
      <h3>{props?.hit?.frontmatter?.title ?? "no title"}</h3>
    </div>
    <p>{props?.hit?.frontmatter?.excerpt ?? ""}</p>
  </a>)
}

export default Hit;



๊ฒ€์ƒ‰ ๊ตฌ์„ฑ์š”์†Œ



์ด์ „ ๋‹จ๊ณ„์—์„œ ๊ตฌ์„ฑํ•œ InstantSearch ๊ตฌ์„ฑ ์š”์†Œ ๋ฐ ๊ฒ€์ƒ‰ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์ธ๋ฑ์Šค ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜๊ณ  AlgoliaSearchBox ๊ตฌ์„ฑ ์š”์†Œ์™€ ์ด์ „์— ๋งŒ๋“  ๋นˆ ์ƒํƒœ ๋™์ž‘์ด ๋ณ€๊ฒฝ๋œ ์‚ฌ์šฉ์ž ์ง€์ •Results ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

<InstantSearch indexName="simplelocalize-docs" searchClient={searchClient}>
    <SearchBox/>
    <Results/>
</InstantSearch>



ํ•„์š” ์ด์ƒ์œผ๋กœ ๋ณต์žกํ•˜๊ฒŒ ๋งŒ๋“ค๊ณ  ์‹ถ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ๊ณผ ์žฌ์„ค์ • ๋ฒ„ํŠผ์„ ์ˆจ๊ธฐ๊ธฐ ์œ„ํ•ด ์Šคํƒ€์ผ์‹œํŠธ์˜ ์ผ๋ถ€ CSS ์†์„ฑ์„ ๋ฎ์–ด์”๋‹ˆ๋‹ค. ๋˜ํ•œ SCSS@extend ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฒ€์ƒ‰์ฐฝ์— ๋ถ€ํŠธ์ŠคํŠธ๋žฉ ์Šคํƒ€์ผ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ„๋‹จํ•˜๊ณ  ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

.ais-SearchBox-input {
  @extend .form-control;
}

.ais-SearchBox-submit {
  display: none;
}

.ais-SearchBox-reset {
  display: none;
}



์ธ๋ฑ์‹ฑํ•  ๋งˆํฌ๋‹ค์šด ํŒŒ์ผ ๊ฐ€์ ธ์˜ค๊ธฐ



์ด์ œ ์ธ๋ฑ์‹ฑ๋˜๊ณ  ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์— ํ‘œ์‹œ๋˜์–ด์•ผ ํ•˜๋Š” ๋ชจ๋“  ํŒŒ์ผ์„ ๊ฐ€์ ธ์˜ฌ ๋•Œ์ž…๋‹ˆ๋‹ค. CI/CD ์„œ๋ฒ„์—์„œ ์„ฑ๊ณต์ ์œผ๋กœ ๋นŒ๋“œํ•  ๋•Œ๋งˆ๋‹ค ์‹คํ–‰ํ•  ์ƒˆindex-docs.js ํŒŒ์ผ์„ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ๋ฌธ์„œ ํŽ˜์ด์ง€๋Š” /docs/ ๋””๋ ‰ํ† ๋ฆฌ์— ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ Algolia ๊ฒ€์ƒ‰ ์ƒ‰์ธ์„ ์ฑ„์šฐ๊ธฐ ์œ„ํ•ด ๊ฐ์ฒด ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ ์œ„ํ•œ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

import {globby} from 'globby';
import fs from "fs";
import matter from "gray-matter";
import algoliasearch from 'algoliasearch';

const pages = await globby([
  'docs/',
]);

const objects = pages.map(page => {
  const fileContents = fs.readFileSync(page, 'utf8')
  const {data, content} = matter(fileContents)
  const path = page.replace('.md', '');
  let slug = path === 'docs/index' ? 'docs' : path;
  slug = "/" + slug + "/"
  return {
    slug,
    content,
    frontmatter: {
      ...data
    }
  }
})

//algolia update index code



  • /docs/ ,
  • ๊ฐ€ ์žˆ๋Š” globby ๋””๋ ‰ํ† ๋ฆฌ์—์„œ ๋ชจ๋“  ๋งˆํฌ๋‹ค์šด ํŒŒ์ผ์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
  • fs ,
  • ๋กœ ํŒŒ์ผ ๋‚ด์šฉ ์ฝ๊ธฐ
  • ๋งˆํฌ๋‹ค์šด์„ ์œ„ํ•ด gray-matter๋กœ ํŒŒ์ผ ๊ตฌ๋ฌธ ๋ถ„์„,
  • ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉ๋˜๋Š” ์Šฌ๋Ÿฌ๊ทธ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ช‡ ๊ฐ€์ง€ ๋งˆ์ˆ ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

  • ํ•„์ž์˜ ๊ฒฝ์šฐ ์Šฌ๋Ÿฌ๊ทธ๋Š” ํŒŒ์ผ ๊ฒฝ๋กœ์ผ ๋ฟ์ด๋ฉฐ ํŒŒ์ผ ๊ฒฝ๋กœ๋Š” /docs/{category}/{title}.md , ์˜ˆ: /docs/integrations/next-translate.md ์ด๋ฏ€๋กœ ์Šฌ๋Ÿฌ๊ทธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. /docs/integrations/next-translate/
    ๊ฒฐ๊ตญ ๋‚˜๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฐ์ฒด ๋ฐฐ์—ด์„ ์–ป์Šต๋‹ˆ๋‹ค.

    [
        {
          "slug": "/docs/integrations/next-translate/",
          "content": "My article content for indexing",
          "frontmatter": {
            "category": "Integrations",
            "date": "2022-01-20",
            "some-other-properties": "properties"
          }
        }
    ]
    
    


    Algolia ์ง€์ˆ˜ ์—…๋ฐ์ดํŠธ



    Algolia๋Š” ์ •๋ง ๊ฐ„๋‹จํ•˜๊ณ  ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฌ์šด JavaScript ํด๋ผ์ด์–ธํŠธ๋ฅผ ์ œ๊ณตํ•˜๋ฏ€๋กœ ์—ฌ๊ธฐ์— ๋งˆ๋ฒ•์ด ์—†์Šต๋‹ˆ๋‹ค. ์ธ๋ฑ์Šค๋ฅผ ํš๋“ํ•˜๊ณ  ์ด์ „ ๋‹จ๊ณ„์—์„œ ์ƒ์„ฑํ•œ ๊ฐœ์ฒด๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

    const client = algoliasearch(
      'APP_ID',
      'ADMIN_API_KEY'
    );
    const index = client.initIndex("simplelocalize-docs")
    index.saveObjects(objects, {
      autoGenerateObjectIDIfNotExist: true
    });
    
    


    package.json์— ์ธ๋ฑ์Šค ์—…๋ฐ์ดํŠธ ํฌํ•จ




    {
      "name": "simplelocalize-docs",
      "version": "1.0.0",
      "private": true,
      "scripts": {
        "dev": "next dev",
        "build": "next build && next export -o build/ && i18n:download && index:docs",
        "index:docs": "npx ts-node --skip-project index-docs.mjs",
        "i18n:upload": "simplelocalize upload",
        "i18n:download": "simplelocalize download"
      }
    }
    
    

    npm run index:docs ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ˆ˜๋™์œผ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.



    ๊ทธ๊ฒŒ ๋‹ค์•ผ? ๊ทธ๊ฒŒ ๋‹ค์•ผ! SimpleLocalizedocumentation page์—์„œ ์‹คํ–‰ ์ค‘์ธ ๊ฒ€์ƒ‰์„ ์ฐธ์กฐํ•˜์‹ญ์‹œ์˜ค.

    ์ข‹์€ ์›นํŽ˜์ด์ง€ ์ฆ๊ฒจ์ฐพ๊ธฐ