๋ฐ˜์‘ํ˜•

Search Times ๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•œ ๊ฒ€์ƒ‰์–ด ํ•˜์ด๋ผ์ดํŠธ

 

๊ฒ€์ƒ‰์ฐฝ์— ์ž…๋ ฅํ•œ ํ‚ค์›Œ๋“œ์™€ ์ผ์น˜ํ•˜๋Š” ๋‹จ์–ด๋ฅผ ํ•˜์ด๋ผ์ดํŠธ(๊ฐ•์กฐ) ํ•˜๊ธฐ ์œ„ํ•ด split, join ๋“ฑ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์ฝ”๋“œ๊ฐ€ ๋‹ค์†Œ ๋ณต์žกํ•ด์ง„๋‹ค. ์ •๊ทœ์‹์„ ์‚ฌ์šฉํ•˜๋ฉด ๊น”๋”ํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

ํ•˜์ด๋ผ์ดํ„ฐ ํ•จ์ˆ˜ ์ •์˜ โ–ผ

const getHighlightedText = (text, query) => {
  const re = new RegExp(`(${query})`, 'gi');

  if (query !== '' && text.match(re)) {
    const parts = text.split(re);

    return (
      <>
        {parts.map((part) =>
          part.toLowerCase() === query.toLowerCase() ? (
            <mark key={Math.random().toString(36).slice(2)}>{part}</mark>
          ) : (
            part
          ),
        )}
      </>
    );
  }

  return text;
};

 

ํ•˜์ด๋ผ์ดํ„ฐ ํ•จ์ˆ˜ ์‚ฌ์šฉ โ–ผ

return (
  // ...์ƒ๋žต
  <h2>{getHighlightedText(headline, term)}</h2>
);

 

ํ•˜์ด๋ผ์ดํ„ฐ ํ•จ์ˆ˜ ํ†บ์•„๋ณด๊ธฐ


text์™€ query ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์•„๋ž˜์ฒ˜๋Ÿผ ๋ฐ›์•˜๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ณธ๋‹ค. text ๋ฌธ์žฅ์— alibaba๋ผ๋Š” ๋ฌธ์ž์—ด(๋‹จ์–ด)์ด ๋“ค์–ด๊ฐ€ ์žˆ๋‹ค๋ฉด ํ•ด๋‹น ๋ถ€๋ถ„์€ ๋ชจ๋‘ ํ•˜์ด๋ผ์ดํŠธ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•œ๋‹ค(์ฃผํ™ฉ์ƒ‰์œผ๋กœ ํ‘œ์‹œํ•œ ๋ถ€๋ถ„)

// text ํŒŒ๋ผ๋ฏธํ„ฐ
'A Chastened Alibaba Tones Down Its Singles alibabaDay Retail Bonanza';

// query ํŒŒ๋ผ๋ฏธํ„ฐ
'alibaba';

 

RegExp ๊ฐ์ฒด ์ƒ์„ฑ

์ •๊ทœ์‹ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด () ๋ฒกํ‹ฑ๊ณผ ์†Œ๊ด„ํ˜ธ๋กœ ํ•œ๋ฒˆ ๊ฐ์‹ผ ํ›„ (${๋ณ€์ˆ˜๋ช…}) ํ˜•ํƒœ๋กœ ์ž…๋ ฅํ•ด์ค€๋‹ค.

const re = new RegExp(`(${query})`, 'gi'); // /(alibaba)/gi

 

  • g : ์ „์—ญ ๊ฒ€์ƒ‰ ํ”Œ๋ž˜๊ทธ. ๋ชจ๋“  ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜. g ํ”Œ๋ž˜๊ทธ๊ฐ€ ์—†์œผ๋ฉด ์ตœ์ดˆ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋งŒ ๋ฐ˜ํ™˜.
  • i : ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ ์—†์Œ ํ”Œ๋ž˜๊ทธ(insensitive). i ํ”Œ๋ž˜๊ทธ๊ฐ€ ์žˆ์œผ๋ฉด ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๊ตฌ๋ถ„ํ•˜์ง€ ์•Š์Œ

 

๊ฒ€์‚ฌํ•  ํ…์ŠคํŠธ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ

