[NextJS] searchParams로 태그 검색

먼지·2023년 6월 18일
0

NextJS

목록 보기
2/2

요구사항

태그를 선택해 클래식을 검색할 수 있어야 함

  • 총 3개의 태그까지 검색할 수 있음
    - 태그가 없을 때 /tags
    - 태그가 한 개 /tags?tag1=태그1
    - 태그가 두 개 /tags?tag1=태그1&tag2=태그
    - 태그가 세 개 /tags?tag1=태그1&tag2=태그&tag3=태그3
  • 이미 선택된 태그를 클릭하면 삭제해야 함
    - 태그 1을 선택 /tags?tag2=태그2&tag3=태그3 or /tags?tag1=태그2&tag2=태그2
    - 태그 2를 선택 /tags?tag1=태그1&tag3=태그3 or /tags?tag1=태그1&tag2=태그3

구현

app/tags/page.tsx

export default async function TagsPage(props: Props) {
  const supabase = createServerClient();
  const selectedTags = [
    props?.searchParams?.tag1 ?? '',
    props?.searchParams?.tag2 ?? '',
    props?.searchParams?.tag3 ?? ''
  ];
  
  const { data } = await supabase.from('allClassics').select();

  const tags = [...new Set(data?.map(classic => classic.tags).flat())];
  
  return (
    <div className='p-4'>
      <TagList tags={tags} selectedTags={selectedTags} />
      <ClassicList classics={data ?? []} selectedTags={selectedTags} />
    </div>
  )
}

app/tags/TagList.tsx

export default function TagList({ tags, selectedTags }: Props) {
  const router = useRouter();
  const searchParams = useSearchParams();
  
  const handleTagClick = (tagName: string) => {
    // URLSearchParams 객체 생성
    const params = new URLSearchParams(searchParams.toString());
    
    // 다음에 추가할 태그 인덱스 변수 1로 초기화
    let nextTagIndex = 1;
    // tag1, tag2, tag3이 존재하고 index가 3 이하인 동안 index 값을 증가시켜 다음 추가할 태그의 인덱스 결정
    while (params.has(`tag${nextTagIndex}`) && nextTagIndex <= 3) {
      nextTagIndex++;
    }

    // params를 배열로 변환 후 태그 이름과 일치하는 키를 찾기 (이미 선택된 태그)
    const existingTagIndex = Array.from(params.keys())
      .find((key) => key.startsWith('tag') && params.get(key) === tagName);
    
    // 이미 선택한 태그인 경우 해당 키를 params에서 삭제
    // 추가할 수 있는 경우 index가 3 이하인 경우 새로운 태그를 추가
    if (existingTagIndex) {
      params.delete(existingTagIndex);
    } else if (nextTagIndex <= 3) {
      params.set(`tag${nextTagIndex}`, tagName);
    }
    // params를 문자열로 변환
    const newSearchParams = params.toString();
    
    // 새로운 URL로 이동
    router.push(`tags?${newSearchParams}`);
  };

  return (
    <div className="flex flex-wrap items-center">
      {tags.map(tag => (
        <button
          key={tag}
          onClick={() => handleTagClick(tag)}
          className="rounded-sm p-1 bg-white mr-1 mb-1"
        >
          {tag}
        </button>
      ))}
    </div>
  );
}

문제점

이미 선택된 태그 삭제 후 다시 새 태그를 누르면 이런식으로
/tags?tag1=%EC%8A%AC%ED%94%94&tag3=%EC%9A%B8%EC%A0%81%ED%95%A8 = /tags?tag1=슬픔&tag3=울적함
순서가 뒤죽박죽?이 됨

완성 코드

app/tags/page.tsx

type Props = {
  params?: { num?: string; };
  searchParams?: { tag1?: string, tag2?: string, tag3?: string };
};

export default async function TagsPage(props: Props) {
  const supabase = createServerClient();
  const selectedTags = [
    decodeURIComponent(props?.searchParams?.tag1 ?? ''),
    decodeURIComponent(props?.searchParams?.tag2 ?? ''),
    decodeURIComponent(props?.searchParams?.tag3 ?? '')
  ];
  
  const { data } = await supabase.from('allClassics').select();
  
  return (
    <div className='p-4'>
      <TagsContainer classics={data ?? []} selectedTags={selectedTags} />
    </div>
  );
}

