Skip to content

Conversation

@cskime
Copy link
Collaborator

@cskime cskime commented Sep 11, 2025

요구사항

기본

  • 목록 조회
    • ‘로고’ 버튼을 클릭하면 ‘/’ 페이지로 이동합니다. (새로고침)
    • 진행 중인 할 일과 완료된 할 일을 나누어 볼 수 있습니다.
  • 할 일 추가
    • 상단 입력창에 할 일 텍스트를 입력하고 추가하기 버튼을 클릭하거나 엔터를 치면 할 일을 새로 생성합니다.
  • 할 일 완료
    • 진행 중 할 일 항목의 왼쪽 버튼을 클릭하면 체크 표시가 되면서 완료 상태가 됩니다.
    • 완료된 할 일 항목의 왼쪽 버튼을 다시 클릭하면 체크 표시가 사라지면서 진행 중 상태가 됩니다.

심화

주요 변경사항

  • 할 일 목록 페이지를 구현합니다.
  • 최초 할 일 목록을 렌더링하는 로딩 시간을 단축하기 위해 SSR 방식으로 렌더링합니다.
  • 할 일을 추가하고 상태를 변경하는 동안 loading 중임을 나타내기 위해 Portal을 사용해서 dimmed view를 보여줍니다.

스크린샷

localhost_3000_

멘토에게

  • 렌더링 방식 결정 질문
    • 할 일 목록 페이지에서 초기 데이터 로딩을 기다리는 시간을 제거하고, 이 페이지에 접속할 때 항상 최신 데이터를 보여주기 위해 getServerSideProps() 함수를 추가해서 SSR 방식으로 렌더링되도록 구현했습니다.
    • 그런데, Next.js 문서에서는 가능하면 SSG 방식을 사용하고 필요할 때만 SSR 방식을 사용하는 것을 권장하는 것 같았습니다. (문서 링크는 찾지 못했습니다..)
    • 그럼, 할 일 목록 페이지를 On-Demand ISR 방식으로 구현하는게 권장되는 방식일까요?
    • 이 미션의 경우는 너무 단순해서 별로 차이가 없을 것 같긴 한데요. 그래서 더더욱 SSR 대신 ISR로 구현하는게 맞는건지 궁금합니다.
  • React portal 사용 관련 질문
    • 로딩 중에 화면 전체를 덮는 dimmed view를 추가하기 위해 아래와 같이 <Portal> component를 구현했습니다.
      export default function Portal({ children }) {
        const [container, setContainer] = useState(null);
      
        useEffect(() => {
          setContainer(document.getElementById("portal"));
        }, []);
      
        return container && createPortal(children, container);
      }
    • 그리고, portal로 등록한 요소를 보여줄 <div id="portal"></div>_document.js에 추가했습니다.
      export default function Document() {
        return (
          <Html lang="ko">
            <Head>...</Head>
            <body>
              <Main />
              <NextScript />
              <div id="portal"></div>
            </body>
          </Html>
        );
      }
    • 이렇게 추가한 <div> tag는 npm run dev를 실행했을 때는 나타나지 않다가, npm run buildnpm run start를 실행했을 때만 나타나고 정상 동작합니다.
    • 개발 모드에서는 _document.js에 추가한 요소가 왜 HTML에 추가되지 않는건가요? 그리고, 개발모드에서도 <div id="portal"> 요소가 HTML에 추가되려면 어떻게 수정해야 할까요?
  • Font 사용 질문
    • Figma 시안은 HS산토끼와 나눔스퀘어 두 가지 폰트를 사용하고 있어서 각 폰트를 추가해 주었습니다.
    • 그런데, 나눔스퀘어와 HS산토끼 폰트는 각각 설정하는 방법이 달랐는데요.
      1. HTML에서 <link>로 추가
      2. CSS에서 @font-family로 추가
    • 보통 두 가지 방식 중 어떤 방식을 주로 사용하는지, 어떤 기준으로 선택하면 좋은지 궁금합니다.

@cskime cskime self-assigned this Sep 11, 2025
@cskime cskime changed the base branch from main to Next-김참솔 September 11, 2025 09:27
@cskime cskime requested a review from kiJu2 September 11, 2025 09:27
@cskime cskime added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Sep 11, 2025
@kiJu2
Copy link
Collaborator

kiJu2 commented Sep 12, 2025

스프리트 미션 하시느라 수고 많으셨어요.
학습에 도움 되실 수 있게 꼼꼼히 리뷰 하도록 해보겠습니다. 😊