โžŠquery ํ˜น์€ text๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ด๊ฑฐ๋‚˜, โž‹text์— ๋งค์นญ๋˜๋Š” query๊ฐ€ ์—†๋‹ค๋ฉด ํ•˜์ด๋ผ์ดํŒ…์„ ํ•  ํ•„์š”๊ฐ€ ์—†์œผ๋ฏ€๋กœ ์ด์— ๋Œ€ํ•œ ์กฐ๊ฑด์„ ๊ฑธ์–ด์ค€๋‹ค.

if (query !== '' && text.match(re)) {
  // ...ํ•˜์ด๋ผ์ดํŒ… ์ถ”๊ฐ€ ๋กœ์ง
}

 

str.match(regexp) ๋ฉ”์„œ๋“œ์—์„œ str์ด ๋นˆ ๋ฌธ์ž์—ด์ด๊ฑฐ๋‚˜, ๋งค์นญ๋˜๋Š”๊ฒŒ ์—†์œผ๋ฉด null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. gi ํ”Œ๋ž˜๊ทธ๋ฅผ ๋ถ™์˜€์œผ๋ฏ€๋กœ ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๊ตฌ๋ถ„ํ•˜์ง€ ์•Š๊ณ , ์ฐพ๋Š” ๋‹จ์–ด๊ฐ€ ์—ฌ๋Ÿฌ๋ฒˆ ๋“ฑ์žฅํ•˜๋ฉด ๋ชจ๋“  ๊ฒฐ๊ณผ๋ฅผ ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

''.match(re); // null
'hello'.match(re); // null

'tencentaLiBaBa'.match(re); // ['aLiBaBa']
'tencentalibabadouyinALIbaba'.match(re); // ['alibaba', 'ALIbaba']

 

ํ•˜์ด๋ผ์ดํŠธ ๋‹จ์–ด๋ฅผ ๊ธฐ์ค€์œผ๋กœ split

ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์€ query๋Š” alibaba ๋ผ๋Š” ๋ฌธ์ž์—ด์ด๋ฏ€๋กœ ์ด๋ฅผ ๊ธฐ์ค€(๊ตฌ๋ถ„์ž)์œผ๋กœ ๋ฐฐ์—ด์— ๋‹ด๊ธด๋‹ค.

const parts = text.split(re);
// ['A Chastened ', 'Alibaba', ' Tones Down Its Singles ', 'alibaba', 'Day Retail Bonanza']

 

โญ๏ธ ๋ˆˆ์—ฌ๊ฒจ ๋ณผ ์ ์€ split์˜ ๊ตฌ๋ถ„์ž(seperator)๋„ ๋ฐฐ์—ด์— ํฌํ•จ๋๋‹ค๋Š” ์ ์ด๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ split์€ ๊ตฌ๋ถ„์ž๋ฅผ ๋ฐฐ์—ด์— ํฌํ•จํ•˜์ง€ ์•Š๋Š”๋‹ค. ํ•˜์ง€๋งŒ ๊ตฌ๋ถ„์ž๊ฐ€ ํฌํš๊ด„ํ˜ธ()๋ฅผ ํฌํ•จํ•˜๋Š” ์ •๊ทœ์‹์ด๋ฉด ํฌํš๋œ ๊ฒฐ๊ณผ๋„ ๋ฐฐ์—ด์— ํฌํ•จ๋œ๋‹ค. ํฌํš ๊ด„ํ˜ธ๋Š” ์ •๊ทœ์‹์—์„œ ํ‘œํ˜„์‹์„ ํ•˜๋‚˜์˜ ๋‹จ์œ„๋กœ ์ทจ๊ธ‰ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.

'12345six789'.split('six'); // ['12345', '789']
'12345six789'.split(/six/); // ['12345', '789']
'12345six789'.split(/(six)/); // ['12345', 'six', '789']

 

ํ•˜์ด๋ผ์ดํŠธํ•  ๋‹จ์–ด์— mark ํƒœ๊ทธ ์ถ”๊ฐ€