app/tags/TagsContainer.tsx
디자인이 조금 허전해서 classics 페이지에도 사용했던 ClassicSearchForm 컴포넌트를 재사용 했다. 일단 검색창에 검색어가 있으면 검색어에 해당하는 태그 배열을 필터해서 보여주는데, 없다면 url을 확인해서 searchParams에 해당하는 태그로 필터한다.

결국 검색을 하거나 태그를 클릭하거나 둘 중 하나밖에 못하기 때문에 허접하지만.. 나중에 어떻게 할지 좀 더 생각해 보고 싶다.

function TagsContainer({ classics, selectedTags }: TagContainerProps) {
  const [tagInput, setTagInput] = useState(''); 
  const tags = [...new Set(classics?.map(classic => classic.tags).flat())];
  const filteredClassics = tagInput
    ? classics.filter(classic => classic.tags.includes(tagInput))
    : selectedTags[0] === ''
        ? classics
        : classics.filter(classic => selectedTags.some(tag => classic.tags.includes(tag)));
    
  return (
    <>
      <ClassicSearchForm
        value={tagInput}
        onChange={(e) => setTagInput(e.target.value)}
        onClick={() => setTagInput('')}
        placeholder='클래식 태그 검색하기'
      />
      <TagList tags={tags} selectedTags={selectedTags} />
      <ClassicList classics={filteredClassics} selectedTags={selectedTags} />
    </>
  );
}

app/tags/TagList.tsx

function TagList({ tags, selectedTags }: TagListProps) {
  const router = useRouter();
  const searchParams = useSearchParams();
  
  const handleTagClick = (tagName: string) => {
    const encodedTagName = encodeURIComponent(tagName);
    const params = new URLSearchParams(searchParams.toString());
    let nextTagIndex = 1;
    while (params.has(`tag${nextTagIndex}`) && nextTagIndex <= 3) {
      nextTagIndex++;
    }

    const existingTagIndex = Array.from(params.keys())
      .find((key) => key.startsWith('tag') && params.get(key) === encodedTagName);

    if (existingTagIndex) {
      params.delete(existingTagIndex);
    } else if (nextTagIndex <= 3) {
      params.set(`tag${nextTagIndex}`, encodedTagName);
    }

    const newSearchParams = params.toString();
    
    router.push(`tags?${newSearchParams}`);
  };

  return (
    <div className="flex flex-wrap items-center">
      {tags.map(tag => (
        <button
          key={tag}
          onClick={() => handleTagClick(tag)}
          className={`rounded-sm p-1 mr-1 mb-1 ${selectedTags.includes(tag) ? 'bg-violet-400 text-white' : 'bg-white'}`}
        >
          {tag}
        </button>
      ))}
    </div>
  );
}

알게 된 점

JavaScript 논리 연산자

자바스크립트로 기본값을 설정할 때, 예전엔 || 연산자를 사용했는데 요즘 ??(Nullish 병합 연산자)를 자주 사용한다. 둘 다 비슷한데 차이점을 몰라서 찾아봤다!

  • ?? : 왼쪽 피연산자가 null 또는 undefined인 경우 오른쪽 피연산자를 반환
  • || : 왼쪽 피연산자 값이 falsy 한 값인 경우 오른쪽 피연산자를 반환

이번 작업에선 URLSearchParams의 get() 함수를 사용하는데, 이 함수는 파라미터로 전달한 값이 존재하지 않을 경우 null을 반환한다. 이 때 값이 존재하지 않을 경우 대체 값을 제공할 때 ?? 연산자를 사용하는 것이 적합하다 생각했다.

인코딩 에러

Failed to fetch RSC payload. Falling back to browser navigation. TypeError: Failed to execute 'fetch' on 'Window': Failed to read the 'headers' property from 'RequestInit': String contains non ISO-8859-1 code point.

한글로 검색하면 문자 인코딩 에러가 뜬다. 그래서 태그를 클릭해 searchParams를 설정해 검색할 때 URL tag를 encodeURIComponent() 함수로 인코딩하고, 스타일을 위해 decodeURIComponent() 함수를 사용해 원래의 문자열로 복원했다!

profile
꾸준히 자유롭게 즐겁게

0개의 댓글