@kiJu2
Copy link
Collaborator

kiJu2 commented Sep 12, 2025

렌더링 방식 결정 질문

  • 할 일 목록 페이지에서 초기 데이터 로딩을 기다리는 시간을 제거하고, 이 페이지에 접속할 때 항상 최신 데이터를 보여주기 위해 getServerSideProps() 함수를 추가해서 SSR 방식으로 렌더링되도록 구현했습니다.
  • 그런데, Next.js 문서에서는 가능하면 SSG 방식을 사용하고 필요할 때만 SSR 방식을 사용하는 것을 권장하는 것 같았습니다. (문서 링크는 찾지 못했습니다..)
  • 그럼, 할 일 목록 페이지를 On-Demand ISR 방식으로 구현하는게 권장되는 방식일까요?
  • 이 미션의 경우는 너무 단순해서 별로 차이가 없을 것 같긴 한데요. 그래서 더더욱 SSR 대신 ISR로 구현하는게 맞는건지 궁금합니다.

유경험자셔서 오히려 더 혼동이 있으신 것 같아요.
지금 질문 주신 것은 "SSR로 구현해야 하는건가? ISR로 구현해야 하는건가?"를 헷갈리시다는 것은 곧 "ISR"로도 만들 수 있고 "SSR"로도 만들 수 있다. 라는 거지요? 논리적으로는 두 방법 모두 요구사항을 해결할 수 있습니다.
그렇다면 "어떤 방식이 올바른가?"보다는 "어떤 방식이 더 나은 방법일까?"를 고민하고 계신 것으로 보여요 !
질문 주신 것을 보니 ISR과 SSR 모두 잘 이해 하신 것으로 보이는군요.

말씀주신대로 해당 미션은 단순해서 별로 차이가 없을 것이긴 하지만 저였다면 SSR로 구현했을 것 같아요.
이유는 페이지에 데이터 변화가 많은 만큼 무효화 처리가 많을 것이라고 판단되기 때문이예요 ! (사실 지금 규모에서는 미미하긴 할거예요.)

좀 더 규모를 확장해서 100만 유저를 보유한 어플리케이션의 "마이 페이지"로 예시를 들어보면 어떨까요?
"마이 페이지"도 ISR과 SSR 둘 다 처리할 수 있을거예요. 참솔님이 이해하신 SSR과 ISR 둘 중 어떤 것을 선택하시는게 좋을 것 같으신가요?
잠시 멈추고 조금 생각해봅시다.

만약 저였다면 SSR + CSR로 만들었을거예요.
100만 유저의 각각 사전 렌더링 HTML 파일을 물리적으로 가지고 있다는 것도 스토리지에 적지않은 부하가 있을 것이기에 차라리 특정 API 요청을 캐싱 해주고 렌더링은 동적으로 다루는 것도 좋은 전략이 될 수 있겠죠 !(= ISR을 선택하는 것만으로 성능을 향상시킬 필요 없지요 !)

지금 정말 좋은 것은 참솔님은 이미 ISR과 SSR을 완벽히 이해했다는 점입니다. 👏
개발에는 정답이 무수히 많기에 현재 상황에 맞는 판단에 따라 설계되는 점이 참 매력있죠.

이런 생각도 해볼 수 있을 것 같네요.
ISR이 렌더링 성능 측면에서 더 좋은데 모든 페이지를 ISR로 만들면 되지 않나?

vs

그럼 다음과 같이 비교해볼 수 있을 것 같아요.
RAM이 ROM보다 훨씬 빠른 메모리인데 모든 메모리를 RAM으로 처리하면 되지 않나?

결국 적절한 비용에 대한 저울질 싸움이군요 🤣

@kiJu2
Copy link
Collaborator

kiJu2 commented Sep 12, 2025

React portal 사용 관련 질문

  • 로딩 중에 화면 전체를 덮는 dimmed view를 추가하기 위해 아래와 같이 <Portal> component를 구현했습니다.
    export default function Portal({ children }) {
      const \[container, setContainer\] \= useState(null);
    
      useEffect(() \=> {
        setContainer(document.getElementById("portal"));
      }, \[\]);
    
      return container && createPortal(children, container);
    }
  • 그리고, portal로 등록한 요소를 보여줄 <div id="portal"></div>_document.js에 추가했습니다.
    export default function Document() {
      return (
        <Html lang\="ko"\>
          <Head\>...</Head\>
          <body\>
            <Main />
            <NextScript />
            <div id\="portal"\></div\>
          </body\>
        </Html\>
      );
    }
  • 이렇게 추가한 <div> tag는 npm run dev를 실행했을 때는 나타나지 않다가, npm run buildnpm run start를 실행했을 때만 나타나고 정상 동작합니다.
  • 개발 모드에서는 _document.js에 추가한 요소가 왜 HTML에 추가되지 않는건가요? 그리고, 개발모드에서도 <div id="portal"> 요소가 HTML에 추가되려면 어떻게 수정해야 할까요?

