기억의 실마리
2024. 10. 20. 18:46

🤔NextJS 쿠키?

브라우저에 저장되는 일반적인 쿠키와 다를게 없지만

NextJS는 SSR을 지원한다는 점에서

cookie를 서버에서 확인할 수 있는 방법이

제한적이기 때문에 CSR(React 등)만 활용하여

개발하던 개발자들은 생각보다 더 난해한 문제를

겪게 될 수 있다.

 

그런 개발자 중 한명이 필자였고 특히

쿠키를 활용한 JWT 로그인 유지를 구현할 때

interceptor를 통해 매번 토큰을 header에 넣어 요청하고

accessToken이 만료된 경우에는 refreshToken을

서버에 재발급요청하여 재생성된 토큰들을 쿠키에 저장하고

저장된 쿠키를 통해서 이전 요청을 재요청하는 프로세스에서

accessToken 쿠키가 undefined인 상태로 요청이 되는 이슈

꽤나 험난한 삽질과 많은 시간을 고민하게 되었다.

 

결과적으로 NextJS를 제대로 다루기 위해서는

hydration,

middleware,

server - component,

server - actions / mutations,

위 4가지의 작동방식은 필수적으로 학습을 통해

Next가 작동되는 방식에 대해 이해하고 있어야한다는

생각이 들었다.

 

🫗hydration

(hydration에 대한 글은 개인적 주관이 강한 글이기 때문에 넘어가셔도 좋습니다😊)

필자는 단순히 하이드레이션을 주입식 교육처럼

하이드레이션 = 서버의 결과를 클라이언트로 합치는 것

이라고만 생각했지만 결과적으로 이해하게 된 계기는

여러 이슈를 겪으며 어원에 대해 다시 생각하는 것으로

이해도를 갖추게 된 것 같다.

 

hydration이란 수분공급 이라는 어원을 가지고 있다.

그렇다면 어째서 수분공급이지? 라는 의구심을 가지고

좀 더 단순하게 어원에 대해 생각했을 때 식물이 뿌리에서

흡수한 물을 줄기를 통해서 수분공급을 하는 방식을 떠올려 봤다.

 

뿌리 = Server

(땅속에 있어 우리가 표면적으론 볼 수 없지만 핵심이 되는 뿌리)

 

줄기, 잎, 꽃 등 = Client

(땅을 파지 않아도 겉으로 볼 수 있는 식물의 외형)

 

식물은 수분공급이 제대로 이루어지지 않는다면

그 식물은 시들어버리거나 제대로 성장하지 못한다.

 

그렇기 때문에 Server에서 보여주고자 하는 것을

Client에 공급해주어 제대로된 결과물을 보여주는 것이

Hydration이구나! 라는 결론을 내리게 되었다.

 

물론 그저 나의 상상력으로 여기까지 온 것일 수도 있다...

하지만 좀 더 어원에 대해 이해하려할 수록

이해도가 높아지는 경우가 생각보다 많았던 것 같다.

 

만약 영어가 아닌 한글이었다면 조금 더 직관적으로

프로세스를 이해했을 수도 있지만 프로그래밍 언어가

기본적으로 영어로 되어있기 때문에 우리가 프로그래밍에

대해서 이해하기 어렵게하는 하나의 벽이 되고있지 않나...

하는 생각이 들곤 한다.

 

🔗middleware

middleware는 server에서 client에 렌더링되기 이전에

거치게 되는 곳으로 서버와 클라이언트의 교집합 지점이라고

볼 수도 있는 영역이다. 때문에 NextJS를 다룬다면 정말 중요한

기능이기 때문에 꼭 알아야된다고 생각하는 것 중 하나다.

 

NextJS를 활용해서 사용자에게 보여주기전에 server에서

내부적으로 redirect하거나 필수적인 쿠키를 미리 설정

가능하고 국제화를 하는 경우에는 페이지에 접속한

국가에 적합한 언어로 구성된 페이지를 제공하기 위해서

활용하기도 한다.

 

middleware 활용처는 너무 많기 때문에 예제코드는

필자의 깃허브 링크로 남겨두겠습니다.

 

* nextjs14 app-router 국제화를 적용한 템플릿을 예제로 넣었습니다.

Github: https://github.com/zeriong/next14-app-router-i18n-template/blob/master/middleware.ts

 

⚙️server-component

NextJS에서 컴포넌트를 구성할 때 최상단에 "use client"를

작성하지 않는다면 기본적으로 server-component로 작성된다.

그리고 "use client"를 작성한다면 react에서 사용되는 CSR관련

Hooks를 활용할 수 있게 된다.

 

단 여기서 중요한 것은 "use client"라고 해서 SEO가 안되는 것은

아니라는 점이다.

 

html 구성은 server에서 렌더링이 되기 때문에 SEO는

영향을 끼치지 않는다. 단지 CSR에서 활용 가능한

React의 Hooks를 활용해서 CSR에서 작동되는 JS를 활용하겠다.

라고 선언하고 활용하는 것에 불과하다.

 

그러니 "use clinet"를 썼다고 해서 SEO가 안될 것이라는

걱정은 하지 않아도 될 것이다.

(useEffect를 활용하여 state 변경후에 생성되는 컴포넌트 제외)

 

 

🪄server - actions / mutations

서버에서 실행되는 비동기함수를 뜻한다.

form에서 활용하는 action이나 기타 fetch를 활용한

api요청 등을 server에서 작동하게 만드는 것이다.

 

활용방식은 아주 간단하게 해당 함수의 환경 최상단에

"use server" 를 선언하게 되면 이는 server에서 작동하는

함수로 정의된다.

 

"use server"를 정의하는 환경은 주로 파일단위나

함수단위로도 활용 가능하다.

 

상황에 따라서 우리는 다양하게 활용하지만 주로

하나의 파일에서 "use server"를 최상단에

정의하여 모듈화하여 활용하는 경우가 보편적이다.

 

예시적으로 actions.ts 파일을 생성한다고

가정했을 때의 예제 코드이다.

 

  • server-actions 예제코드
// actions.ts

"use server";