๊ฐ ๋‹จ์–ด๋ฅผ ์†Œ๋ฌธ์ž๋กœ ๋ณ€ํ™˜ํ•ด์„œ query ๋ฌธ์ž์—ด๊ณผ ๊ฐ™๋‹ค๋ฉด ํ•ด๋‹น ๋‹จ์–ด์— <mark> ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

return (
  <>
    {parts.map((part) =>
      part.toLowerCase() === query.toLowerCase() ? (
        <mark key={Math.random().toString(36).slice(2)}>{part}</mark>
      ) : (
        part
      ),
    )}
  </>
);

 

<mark> ํƒœ๊ทธ๋Š” ํ˜„์žฌ ๋งฅ๋ฝ๊ณผ ๊ด€๋ จ์žˆ๋Š” ํ…์ŠคํŠธ๋ฅผ ๊ฐ•์กฐํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋ฉฐ, ๋…ธ๋ž€์ƒ‰ ํ•˜์ด๋ผ์ดํŠธ ๋ฐฐ๊ฒฝ์œผ๋กœ ๋ Œ๋”๋œ๋‹ค.

`<mark>`์™€ `<strong>` ์š”์†Œ์˜ ์ฐจ์ด์ ์„ ๊ธฐ์–ตํ•˜์„ธ์š”. ํ…์ŠคํŠธ์—์„œ, `<mark>`๋Š” ์—ฐ๊ด€์„ฑ์„ ๊ฐ€์ง„ ๋ถ€๋ถ„์„, `<strong>`์€ ์ค‘์š”๋„๋ฅผ ๊ฐ€์ง„ ๋ถ€๋ถ„์„ ๋‚˜ํƒ€๋‚ผ ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. — MDN

 

<mark> ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•œ ๊ณณ์—” ์œ„์ฒ˜๋Ÿผ ๋…ธ๋ž€์ƒ‰ ๋ฐฐ๊ฒฝ์œผ๋กœ ๋‚˜์˜จ๋‹ค

 

ํ•˜์ด๋ผ์ดํŠธํ•  ๋‹จ์–ด๊ฐ€ ๋งŽ๋‹ค๋ฉด key๋ฅผ ์ง€์ •ํ•˜๊ธฐ ์• ๋งคํ•˜๋ฏ€๋กœ ๋‚œ์ˆ˜ํ™”ํ•œ ๋ฌธ์ž์—ด์„ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๋”๋ณด๊ธฐ
// lib/utils.ts
export const getRandomKey = (id?: string | number) => {
  const randomNum = Math.random().toString(36).slice(2);
  return id ? `${id}-${randomNum}` : randomNum;
};
Math.random().toString(36).slice(2);
// Math.random() : 0.1656391799983088
// toString(36) : '0.5yo27t64476'
// slice(2) : 'yo27t64476'

 

  • Math.random() : 0์ด์ƒ 1๋ฏธ๋งŒ์˜ ๋ถ€๋™ ์†Œ์ˆ˜์  ๋‚œ์ˆ˜
  • toString(36) : 36์ง„์ˆ˜๋กœ ๋ณ€ํ™˜ — toString์€ 2~36 ์‚ฌ์ด์˜ ์ง„์ˆ˜(Radix)๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๋Š”๋‹ค.

 

๋ฒˆ์™ธ — ๋ฌธ์ž์—ด์— ๋งํฌ ๊ฑธ๊ธฐ(Linkify)


๊ฒ€์ƒ‰์–ด ํ•˜์ด๋ผ์ดํŠธ์™€ ๋น„์Šทํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ๋ฌธ์ž์—ด์— URL์ด ์žˆ๋‹ค๋ฉด <a> ํƒœ๊ทธ๋กœ ๊ฐ์‹ธ๋Š” Linkify ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค. http https ๋“ฑ URL๊ณผ ๊ด€๋ จ๋œ ํ”„๋กœํ† ์ฝœ์„ ์‹๋ณ„ํ•  ์ˆ˜ ์žˆ๋Š” ์ •๊ทœ์‹์„ ์ž‘์„ฑํ•œ ํ›„ split ๋ฉ”์„œ๋“œ์˜ ๊ตฌ๋ถ„์ž๋กœ ์‚ฌ์šฉํ•ด์„œ ๋ถ„๋ฆฌํ•˜๋ฉด ๋œ๋‹ค. ๋ฐฐ์—ด์˜ ๊ฐ ์š”์†Œ๋กœ ๋ถ„๋ฆฌ๋œ ๋ฌธ์ž์—ด์—์„œ ์ •๊ทœ์‹์— ๋งค์นญ๋œ ๋ฌธ์ž์—ด(๋งํฌ)์€ ํ•ญ์ƒ ํ™€์ˆ˜ ๋ฒˆ์งธ ์ธ๋ฑ์Šค์— ์œ„์น˜ํ•˜๊ฒŒ ๋œ๋‹ค(์ •๊ทœ์‹์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์Œ).

