inflearn logo
강의

강의

N
챌린지

챌린지

멘토링

멘토링

N
클립

클립

로드맵

로드맵

지식공유

묻고 답해요

173만명의 커뮤니티!! 함께 토론해봐요.

해시태그 검색하기 강의에서

미해결

[리뉴얼] React로 NodeBird SNS 만들기

import { Card, Button, Avatar, Popover, List, Comment } from "antd"; import { RetweetOutlined, HeartOutlined, MessageOutlined, HeartTwoTone, EllipsisOutlined, } from "@ant-design/icons"; import { useDispatch, useSelector } from "react-redux"; import PropTypes from "prop-types"; import PostImages from "./PostImages"; import { useCallback, useEffect, useState } from "react"; import CommentForm from "./CommentForm"; import PostCardContent from "./PostCardContent"; import FollowButton from "./FollowButton"; import { REMOVE_POST_REQUEST, LIKE_POST_REQUEST, UNLIKE_POST_REQUEST, RETWEET_REQUEST, } from "../reducers/post"; import Link from "next/link"; const PostCard = ({ post }) => { const dispatch = useDispatch(); const { removePostLoading } = useSelector((state) => state.post); const [commentFormOpened, setCommentFormOpened] = useState(false); const id = useSelector((state) => state.user.me?.id); const onLike = useCallback(() => { if (!id) { return alert("로그인이 필요합니다."); } return dispatch({ type: LIKE_POST_REQUEST, data: post.id, }); }, [id]); const onUnLike = useCallback(() => { if (!id) { return alert("로그인이 필요합니다."); } return dispatch({ type: UNLIKE_POST_REQUEST, data: post.id, }); }, [id]); const onToggleComment = useCallback(() => { setCommentFormOpened((prev) => !prev); }, []); const onRemovePost = useCallback(() => { if (!id) { return alert("로그인이 필요합니다."); } return dispatch({ type: REMOVE_POST_REQUEST, data: post.id, }); }, [id]); const onRetweet = useCallback(() => { if (!id) { return alert("로그인이 필요합니다."); } return dispatch({ type: RETWEET_REQUEST, data: post.id, }); }, []); const liked = post.Likers?.find((v) => v.id === id); return ( <div style={{ marginBottom: 10 }}> <Card cover={post.Images[0] && <PostImages images={post.Images} />} actions={[ <RetweetOutlined key="retweet" onClick={onRetweet} />, liked ? ( <HeartTwoTone twoToneColor="#eb2f96" key="heart" onClick={onUnLike} /> ) : ( <HeartOutlined key="heart" onClick={onLike} /> ), <MessageOutlined key="message" onClick={onToggleComment} />, <Popover key="more" content={ <Button.Group> {id && post.user?.id === id ? ( <> <Button>수정</Button> <Button type="danger" onClick={onRemovePost} loading={removePostLoading} > 삭제 </Button> </> ) : ( <Button>신고</Button> )} </Button.Group> } > <EllipsisOutlined /> </Popover>, ]} title={ post.RetweetId ? `${post.user.nickname}님이 공유하셨습니다` : null } extra={id && <FollowButton post={post} />} > {post.RetweetId && post.Retweet ? ( <Card cover={ post.Retweet.Images[0] && ( <PostImages images={post.Retweet.Images} /> ) } > <Card.Meta avatar={ <Link href={`/user/${post.Retweet.user.id}`}> <a> <Avatar>{post.Retweet.user.nickname[0]}</Avatar> </a> </Link> } title={post.Retweet.user.nickname} description={<PostCardContent postData={post.Retweet.content} />} /> </Card> ) : ( <Card.Meta avatar={ <Link href={`/user/${post.user.id}`}> <a> <Avatar>{post.user.nickname[0]}</Avatar> </a> </Link> } title={post.user.nickname} description={<PostCardContent postData={post.content} />} /> )} </Card> {commentFormOpened && ( <div> <CommentForm post={post} /> <List header={`${post.Comments.length}개의 댓글`} itemLayout="horizontal" dataSource={post.Comments} renderItem={(item) => ( <li> <Comment author={item.user?.nickname} avatar={ <Link href={`/user/${post.user?.id}`}> <a> <Avatar>{post.user?.nickname[0]}</Avatar> </a> </Link> } content={item.content} /> </li> )} /> </div> )} </div> ); }; PostCard.propTypes = { post: PropTypes.shape({ id: PropTypes.number, user: PropTypes.object, content: PropTypes.string, createdAt: PropTypes.string, Comment: PropTypes.arrayOf(PropTypes.object), Images: PropTypes.arrayOf(PropTypes.object), Likers: PropTypes.arrayOf(PropTypes.object), RetweetId: PropTypes.number, Retweet: PropTypes.objectOf(PropTypes.any), }).isRequired, }; export default PostCard; 아바타를 클릭하면 해당 아이디 정보하고 그 아이디 게시물만 떠야하는데 해당 아이디 프로필은 잘뜨는데 게시물이 다른사람 게시글까지 같이 뜹니다. 참고로 저는 post.User를 u로 바꿔야지만 뜹니다

  • react
  • redux
  • node.js
  • express
  • next.js