/**@desc login server-action */
export async function loginAction(prevState: { success?: boolean; msg?: string }, formData: FormData) {
  const userId = String(formData.get("userId")).trim() as string;
  const userPassword = String(formData.get("userPassword")).trim() as string;

  if (!userId.length || !userPassword.length) return { success: false, msg: "아이디 또는 비밀번호를 확인해주세요" };

  const reqBody = { userId, userPassword };

  const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL_AUTH}/auth/login`, {
    method: "POST",
    body: JSON.stringify(reqBody),
  });

  const resBody = await res.json();

  if (resBody.code != 200) return { success: false, msg: resBody?.message };

  return { success: true, msg: "success" };
}

 

위처럼 server-action을 구성하고 해당 server-action을 로그인을 하는 컴포넌트에서 useFormState 훅을 활용해서 state로 response 결과값을 client에서 동적으로 확인할 수 있다.

 

  • server-mutations 예제코드
// mutations.ts

"use server";

import { getUserInfo } from "@/apis/user";

/** get user data */
export async function getUserData() {
  const res = await getUserInfo();
  return res.body.data;
}

 

정말 단순하게 "use server"를 명시해주어 server에서 해당 api로 fetch를 통해 response 값을 받아볼 수 있다.

 

server - actions / mutations 장점

  1. network 요청 수를 줄일 수 있다
  2. server 내부에서 미리 값을 fetch 받아 렌더링하기 때문에 SEO 향상
  3. API route 없이 서버 측에서 직접 DB에 접근가능
  4. 민감한 데이터를 클라이언트 측에 노출시키지 않을 수 있음
  5. client side 번들 사이즈가 줄어 초기 로딩 속도 개선

 

🍪NextJS 쿠키 트러블 슈팅

내가 맞닥뜨린 이슈는 결국 NextJS에서 쿠키에 대한

hydration이 필요한 상황이었고 가장 첫번 째로 고민한 것은

가능한 것에 대한 고찰이었다.

 

서버에서 쿠키에 접근가능한 방식을 생각해봤을 때

next header에 접근할 수 있어야하는 것이

우선 선행이 되어야 했다.

 

그렇다면 어떠한 경우에 server에서 쿠키에 접근할 수 있는가?

내가 떠올릴 수 있는건 두가지였다.

 

1. middleware의 NextRequest를 통한 접근

2. server-component에서 next-header의 cookies()로 접근

 

1번의 경우는 현실성이 부족했고 2번으로 접근하기로 했다.

 

하지만 interceptor를 구성할 때 따로 api를 요청할 때 마다

지속적으로 재사용해야 하기 때문에 파일로 분기하여

구성할 수 밖에 없었다.

 

단순히 fetch에 interceptor를 구성하여 붙이는 코드로 구성되어있어

최상단에 "use server"를 명시하는 것도 말이 안되고,

그렇다고 interceptor를 server-component에서 구성하는 것도

말이 안된다고 생각이 들었다.

 

그때 마침 생각난 것이 바로 server-component가

next-header에 접근할 수 있는 이유에 대해서 생각해봤다.

 

nextjs14에서는 컴포넌트 구성 시 최상단에

"use client"를 명시하지 않으면 서버컴포넌트로 정의된다.

 

이는 즉 이전 버전에서 사용하던 "use server"가 생략됐을 뿐이고

server - actions / mutations에서 최상단에 "use server"를

명시하고 활용하는 것과 동일하다고 생각이 들었다.

 

그렇다면 방법은 하나 뿐이다.

utils에 server 폴더를 만들고 최상단에 "use server"를 명시한

cookies.ts파일을 만들어 내부에 유틸을 구성하는 방법이다.

 

getServerSideCookie,

setServerSideCookie,

removeServerSideCookie,

 

위 세가지 쿠키관련 유틸을 구성하고

next-header에 접근할 수 있는 환경에서

쿠키를 컨트롤할 수 있도록 만들어주었다.

 

결과는 성공적이었고,

모든 server - actions / mutations와

클라이언트에서의 fetch 요청으로도 성공적인

로그인 유지가 가능한 결과를 얻을 수 있었다.

 

✍️마치며...

개발에 대한 경험이 늘어남에 따라 느끼는 것은 개발은 언제나 정답이 정해져있는 것이고 그 정답에 도달하기 위해선 내가 맞닥뜨린 문제에서 성공적인 예시를 통해 하나씩 이어가는 것이 가장 빠르고 올바르게 이해하며 정확하게 문제를 해결할 수 있는 방법인 것 같다.

2024. 10. 13. 16:27

🤔서론과 계기

회사에서 진행하는 프로젝트에서 useHookForm을

활용 가능하도록 input 공용 컴포넌트를 만들다가

tailwindcss를 통해 스타일을 지정하다"focus:", "hover:" 문득

특정 문자열을 반드시 포함하는 타입을 만들면 어떨까?

라는 생각과 함께 고민해보고 만들어보게 되었다.

 

어떻게 만들면 그게 가능할까 라고 고민하는 순간

가장 먼저 템플릿 리터럴이 떠올랐고 단순하게 작성해봤는데

다행이 해매지않고 원하는 결과를 얻었다.

 

typescript로 고생하는 누군가에게 이 포스팅이 도움이 됐으면 좋겠다.

 

  • 예제코드
// interface 예제
interface IHolder {
  inputPlaceholder?: `placeholder-${string}`;
}

// type 예제
type TFocus = `focus:${string}`;

 

✍️마치며...

개발을 할 때 무언가 필요할 때 검색을 하기전 스스로 떠올려 만들어보는 것이 무엇보다 중요한 것 같다. 정보가 넘쳐나는 시대이기에 더욱 스스로 사고하는 기회가 적어지게 되는 것 같고 이는 개발자에게 치명적이지 않을까? 라는 생각을 하게된다... 앞으로도 초심을 잃지않고 사고력을 꾸준히 키워나아가자!

'Frontend > TypeScript' 카테고리의 다른 글

[ Typescript ] 타입스크립트  (0) 2023.02.15
2024. 9. 22. 21:42

🐻Zustand

Zustand가 가진 특징으로 가벼운 번들사이즈,

폭넓은 확장 가능성, provider가 없는 구조,

Flux패턴의 단방향 Pub/Sub 모델구조이다.

 

이러한 특징들로 요즘 대세 상태관리 라이브러리로

각광받고있다.

 

npm 공식 페이지에서 주간 다운로드 횟수를 보면

놀라운 주간 다운로드 횟수를 갱신중이다.

 

※ 2024-09-22 주요 상태관리 라이브러리 주간 다운로드 횟수

- Redux : 10,278,099

- Zustand : 4,243,907

- Redux-toolkit : 3,609,254

- Jotai : 1,177,983

- Recoil : 489,922

 

부동의 1위인 오리진 Redux를 제외한다면

Redux의 본체라 해도 무방한 Redux-toolkit 조차도 뛰어넘은

대세 상태관리 라이브러리이다.

 

이번 포스팅은 Zustand를 React에서 사용한다는 것을

기반으로 Zustand의 특징과 간단한 예제코드를

포스팅하고자 한다.

 

🪶가벼운 번들사이즈

Zustand는 획기적으로 가벼운 번들사이즈를 자랑한다.

고작 1.16kb의 사이즈 만으로 전체 상태를 용이하게

관리할 수 있어 프로젝트 자체의 용량을 더욱 가볍게

가져갈 수 있다.

 

🪝확장 가능성

redux와 같은 Flux패턴이기 때문에 redux의

reducer와 같은 개념으로 확장하여 유지보수성을

높일 수 있고 Zustand에서 제공하는 미들웨어를 통해

Redux dev-tools를 활용할 수 있어 디버깅에도

용이하게 확장이 가능하다.

 

🧺Provider가 없는 구조

이는 단순하게 생각하면 provider가 없어서 어쩌라고?

라는 생각이 들 수 있다. 하지만 React의 구조가 이를

엄청난 최적화요소로 만들어준다.

 

바로 Re-Render에 대한 최적화이다.

Provider로 감싸지 않는 구조로 상태 변화에 따른

불필요한 렌더링을 최소화 할 수 있게 되고

멀리보면 UX까지 영향을 끼칠 수 있는

핵심적인 요소라고 볼 수 있다.

 

📑Flux패턴의 구조

Flux패턴은 간단히 이야기 하자면 React와 Redux처럼

논리가 단방향으로 흐르는 아키텍처이며 대표적인 특징으로

디버깅이 쉽다는 특징을 가지고 있다.

 

  • Flux 패턴 시각화

사용자 입력을 기반으로 Action을 만들고 Dispatcher에

전달한 후 Store의 데이터를 변경하고 View에 반영하여

단방향의 흐름으로 작동하며, View에서 State 변경이 감지되면

순서대로 재실행한다. 이러한 흐름을 통해 누락없이

변경된 State를 반영할 수 있다.

 

🔍Pub/Sub (발행/구독) 모델

Pub/Sub 모델은 Publish/Subscribe의 축약어로 메시지 기반의

미들웨어 시스템을 말하며, 메시지를 전송할 때 publisher(발행자)가

subscriber(구독자)에게 직접 메시지를 전송하지만 Pub/Sub 모델은

발행자(Pub)는 어떤 구독자(Sub)가 있는지 모르는 상태에서

메시지를 전송하고 구독자(Sub)발행자(Pub)에 대한 정보 없이

자신의 Interest(구독한 것)에 맞는 메시지만을 전송 받는 것을 의미한다.

 

즉, Zustand에서는 스토어의 상태 변경이 일어날 때 실행할

Listener 함수를 모아 두었다가(sub), 상태가 변경되었을 때

등록된 Listener 에게 상태가 변경되었다고 알려준다(pub).

 

🧑‍💻예제코드

간단한 예제코드로 Zustand의 미들웨어를 사용하여 Redux dev-tools를 활용가능한 코드이다.

import type {StoreApi, UseBoundStore} from "zustand";
import {create} from "zustand";
import {devtools} from "zustand/middleware";

export interface ITestObjState {
  id: number;
  title: string;
  content: string;
}

export interface ITestStore {
  testState: string;
  testObjState: ITestObjState;
  setTestState: (payload: string) => void;
  setTestObjState: (payload: ITestObjState) => void;
}

export const useTestStore: UseBoundStore<StoreApi<ITestStore>> = create(
  devtools((setState, getState, store) => ({
    testState: "default value",
    testObjState: {id: 0, title: "title", content: ""},
    setTestState: (payload: string) => setState({testState: payload}),
    setTestObjState: (payload: ITestObjState) => setState({testObjState: payload}),
  })),
);

 

위 예제코드처럼 단순하게 devtools를 감싸주기만 하면 Redux dev-tools를 활용하여 디버깅이 가능해진다.

 

✍️마치며...

기존에 Redux로 상태관리 라이브러리를 입문했지만, Zustand를 접하고 나서부터는 Redux를 거의 사용하지 않았던 것 같다. 그 만큼 DX가 좋은 라이브러리라고 생각이 들었고 대세 라이브러리로 부상한 의미를 바로 알 수 있었다. 실제로 현재 다니는 회사에서 새로 시작하게된 프로젝트에서 이전 프로젝트에서 활용하던 Redux를 그대로 채용하자고 했을 때 Zustand를 적극 추천하며 팀원을 설득한 끝에 Zustand를 본격 도입할 수 있었다. 그 이후 모두에게 확실히 편하게 써서 좋다는 말을 듣고 팀 단위에서 DX를 높인 것 같아 괜히 뿌듯했다. ㅎ

2024. 7. 13. 20:17

TinyMCE의 이미지 첨부

기본적으로 이미지 첨부 기능을 제공하지만,

UX를 고려했을 때 사용자에게 익숙하지 않은 방식의

이미지 첨부 기능을 제공하고 있다.

 

  • 기본으로 제공되는 TinyMCE 이미지 첨부 방식

위 이미지 처럼 기본적으로 소스를 붙여넣어서 첨부하는 방식으로

제공되고 있다.

 

이러한 방식은 UX에 좋지 않다는 생각이 들었고

업로드 버튼을 통해 이미지를 업로드하는 방식으로

UX를 향상시키고자 커스텀 방식에 대해 알아보게 되었고

 

실제로 커스텀하여 업로드 버튼을 통해서도 이미지 첨부가

가능하도록 만든 경험과 방법을 공유하고자 포스팅을 하게 됐다.

 

라이브러리는 이전 포스팅 https://zeriong.tistory.com/65

과 동일하게  tinymce/tinymce-react 를 활용하여 구현했다.

 

소스코드

const TinyMceEditor = () => {
  return (
    <Editor
      apiKey={YourApiKey}
      init={{

        // 필요한 툴을 공식 document에서 찾은 후 추가
        plugins: [ ... ],

        // 플러그인에 추가한 것을 툴바에 표시하기 위한 객체(생략)
        toolbar: " ... ",

        // 이미지 툴바 클릭 시 이미지 파일 첨부 설정 추가
        image_title: true,
        automatic_uploads: true,
        file_picker_types: "image",
        
        // 업로드 버튼을 눌렀을 때 실행되는 함수
        file_picker_callback: (callback, value, meta) => {
          const input = document.createElement("input");
          input.setAttribute("type", "file");
          input.setAttribute("accept", "image/*");

          input.addEventListener("change", (e) => {
            const file = e.target.files[0];
            const reader = new FileReader();

            reader.addEventListener("load", () => {
              // 파일 블로핑
              const id = "blobid" + new Date().getTime();
              const blobCache = window.tinymce.activeEditor.editorUpload.blobCache;
              const base64 = reader.result.split(",")[1];
              const blobInfo = blobCache.create(id, file, base64);
              blobCache.add(blobInfo);

              // 콜백을 호출하고 파일 이름으로 파일 데이터(file의 meta) 파일이름 적용
              callback(blobInfo.blobUri(), {title: file.name});
            });
            // 블로핑된 파일 URL을 읽어옴
            reader.readAsDataURL(file);
          });

		  // run function
          input.click();
        },
        
		// ...
        
      }}
    />
  );
};

컴포넌트 형식으로 구현하였고 필요한 코드만을 적어두었다.

나머지 필요 요소는 공식 문서(https://www.tiny.cloud/docs/tinymce/latest/)

를 확인하여 추가해주면 아래 이미지처럼

구현할 수 있게 된다.

 

  • 소스 인풋 옆에 추가된 업로드 버튼

 

마치며...

회사에서 여러 프로젝트를 진행하며 요구사항에 맞춘 라이브러리 커스텀이나 구현해보지 못한기능을 구현하는 일이 점점 잦아지는 것 같다. 새로운 기능을 구현하며 느낀 것 언제나 "정답은 공식문서(Docs)에서 찾을 수 있다." 라는 것이다. 타 블로그나 커뮤니티 글을 찾아보기 이전에 공식문서를 자세히 살펴 본 후 추가적인 아이디어를 얻기 위해 블로그 또는 커뮤니티 글을 참고하여 구현하는 것이 해당 라이브러리에 대한 이해도를 높이면서 활용도를 확장시킬 수 있는 것 같다.

'Frontend > Editor Tool' 카테고리의 다른 글

[ TinyMCE ] 1000회 무료 에디터툴!  (0) 2024.05.04
2024. 5. 19. 21:55

이번 포스팅에서 다룰 내용은

정말 단순하게 컴포넌트 순서에 대한 것이다.

 

제목에 "모달" 을 붙인 이유는 필자가

한 화면에서 여러 모달을 필요로 하고

모달 위에 또 모달을 띄우는 겹모달을

구현해야 할 때 컴포넌트 순서를 정리하여

간결하게 해결하는 것이 개인적으로

개발자 경험이 가장 좋다는 생각

들었기 때문에 포스팅하기로 했다.

 

컴포넌트의 순서

사실 컴포넌트의 순서라고 해봐야 결국

위에서 아래로 순서대로 쌓인다고 볼 수 있다.

필자의 주력인 React로 예시를 들어보겠다.

const Test = () => {
    return (
        <div>
            <p>1</p>
            <p>2</p>
            <p>3</p>
        </div>
    )
}

 

div 태그의 css 속성이 기본적으로 display: block 이라고 가정한다면

1
2
3

 

위와 같은 형태를 볼 수 있고 만약 display: flex 라고 가정하면 아래와 같이 화면에 나타날 것이다.

123

 

 

어찌보면 너무너무 당연한 이야기다.

 

하지만 position: fixed 또는 absolute라면 어떤가?

 

분명 컴포넌트의 순서를 생각해보지 않았다면

혼란이 올 것이다.

 

필자의 경우 위 123 태그들을 fixed로 띄운다면

순서에 관계 없이 동일선상에 띄워지는 것으로

생각하고 있었다.

 

그렇기 때문에 123 태그들의 우선순위를

각각 지정해주기 위해서는 css속성인 z-index를

활용해서 순서를 반드시 지정해주어야 하는 것으로

생각하고 있었다.

 

이 방법은 일차원적이면서도 가장 확실한 방법이기도 하다.

 

결론으로 이러한 상황에서 가장 간결하고

빠르게 해결할 수 있는 방법이 바로 컴포넌트의

순서를 활용하는 것이다.

 

일반적으로 html 태그가 위에서 아래로 쌓이는 것과

마찬가지로 position이 fixed거나 absolute라면

화면의 z축을 기준으로 태그가 한장씩 쌓이게 되는

개념이 되는 것이다.

 

즉, fixed 또는 absolute position을 가진 태그는

아래에 있는 컴포넌트일수록 더 높이 위치해있다고

볼 수 있다.

 

아래 예시 코드를 확인해보자.

const YoutubeQueuePlay = () => {
    return (
        <>
            {/* transform을 설정하여 내부 컨텐츠 fixed 의 기준을 지정 */}
            <div className="transform flex flex-col pc:flex-row w-full h-full cursor-default relative">

                {/* 저장된 플레이리스트 버튼 */}
                <SavedMusicListButton/>

                {/* 플레이어 컨텐츠 섹션 */}
                <PlayerSection/>

                {/* 어사이드 바 */}
                <PlayerAside/>
            </div>

            {/* 신청/수정 모달 */}
            <EditModal/>

            {/* 미리보기 모달 */}
            <PreViewModal/>
            
            {/* 저장된 리스트 모달 */}
            <SavedListModal/>
        </>
    )
}

위에서 총 3가지 모달이 있다.

< 신청, 수정 모달 >

< 저장된 리스트 모달 >

< 미리보기 모달 >

 

여기에서 이중 모달이 필요한 경우는

< 저장된 리스트 모달 >에서 저장된 유튜브의

미리보기를 볼 수 있도록 하기 위해서

저장된 리스트 모달 위에 < 미리보기 모달 >

띄워야 하는 경우이다.

 

이러한 경우 < 미리보기 모달 >

< 저장된 리스트 모달 > 보다 상단에 위치해야 한다.

 

그렇다면 지금 위 예시코드로는 불가능하다.

< 미리보기 모달 > < 저장된 리스트 모달 > 보다

상단에 존재하기 때문에 < 저장된 리스트 모달 >

우선순위가 더 높아져서 미리보기를 눌러서

< 미리보기 모달 >을 띄워도 가려지게 된다.

 

해결법은 단순히 순서만 바꿔주면 된다.

const YoutubeQueuePlay = () => {
    return (
        <>
            ...

            {/* 신청/수정 모달 */}
            <EditModal/>
            
            {/* 저장된 리스트 모달 */}
            <SavedListModal/>
            
            {/* 미리보기 모달 */}
            <PreViewModal/>
        </>
    )
}

 

 

마치며...

프로그래밍에 대한 이해도가 낮았을땐 결론을 짧은 경험과 유추만으로 결정짓는 습관이 고질적인 문제였다. 하지만 꾸준히 공부하고 개발 경험을 늘려갈수록 더 많은 것들이 보이게 되고 더 겸손해지고 부족함을 많이 느끼게 되는 것 같다. 스스로 유추하고 확신을 가지기보다 근거를 기반으로 확신을 내리는 방향성을 절대로 잊지 말자.

2024. 5. 4. 13:43

Editor Tool

일반적으로 에디터 툴은 프론트에서 서비스 이용자가

다양한 스타일로 글을 작성할 수록 있도록 돕는 툴이다.

다양한 에디터 툴이 존재하며 필자는 에디터 툴하면

가장 먼저 Toast UI Editor가  떠오른다.

 

필자 역시 개발을 진행하다 보면 에디터 툴이 필요하여

여러 에디터 툴을 알아보았고 Toast UI Editor를

사용하고 싶었으나 리액트 최신 버전을 지원하지 않아

당장은 사용할 수 없는 상황이었다.

 

필자는 리액트를 주력으로 개발하기에

결국 리액트 최신버전을 지원하는 에디터 툴을

찾다가 TinyMCE라는 에디터 툴을 알게되었고

 

해외에서는 꽤 활발한 커뮤니티가 형성되어 있어

접근성이 좋을 것으로 판단했고 리액트 최신버전을

지원하고 있어 선택하게 되었다.

 

TinyMCE

이번에 소개할 에디터 툴은 TinyMCE이다.

회원가입 시 계정 API key를 제공하고

월 1,000회 로드를 무료로 제공하고 있다.

 

Microsoft, NASA, DRIFT, ATLASSIAN 등 해외 유명 기업에서

많이 활용되고 있고 확장성이 뛰어나 다양하게

활용할 수 있는 장점이 있고

 

React 주력 개발자로서 React 최신버전을 지원한다는

면이 필자는 가장 마음에 들었다.

또한 React, Vue, Angular, JAVA 등 다양한 언어를

지원하고 있어 주력 에디터로서도

좋은 선택지라는 생각이 들었다.

 

Install

npm install --save @tinymce/tinymce-react

 

 

예시코드

import React from 'react';
import { Editor } from '@tinymce/tinymce-react';

export default function App() {
  return (
    <Editor
      apiKey='your api key'
      init={{
        plugins: 'anchor autolink charmap codesample emoticons image link lists media searchreplace table visualblocks wordcount checklist mediaembed casechange export formatpainter pageembed linkchecker a11ychecker tinymcespellchecker permanentpen powerpaste advtable advcode editimage advtemplate ai mentions tinycomments tableofcontents footnotes mergetags autocorrect typography inlinecss markdown',
        toolbar: 'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | link image media table mergetags | addcomment showcomments | spellcheckdialog a11ycheck typography | align lineheight | checklist numlist bullist indent outdent | emoticons charmap | removeformat',
        tinycomments_mode: 'embedded',
        tinycomments_author: 'Author name',
        mergetags_list: [
          { value: 'First.Name', title: 'First Name' },
          { value: 'Email', title: 'Email' },
        ],
        ai_request: (request, respondWith) => respondWith.string(() => Promise.reject("See docs to implement AI Assistant")),
      }}
      initialValue="Welcome to TinyMCE!"
    />
  );
}

Tiny MCE에서 제공하고 있는 아주 간단 소개정도의 구현코드이다.

ai_request 기능을 제공하고 있어 ai를 활용할 수 있는

몇가지 기능도 제공하는 것으로 보인다.

 

다음에는 현재 필자가 직접 커스텀한 방식이나 방법에 대해서

Tiny MCE 게시글을 추가적으로 포스팅할 예정이다.

 

마치며...

서비스 이용자로서 에디터 툴은 많이 사용해봤지만 막상 서비스를 만드는 개발자 입장에서 에디터 툴을 직접 구현하기 위해서는 복잡하고 많은 프로세스를 요구한다는 것을 깨닫고 직접적으로 구현한다는 것은 매우 어렵다는 것을 깨달았다. 좋은 에디터 툴을 라이브러리로 제공하는 선두 개발자들에게 깊은 감사를 느꼈다... 그리고 이번 경험으로 또 하나의 목표가 생겼다. 바로 에디터 툴을 직접 구현해보는 것이다. 혼자의 힘으론 어려울 수 있겠지만 분명 좋은 경험이 될 것 같다.

2024. 4. 4. 21:25

findIndex

특정 배열에서 중복 객체를 제외하는 많은 방법 중

주관적인 견해로 가장 간결하게 처리할 수 있는 방법으로

findIndex를 활용하는 방식을 채택하여 실제 프로젝트에

적용한적이 있다.

 

findIndex매서드는 iterable한(반복 가능한) 객체에서

사용 가능한 매서드로 해당 객체를 0번째 요소부터

순회하며 매개변수내에서 true를 반환하게 되면

해당 요소의 index를 반환하는 매서드이다.

 

사용예시

const array = [{id:1, title: "test"}, {id:2, title: "test"}, {id:3, title: "test"}, ];

// id가 2인 객체의 인덱스를 반환
const findSecondId = array.findIndex((value) => value.id === 2);

console.log(findSecondId); // 1

이런 방식으로 특정 조건을 만족하게 되면 true를 반환하게 되고

findIndex는 true를 감지하면 순회를 멈추고 해당 데이터의 index를

반환하게 된다.

 

어떻게 findIndex로 중복 객체를 제거 할까?

순회의 프로세스를 순차적으로 이해한다면

비교적 간단하게 접근하여 적용할 수 있다.

 

실질적인 데이터베이스에 저장된 데이터들은

각각 고유값(id)을 가지고 있기 때문에 이를 통해서

중복 객체를 제거할 수 있다.

 

꼭 고유 id가 아니더라도 중복되는 데이터를 타겟으로

filter와 findIndex를 활용한다면 쉽게 가공할 수 있다.

 

  • 예시코드
//  데이터 중 title이 "제목입니다." 인 객체는 createAt, title, id 모두 동일하다.
const data = [
  { id: 1, title: "textTitle.", createAt: 1235 },
  { id: 2, title: "제목입니다.", createAt: 1236 },
  { id: 2, title: "제목입니다.", createAt: 1236 },
  { id: 4, title: "newTitle.", createAt: 1238 },
  { id: 5, title: "allNewTitle.", createAt: 12359 },
];
// data 깊은 복사
const data2 = JSON.parse(JSON.stringify(data));

// id를 비교한 경우
const filteredById = data.filter((obj, idx) =>
  (data.findIndex(obj2 => obj.id === obj2.id)) === (idx)
)

console.log(filteredById);
/*
  [
    { id: 1, title: "textTitle.", createAt: 1235 },
    { id: 2, title: "제목입니다.", createAt: 1236 },
    { id: 4, title: "newTitle.", createAt: 1238 },
    { id: 5, title: "allNewTitle.", createAt: 12359 },
  ]
*/


//  createAt를 비교한 경우
const filteredByCreateAt = data2.filter((obj, idx) =>
  (data2.findIndex(obj2 => obj.createAt === obj2.createAt)) === (idx)
)

console.log(filteredByCreateAt);
/*
  [
    { id: 1, title: "textTitle.", createAt: 1235 },
    { id: 2, title: "제목입니다.", createAt: 1236 },
    { id: 4, title: "newTitle.", createAt: 1238 },
    { id: 5, title: "allNewTitle.", createAt: 12359 },
  ]
*/

 

왜 중복이 제거되는걸까?

원리는 아주아주 심플하다.

배열 내의 특정 데이터가 유일한 경우에는 해당 데이터를 가진

데이터의 index 또한 유일해야만 한다.

하지만 같은 데이터가 존재한다면 index만 유일한 값이 되고

특정 데이터는 중복된 데이터를 가지게 된다.

 

그렇기 때문에 배열에서 반드시 유일할 수 밖에 없는

index값을 비교할 수 있도록 findIndex와 filter의 index를

활용하면 중복되는 특정 값을 가진 객체를 제외시킬 수 있다.

 

findIndex는 index 0부터 탐색을 시작하고 해당 조건이 맞는 순간

순회를 멈추고 해당 index를 반환하기 때문에 순회 도중

같은 데이터가 발견되면 서로 index가 달라지기 때문

 

filter의 조건인 currentIndex === findIndex 가 false를

반환하게 되어 결국 중복된 데이터는 제외하고 반환하게

되기 때문이다.

 

마치며...

간혹 개발을 하다보면 이론적으로는 알고 있지만 막상 코드로 구현하고자 할 때 이상하리 만치 고민하게 되는 경우가 종종 있는 것 같다. 이번의 경우도 그랬다. 분명 어려운 것은 아니지만 딱히 활용할만한 상황이 없어서 였는지 설계할 때 고민을 꽤 했던 것 같다. 이론적으로 알고 있더라도 결국 직접 문제를 직면하고 해결하기 위해 설계를 떠올려보고 구상해보는 것이

더 깊은 이해도를 가질 수 있고 내 것으로 만들 수 있는 것 같다. 기술이든 개념이든 내가 설명할 수 있을 정도의 이해도를 갖춰야만 활용할 수 있다는 것을 깨달았다. 앞으로도 내가 아는 것을 설명할 수 있는 사람이 되자.

2024. 4. 3. 21:10

useNavigate

react에서 Client Side Routing을 편리하게 사용할 수 있도록 돕는

"react-router-dom"에서 제공하는 훅으로 react에서 주로 페이지를

동적으로 이동할 때 사용되는 훅이다. Link 태그와 다르게 훅으로서

함수와 함께 사용 가능하기 때문에 다양하게 활용 가능한 것이

특징이다.

 

useNavigate 사용예시

import { useNavigate } from 'react-router-dom';

const navigate = useNavigate();

// 로그인 페이지로 이동
navigate("/login");

// 이전 페이지로 이동
navigate(-1);

기본적인 사용 예시는 위와 같다.

단순하게 Route의 pathName을 기재하는 것 뿐아니라
이전 페이지로 가는 경우 -1을 매개변수로 넣는 등

다양한 기능을 제공한다.

 

이동할 페이지에 State 전달

react에서 useState의 setState를 활용하여 state를 값을 얻는 방법 외

useNavigate의 state option을 사용하면 페이지로 이동 후

이동한 페이지에서 useLocation 훅을 통해 state를 전달 받는 것이 가능하다.

이러한 기능을 활용한다면 URL 파라미터에 값을 노출시키지 않고

특정 값을 private하게 전달해 줄 수 있다.

 

Navigate State 사용예시

/** current.jsx */
import { useNavigate } from 'react-router-dom';

export const current = () => {
  const navigate = useNavigate();

  const linkToNext = () => {
    // next 페이지로 이동하며 { state: fromCurrentPage } 전달
    navigate("/next", { state: { fromCurrentPage: "Hello, Next page?" } });
  }
    
  return <div onClick={linkToNext}> currentPage </div>
}



/** next.jsx */
import { useLocation } from 'react-router-dom';

const Next = () => {
  const location = useLocation();
  
  // "Hello, Next page?"
  console.log(location.state.fromCurrentPage);
  
  return <div> Filnally... Next Page! </div>
}

 

 

마치며...

약 2년간 react 를 주력으로 다뤄왔지만 useNavigate를 활용해서 다음 페이지로 state를 전달할 수 있다는 사실을 이번에 처음알게되었다... 다시금 부족함을 느끼고 멈추지 않고 꾸준히 정진해야겠다는 마음을 굳게 다지는 계기가 되었다.

'Frontend > React' 카테고리의 다른 글

[ React ] 자바스크립트 라이브러리  (0) 2024.01.10