const regex = /(https?:\/\/\S+)/g; // http ๋ฐ https ๊ตฌ๋ถ„

const paragraph = '์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ https://ko.javascript.info/intro ๋Š” ๋ธŒ๋ผ์šฐ์ €๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์„œ๋ฒ„์—์„œ๋„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์™ธ์—๋„ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์—”์ง„ https://en.wikipedia.org/wiki/JavaScript_engine ์ด๋ผ ๋ถˆ๋ฆฌ๋Š” ํŠน๋ณ„ํ•œ ํ”„๋กœ๊ทธ๋žจ์ด ๋“ค์–ด ์žˆ๋Š” ๋ชจ๋“  ๋””๋ฐ”์ด์Šค์—์„œ๋„ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.';

console.log(paragraph.split(regex));
/*
[
  '์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ',
  'https://ko.javascript.info/intro',
  ' ๋Š” ๋ธŒ๋ผ์šฐ์ €๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์„œ๋ฒ„์—์„œ๋„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์™ธ์—๋„ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์—”์ง„ ',
  'https://en.wikipedia.org/wiki/JavaScript_engine',
  ' ์ด๋ผ ๋ถˆ๋ฆฌ๋Š” ํŠน๋ณ„ํ•œ ํ”„๋กœ๊ทธ๋žจ์ด ๋“ค์–ด ์žˆ๋Š” ๋ชจ๋“  ๋””๋ฐ”์ด์Šค์—์„œ๋„ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.'
*/

 

์œ„ ๋ฐฉ๋ฒ•์„ ์ด์šฉํ•ด ํ™€์ˆ˜๋ฒˆ์งธ ์ธ๋ฑ์Šค์˜ ๋ฌธ์ž์—ด์€ <a> ํƒœ๊ทธ๋กœ ๊ฐ์‹ธ๋„๋ก ์ž‘์„ฑํ•˜๋ฉด ๋œ๋‹ค. ์ด๋ฉ”์ผ, ํ•ด์‹œํƒœ๊ทธ ๋“ฑ ๋” ์ „๋ฌธ์ ์ธ Linkify๋ฅผ ์›ํ•œ๋‹ค๋ฉด linkifyjs ๊ฐ™์€ ํŠนํ™”๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ๋„ ์ง€์›ํ•œ๋‹ค.

// LinkifyText.tsx
import React from 'react';
import { getRandomKey } from 'lib/utils';

export default function LinkifyText({ text }: { text: string }) {
  const regex = /(https?:\/\/\S+)/g;

  return (
    <>
      {text.split(regex).map((part, index) =>
        index % 2 === 0 ? (
          part
        ) : (
          <a
            key={getRandomKey(index)} // [๋žœ๋ค key ์ƒ์„ฑ ์œ ํ‹ธ ํ•จ์ˆ˜](https://www.notion.so/TIL-Linkify-e4bb8f6a7527400992ca396e0614d32d?pvs=21)
            href={part}
            target="_blank"
            rel="noopener noreferrer"
            className="text-blue-500 hover:text-blue-400"
          >
            {part}
          </a>
        ),
      )}
    </>
  );
}

 

๋ ˆํผ๋Ÿฐ์Šค


 


๊ธ€ ์ˆ˜์ •์‚ฌํ•ญ์€ ๋…ธ์…˜ ํŽ˜์ด์ง€์— ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”
๋ฐ˜์‘ํ˜•