장산 댓글 1 좋아요 0 조회수 468

비동기 요청시 새로고침

해결됨

[리뉴얼] React로 NodeBird SNS 만들기

실제 몇몇 서비스를 보면 글 작성이나 수정같은 비동기 요청이 성공한 후에 새로고침을 하는데 이 강좌와 같이 Next.js를 사용하는 리액트 개발은 새로고침을 하는게 맞는건가요?

  • react
  • redux
  • node.js
  • express
  • next.js
kenny 댓글 1 좋아요 0 조회수 330

복습 정리

해결됨

[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스

안녕하세요 혹시 강의 내용을 복습할 때 깃허브나 블로그에 정리 요약 해서 업로드 해도 될까요?

  • react
  • node.js
  • seo
  • graphql
  • next.js
댓글 1 좋아요 0 조회수 445

for문과 while문 강의 질문

해결됨

[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스

let i = 0; while (i <10) { console.log(i) i = i +1 } 여기서 콘솔로 0 ~10 까지 찍히는데 왜 10까지 찍히는건가요?? 강의 마지막에 편집오류인지 짤려서 이유가 안나와요

  • react
  • node.js
  • seo
  • graphql
  • next.js
멋진남자 댓글 1 좋아요 1 조회수 302

API 모킹할 때 받아오는 _id 값

해결됨

[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스

안녕하세요 멘토님 매번 강의 잘 듣고 있습니다. 이번 강의를 마무리로 별도로 개인 토이프로젝트 개발을 시작하고 배포를 할 때가 오면 다시 배포 부분부터 수강할 예정인데 이 강의에서 이해가 좀 정확히 안되는 부분이 있어서 질문 글을 올렸습니다. API 모킹을 통해 가짜 게시글 생성 테스트 API를 보내서 result를 받는 부분 중, _id 값 부분이 이해가 잘 안됩니다. writer , title , contents 는 테스트 input 부분에 있던 내용들 그대로 result에 적어서 이 값들이 제대로 insert 되었는지 테스트를 하기 때문에 같은 값을 입력했다는 것은 이해를 했습니다. 그런데 _id 부분은 왜 백엔드에서-받은-게시글ID 이라고 임의로 입력을 했는지 부분을 이해 못했습니다. id, _id 와 같은 값은 랜덤으로 생성되기에 뭘로 받아올지 모르기 때문에 테스팅 라이브러리에서 임의의 값으로 지정하도록 설정이 가능해서일까요? 그렇다면 아폴로에서 캐싱할 때도 _id, id 라는 명을 인식해서 캐싱이 더 잘되듯이, 테스팅 라이브러리도 _id, id 명에 한정해서 개발자가 임의의 값을 입력해도 되도록 만들어 둔 것인지도 궁금합니다. // 가짜 mutation 만들기 (요청, 응답 모두) const mocks = [ { request: { query: CREATE_BOARD, variables: { createBoardInput: { writer: "철수", title: "안녕하세요", contents: "반갑습니다", password: "1234", }, }, }, result: { data: { createBoard: { _id: "백엔드에서-받은-게시글ID", writer: "철수", title: "안녕하세요", contents: "반갑습니다", }, }, }, }, ];

  • react
  • node.js
  • seo
  • graphql
  • next.js
FRONTEER 댓글 1 좋아요 0 조회수 349

사용 게시글 강의에서 SerializableError

미해결

[리뉴얼] React로 NodeBird SNS 만들기

SerializableError: Error serializing `.initialState.post.loadPostsError` returned from `getServerSideProps` in "/user/[id]". Reason: `undefined` cannot be serialized as JSON. Please use `null` or omit this value all together. at isSerializable (/Users/js/Desktop/sansbook/node_modules/next/dist/lib/is-serializable-props.js:7:95) at /Users/js/Desktop/sansbook/node_modules/next/dist/lib/is-serializable-props.js:7:503 at Array.every (<anonymous>) at isSerializable (/Users/js/Desktop/sansbook/node_modules/next/dist/lib/is-serializable-props.js:7:304) at /Users/js/Desktop/sansbook/node_modules/next/dist/lib/is-serializable-props.js:7:503 at Array.every (<anonymous>) at isSerializable (/Users/js/Desktop/sansbook/node_modules/next/dist/lib/is-serializable-props.js:7:304) at /Users/js/Desktop/sansbook/node_modules/next/dist/lib/is-serializable-props.js:7:503 at Array.every (<anonymous>) at isSerializable (/Users/js/Desktop/sansbook/node_modules/next/dist/lib/is-serializable-props.js:7:304) 이 에러가 떴는데 user-[id].js에서 getServerSideProps 코드가 잘못되었다는 뜻인가요? 아니면 undefined나 null을 어떻게 처리할것인지.. 잘모르겠습니다

  • react
  • redux
  • node.js
  • express
  • next.js
장산 댓글 1 좋아요 0 조회수 453

withAuth에서 토큰 유지는 어떻게 하나요?

해결됨

[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스

section 19 refresh Token을 공부하면서 새로고침시 토큰을 유지하는 방법에 대해 다음과 같이 알려주셨는데요 const [accessToken, setAccessToken] = useRecoilState(accessTokenState); useEffect(() => { // 1. 기존방식(refreshToken 이전) // console.log("지금은 브라우저다!!!!!"); // const result = localStorage.getItem("accessToken"); // console.log(result); // if (result) setAccessToken(result); // 2. 새로운방식(refreshToken 이후) - 새로고침 이후에도 토큰 유지할 수 있도록 void getAccessToken().then((newAccessToken) => { setAccessToken(newAccessToken); }); }, []); 이게 withAuth가 아닐시의 방법이고 withAuth 일때는 나중에 리뷰해주신다고 했는데, 강의 자료를 살펴보니 따로 적힌 방법이 없어서요. withAuth가 아닐때는 토큰 유지를 어떻게 하나요?

  • react
  • node.js
  • seo
  • graphql
  • next.js
황수현 댓글 1 좋아요 0 조회수 417

태그의 속성값을 텍스트로 출력할수있나요?

미해결

안녕하세요 현재 파이썬을 공부중인 학생입니다. 이전까지는 태그의 값을 텍스트로 출력하고 있었는데 태그안에 속성값을 텍스트로 출력할일이 생겨서 여러 방면으로 검색 및 시도를 해보고 있는데 잘 안되네요 여기서 data-taitle="BLACK(99)" 의 BLACK(99) 를 텍스트로 출력하고 싶습니다. 어떻게 하면 될까요? #python

  • python
  • selenium
kshssi 댓글 1 좋아요 0 조회수 613

firebase auth 인증 질문

미해결

만들면서 배우는 프론트엔드 DO IT 코딩 (Next.js, Typescript)

fireBase인증 과정에 대하여 질문있습니다. 제가 이해한 것이 맞는지 확인해주시면 감사하겠습니다. 질문 1. (1) try { const signInResult = await signInWithPopup(FirebaseAuthClient.getInstance().Auth, provider); if (signInResult.user) { const idToken = await signInResult.user.getIdToken(); const { uid, displayName, photoURL, email } = signInResult.user; // uid // photoURL // displayName const resp = await memberAddForClient({ data: { uid, displayName: displayName || undefined, email: email!, screenName: email!.split('@')[0], photoURL: photoURL || undefined, provider: 'google', }, token: idToken, }); } signInWithPopup메서드를 통해 구글 로그인 후 로그인 한 유저 정보(signInResult)를 가져옵니다. (2) const idToken = await signInResult.user.getIdToken(); 여기서 accesstoken과 다른 유저정보 email,uid등등을 가져와서 filestore에 저장합니다 (3) export default async function verifyFirebaseIdToken(token: string) { try { const id = await FirebaseAdmin.getInstance().Auth.verifyIdToken(token); return id.uid; } 그후 사용자가 어떤 api요청을 보낼 때 verifyFirebaseIdToken 함수에에 있는 verifyIdToken메서드를 통해 아까 받은 accesstoken을 보내 토큰이 유효한지 검사합니다. accessotken이 유효하면 id를 주어 api에 함께 보내 api 요청을 성공시킵니다. 이 과정이 맞나요? 질문 2. const id = await FirebaseAdmin.getInstance().Auth.verifyIdToken(token); verifyFirebaseIdToken에서 유효성 검사를 할 때 token이 로그인할 때 받은 accesstoken과 동일한 token인가요? 질문 3. firestore에 accesstoken은 저장되어 있지 않던데 어디에 저장되어 있는 건가요? 질문 4. 코드에 accesstoken으로 인증하고 유효성 검사하는 것이 있는데요 따로 refreshToken은 없는데 firebase auth는 refreshtoken 시나리오는 잘 안하나요? 질문 5. 코드에는 브라우저에 쿠키나 로컬스토리지를 사용하지 않음에도 브라우저를 껐다가 다시 켜도 로그아웃 되어있지 않고 로그인이 되어있는데 어떻게 가능한 것인가요? 질문 6. 지금 이 로그인이 흔히 말하는 oauth맞나요?

  • next.js
김건희 댓글 1 좋아요 0 조회수 1230

서버에 코드를 올렸는데 mixed content에러 발생중입니다.

미해결

[리뉴얼] React로 NodeBird SNS 만들기

안녕하세요. 제로초님. 우선 학습관련 내용이 아닌 내용에 대해 질문드리게 되어 죄송합니다. 혹여 문제가 될 시 질문 글을 삭제하도록 하겠습니다. 몇 일 동안 해결하려 했으나 해결이 잘 되지 않아 이 곳을 통해 질문드리게 되었습니다. fe작업물을 테스트 서버에 반영하였으나, 로그인을 시도할 때 아래와 같은 에러가 발생중입니다. xhr.js:247 Mixed Content: The page at 'https://도메인주소/' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'http://백엔드api주소'. This request has been blocked; the content must be served over HTTPS. 구글링을 해본 결과 메타 태그를 index.html에 삽입하라고 하여 삽입을 해보았으나, pending로 통신 자체가 불가능한 상황입니다. http-proxy-middleware 라이브러리를 사용하여 setproxy.js파일도 세팅을 해보았으나 여전히 위와 같은 문제가 발생중입니다. .env 파일을 생성하여 백엔드 api를 프록시로 설정을 하였고, 프론트엔드 host도 지정을 하였는데 위와 같은 문제가 발생 중입니다. 도무지 해결이 되지 않아 여기 질문드린 점 다시 한번 사과드리겠습니다. 혹여 문제가 되면 질문 글은 바로 삭제하도록 하겠습니다.

  • react
  • redux
  • node.js
  • express
  • next.js
댓글 2 좋아요 0 조회수 2457

router undefined로 인한 쿼리요청 실패

해결됨

[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스

import NewBoard from "../../../../src/components/units/write/BoardWrite.container" import { useQuery, gql } from "@apollo/client" import { useRouter } from "next/router" const FETCH_BOARD = gql` query fetchBoard($boardId: ID!){ fetchBoard (boardId: $boardId) _id writer title contents } ` export default function editPage(props) { const router = useRouter() console.log("-------------------") console.log(router.query.detail) console.log("-------------------") const {data} = useQuery(FETCH_BOARD, { variables: { boardId : Number(router.query.detail) } }) console.log("++++++++++++++++++") console.log(data) console.log("++++++++++++++++++") console.log("###################################") return( <> <NewBoard isEdit={true} queryData = {data?.data?.fetchBoard}/> </> ) } 수정 페이지에서 라우터 안에 있는 보드 아이디로 쿼리 요청을 보내려 했는데 처음에는 router.query.detail이 undefined 상태로 있어서 그런지 오류가 나왔습니다 ㅜㅜ 어떤 부분이 잘못된 걸까요..

  • react
  • node.js
  • seo
  • graphql
  • next.js
강정현 댓글 1 좋아요 0 조회수 683

서버사이드렌더링 준비하기 강의 질문

미해결

[리뉴얼] React로 NodeBird SNS 만들기

강의에 올라와 있는 pages폴더에서 index.js 파일 getServerSideProps부분이 저도 store.dispatch오류가 나서 고차함수로 바꾸어 주니까 해결됬습니다. 근데 이유를 잘모르겠습니다 ㅠㅠ export const getServerSideProps = wrapper.getServerSideProps( (store) => async () => { store.dispatch({ type: LOAD_MY_INFO_REQUEST, }); store.dispatch({ type: LOAD_POSTS_REQUEST, }); store.dispatch(END); await store.sagaTask.toPromise(); } );

  • react
  • redux
  • node.js
  • express
  • next.js
장산 댓글 1 좋아요 0 조회수 710

github

해결됨

[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스

제 깃헙 아이디를 만들어서 푸시를 해야 이 섹션 진행 할 수 있는건가요? vscode와 git 연결을 해야하는건지 답변 부탁드립니다.

  • react
  • node.js
  • seo
  • graphql
  • next.js
wodndi0321 댓글 1 좋아요 0 조회수 328

Next.js 버전

해결됨

[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스

이 수업에 대한 질문은 아니지만, next.js의 버전이 올라갔는데 가르쳐주시는 버전으로 배워도 취업하는데에 문제가 없을까요??

  • react
  • node.js
  • seo
  • graphql
  • next.js
wodndi0321 댓글 1 좋아요 0 조회수 685

빌드 아이디 바꾼 후 적용 안됨_재질문

해결됨

[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스

안녕하세요. 이 강의에 질문을 달았는데 해결이 안되서 다시 질문드립니다.ㅠ 우선 로드밸런서에서는 아래의 동적페이지가 잘 작동되는것으로 확인됩니다. cloudFrond에서 연결도 잘 해줬구요. 그런데...제 도메인에서 부터 시작되는 부분은 자꾸 오류가 납니다. 아래처럼요ㅠ 아래 _buildManifest.js 네트워크창을 확인해보면 보드아이디도 잘 먹혀있는걸 확인할 수 있습니다. 그런데 오류가 난 빨간부분에서는 뭔가 이상합니다. x-cache 부분을 보면 클라우드 프론트에 에러가 났다고 하는데 이유를 당최 모르겠습니다.

  • react
  • node.js
  • seo
  • graphql
  • next.js
ju han 댓글 1 좋아요 0 조회수 409

thumb 폴더 생성이 안됩니다.

미해결

[리뉴얼] React로 NodeBird SNS 만들기

이미지 업로드 후 thumb 폴더가 생성 안됩니다! 검색해보니 AWS Lambda 함수에서 S3 서비스를 호출할 때, 해당 Lambda 함수의 실행 역할이 S3 버킷에 대한 필요한 권한을 가지고 있지 않아서 발생한 것이라고 합니다... 역할도 S3객체 읽기 전용 권한 선택했습니다. 근데 여기서 PutObject가 없는게 걸립니다..

  • react
  • redux
  • node.js
  • express
  • next.js
ksh990913 댓글 1 좋아요 0 조회수 428

쿠키공유 질문드립니다.

미해결

[리뉴얼] React로 NodeBird SNS 만들기

도메인까지 연결 성공했고 쿠키 공유만 해결하면 되는데 뭐 때문인지 감이 안잡힙니다....

  • react
  • redux
  • node.js
  • express
  • next.js
ksh990913 댓글 1 좋아요 0 조회수 379

기동시 동적으로 설정 정보를 읽어서 적용하는 방법

해결됨

[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스

FE 서버 기동시(yarn start) 사전에 정의한 설정 정보를 읽어서 사용하는 방법이 있을까요? 예를 들어, 아래 apollo의 uploadLink 생성 코드의 경우, 만약 graphql 서버 주소가 바뀌었다면 uri 부분을 변경해서 다시 빌드를 해야 되는데요. 빌드없이 yarn start 시에 FE 서버 로컬 설정파일에서 uri를 읽어와서 적용하는 방식으로 하고 싶은데 어떤식으로 하면 좋을지요? .env 파일은 빌드 이후에 변경은 반영되지 않는것 같고.. SSR 같은 방법으로 해야 할까요? 아니면, 기동시에 accessToken 처럼 backend에 uri 정보를 조회해서 적용해야 할까요? const uploadLink = createUploadLink({ uri: "https://backendonline.codebootcamp.co.kr/graphql", headers: { Authorization: `Bearer ${accessToken}` }, credentials: "include", });

  • react
  • node.js
  • seo
  • graphql
  • next.js
jskim75 댓글 1 좋아요 0 조회수 387

413 Request Entity Too Large 질문

미해결

[리뉴얼] React로 NodeBird SNS 만들기

안녕하세요 선생님. 프로젝트 배포하고 나서 테스트 중에 특정 이미지를 업로드 하면 '413 Request Entity Too Large'라고 뜨는 오류를 발견했습니다. 특정 이미지 크기는 4.13mb정도구요. 일단 에러가 떠도 그대로 post요청을 보내면 업로드된 이미지가 보이긴 하는데 왜 이런 에러가 뜨는지 모르겠습니다.. 테스트 해보니까 세로 길이가 비교적 긴 이미지를 업로드 요청할 때 에러가 나는걸 확인했습니다. 위 이미지가 프론트에서 나는 에러 상태 입니다. 오류 해결을 위해 먼저 back의 const upload = multer({ storage: multerS3({ s3: new AWS.S3(), bucket: 'fashionary-s3', key(req, file, cb) { cb(null, `original/${Date.now()}_${path.basename(file.originalname)}`) } }), limits: { fileSize: 20 * 1024 * 1024 }, // 20MB }); 여기서 limits부분이 20m인걸 확인하고, sudo vim /etc/nginx/nginx.conf 해서 http { ## # Basic Settings ## client_max_body_size 20M; sendfile on; tcp_nopush on; types_hash_max_size 2048; # server_tokens off; # server_names_hash_bucket_size 64; # server_name_in_redirect off; include /etc/nginx/mime.types; default_type application/octet-stream; 위와 같이 http부분 설정을 20m으로도, 0으로도 설정 후 sudo service nginx reload 했는데도 특정 이미지 전송을 시도하면 같은 에러가 떠요..이럴 땐 어디를 봐야 하나요?

  • react
  • redux
  • node.js
  • express
  • next.js
뉸뉴 댓글 1 좋아요 0 조회수 1458

js d-day 카운터 질문 합니다.

해결됨

[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스

const nowDate = new Date (); const dayDate = new Date ("2023-4-15") const count = (dayDate - nowDate) / 1000 ; const countDate = Math.floor(count / 3600 / 24); const countHours = Math.floor(count / 3600) % 24 ; const countMinutes = Math.floor(count / 60) % 60; const countSec = Math.floor(count) % 60; 수업 시간 작성한 코드는 이해가 갑니다. d day 카운터 관련 구글링중 const dDay = new Date('2023-4-15'); const today = new Date(); const difference = dDay.getTime() - today.getTime(), // Ms 단위로 변환 secInMs = Math.floor(difference / 1000), minInMs = Math.floor(secInMs / 60), hourInMs = Math.floor(minInMs / 60), days = Math.floor(hourInMs / 24), // 남은 시간 계산 seconds = secInMs % 60, minutes = minInMs % 60, hours = minutes % 24; 위의 코드를 보면 hours = minutes % 24 이부분이 이해가 가지 않습니다. minutes 은 나머지 값인데 왜 작동이 잘될까요?

  • react
  • node.js
  • seo
  • graphql
  • next.js
댓글 1 좋아요 1 조회수 587

인기 태그

인프런 TOP Writers

주간 인기글