오잉? 일단 최신 참솔님 브랜치의 최신 _document.js는 다음과 같이 만들어진 것 같아요:

import { Head, Html, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html lang="ko">
      <Head>
        <title>Do It</title>
        <meta
          name="description"
          content="A productivity app to help you get things done"
        />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link
          rel="stylesheet"
          type="text/css"
          href="https://cdn.jsdelivr.net/gh/moonspam/[email protected]/nanumsquare.css"
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

질문주셨던 코드와 조금 다르긴 하지만 여기서 질문 주신 것처럼 임의의 div를 추가해봤습니다:

// ...
      <body>
        <Main />
        <NextScript />
        <div id="portal" />
      </body>
// ...
image

저는 잘 보이는 것 같군요 ! 😅

@kiJu2
Copy link
Collaborator

kiJu2 commented Sep 12, 2025

Font 사용 질문

  • Figma 시안은 HS산토끼와 나눔스퀘어 두 가지 폰트를 사용하고 있어서 각 폰트를 추가해 주었습니다.
  • 그런데, 나눔스퀘어와 HS산토끼 폰트는 각각 설정하는 방법이 달랐는데요.
    1. HTML에서 <link>로 추가
    2. CSS에서 @font-family로 추가
  • 보통 두 가지 방식 중 어떤 방식을 주로 사용하는지, 어떤 기준으로 선택하면 좋은지 궁금합니다.

구글 폰트나 네이버 폰트에서 제공(외부제공자)하는 CDN을 통해서 폰트를 불러오는 것이 질문이라면
외부 제공자에서 로그하게 되면 아주 간편하다는 장점이 있으나, 제어권이 외부에 있다는 단점이 있을 수 있습니다 !

link로 외부 제공자로부터 받는 방법은 써드파티 서버가 다운되면 폰트를 로드할 수 없게 된다는 단점이 있겠네요.
다만, 매우 간편하고 CDN 캐시 덕에 빠르게 설치할 수 있을 것으로 보여져요.

만약 link를 외부 제공자가 아니라 /fonts/HS...과 같이 셀프 호스팅된 자원을 로드의 경우를 질문주시는 경우도 있을 수 있겠네요.
프로덕트에 폰트를 내장시키는 방법은 완전한 제어권을 가지게 된다는 장점이 있을 것 같네요.

그럼 html에서 css를 로드하여 @font-family를 만나는 것과 css로 폰트를 로드하는 것의 차이점에 대한 질문으로 귀결되는 것 같네요.

결국 link로 불러오는 것도 @font-face가 들어있는 css를 요청하는 것이고, css에서 직접 @font-face를 작성하는 것도 html에서 css를 파싱하는 중에 필요한 폰트를 로드하게 될거예요. (결국 별 차이 없을 것 같다는 말..)

규모가 좀 크고 여러 앱(마이크로 서비스 아키텍쳐 같은)들이 같은 폰트를 불러와야 되는 상황이라면 자체 cdn에 css 파일을 배포하여 관리할 것 같네요.
만약 막 시작한 스타트업의 경우 간단하게 외부 제공자로부터 사용할 것 같습니다 !
이 때 next/font 사용하시면 좋습니다. 빌드타임 때 폰트 로드해주는 거로 알고있어요 😉

Comment on lines +1 to +5
const BUTTON_TYPE = Object.freeze({
add: "add",
edit: "edit",
delete: "delete",
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오우... Object.freeze라니. ㄷㄷ 꼼꼼하시군요? 👍

어라 근데 왜 타입스크립트는 안사용하셨나요? 🤔

Copy link
Collaborator Author

@cskime cskime Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

강의가 JavaScript로 진행되어서 별 생각 없이 이어지는 미션도 JavaScript를 사용했어요 ㅎㅎ.. 미션 10은 TypeScript로 바꾼 다음 진행할 예정입니다!

Comment on lines +8 to +12
<meta
name="description"
content="A productivity app to help you get things done"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굿굿. meta도 잊지 않고 잘 작성 하셨네요 👍

Comment on lines +4 to +25
const className = {
[BUTTON_TYPE.add]: `${styles.button} ${styles.add}`,
[BUTTON_TYPE.edit]: `${styles.button} ${styles.edit}`,
[BUTTON_TYPE.delete]: `${styles.button} ${styles.delete}`,
};

const leadingIcon = {
[BUTTON_TYPE.add]: "/icons/ic-plus.svg",
[BUTTON_TYPE.edit]: "/icons/ic-check.svg",
[BUTTON_TYPE.delete]: "/icons/ic-xmark-white.svg",
};

function Button({ children, type = BUTTON_TYPE.add, ...props }) {
return (
<button className={className[type]} {...props}>
<div className={styles.trailingIcon}>
<img src={leadingIcon[type]} alt={type} />
</div>
{children}
</button>
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

크으 공통 컴포넌트가 참 깔끔하네요 👍

다만, propstype이라는 이름 보다는 color(primary, secondary, warning 등)이라고 지을 수도 있었겠네요.
...
라고 작성하려 하였으나 방금 디자인 문서 보고 오니까 디자이너가 정의해준 대로 하셨던거군요 ! 😅😅

Comment on lines +1 to +3
function colorVariable(name, shade) {
return `var(--color-${name}-${Math.max(100, Math.min(900, shade))})`;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

크으.. 여기서도 꼼꼼함이 돋보이는군요.

string은 참 다루기 어려워요. 오타 하나라도 나면 에러가 발생하는 것도 아니고..
이렇게 함수로 만들어두면 휴먼에러 발생률이 현저히 줄겠네요 👍👍
좋은 아이디어입니다 !

Comment on lines +3 to +10
function TodoLabel({ done }) {
let className = styles.todoLabel;
if (done) {
className += ` ${styles.done}`;
}

return <span className={className}>{done ? "DONE" : "TO DO"}</span>;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(제안) 좀 더 확장성있게 만들어볼 수 있겠군요 !

const colorMap = {
  success: styles.success,
  warning: styles.warning,
  error: styles.error,
  info: styles.info,
};

function Label({ status = "info", children }) {
  const baseClass = styles.label;
  const statusClass = colorMap[status] || "";
  const className = `${baseClass} ${statusClass}`.trim();

  return <span className={className}>{children}</span>;
}

위 제안드린 코드는 디자인에 명시된 프로퍼티는 아니지만 핵심은 "컬러를 결정짓는 props"와 "컨텐츠"를 결정짓는 props를 구분하면 어떨까 ! 라는 제안입니다 😉😉

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

크으 ~ 유용하게 쓰시는군요 😂

훌륭합니다. 직접 http client도 만드시고 👍👍👍

Comment on lines +3 to +11
export async function getTodos() {
try {
const client = new Client();
const items = await client.get("items");
return items;
} catch (error) {
return [];
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(제안) 에러가 났는지 안났는지 유저는 모를 수 있겠군요 !

Suggested change
export async function getTodos() {
try {
const client = new Client();
const items = await client.get("items");
return items;
} catch (error) {
return [];
}
}
export async function getTodos() {
try {
const client = new Client();
const items = await client.get("items");
return items;
} catch (error) {
console.error(error?.data?.message ? error.data.message : "Unknown error"); // 예시입니다 !
throw error;
}
}

만약 에러가 발생했다면 해당 api 함수에서 해야 할 에러 처리(로깅과 같은)를 하고 호출부에 에러를 알리는 것이 어떨까 싶습니다 !
그리고 호출부에서 에러가 발생한다면 어떻게 사용자에게 출력할 것인지 결정해볼 수 있겠네요 😉

@kiJu2
Copy link
Collaborator

kiJu2 commented Sep 12, 2025

미션 수행하시느라 수고하셨습니다 참솔님 ~~!!!
여전히 미션 진행도 선두에 있으시군요 😂

질문도 참 재미있는 질문으로 느껴졌습니다. 답변드리고 싶어서 시간가는 줄 모르고 작성한 것 같아요 😏

역량은 충분하시니 참솔님은 수료 후 곧바로 원하시는 길을 빠르게 가실 수 있을거라 생각됩니다 👍

@kiJu2 kiJu2 merged commit aa5e087 into codeit-bootcamp-frontend:Next-김참솔 Sep 12, 2025
@cskime cskime deleted the Next-김참솔-sprint9 branch September 12, 2025 08:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants