Next.js - styled-jsx로 스타일링 하기

carrot·2021년 10월 21일
0

Nextjs

목록 보기
7/15
post-thumbnail
  • styled-jsx는 CSS-in-JS 라이브러리로 css를 캡슐화하고 범위가 지정되게 만들어 구성 요소를 스타일링 할 수 있습니다.
  • 한 구성 요소의 스타일링은 다른 구성요소에 영향을 미치지 않도록 하는 특징을 가지고 있습니다.
  • next에서 기본으로 제공하므로 설치가 필요 없고 활용이 간편합니다.

> example.jsx

import css from "styled-jsx/css";

const style = css`
  h2 {
    margin-left: 20px;
  }
  .user-bio {
    margin-top: 12px;
    font-style: italic;
  }
`;

const username = ({ user }) => {
  return (
    <>
      {user ? (
        <div>
          <h2>{user.name}</h2>
          <p className="user-bio">{user.bio}</p>
        </div>
      ) : (
        <div>유저 정보가 없습니다.</div>
      )}
      <style jsx>{style}</style>
    </>
  );
};

export default username;

1. github 프로필 스타일링

> pages/users/[name].jsx

import css from "styled-jsx/css";
import fetch from "isomorphic-unfetch";

const style = css`
  .profile-box {
    width: 25%;
    max-width: 272px;
    margin-right: 26px;
  }
  .profile-image-wrapper {
    width: 100%;
    border: 1px solid #e1e4e8;
  }
  .profile-image-wrapper .profile-image {
    display: block;
    width: 100%;
  }
  .profile-username {
    margin: 0;
    padding-top: 16px;
    font-size: 26px;
  }
  .profile-user-login {
    margin: 0;
    font-size: 20px;
  }
  .profile-user-bio {
    margin: 0;
    padding-top: 16px;
    font-size: 14px;
  }
`;

const name = ({ user }) => {
  if (!user) return null;

  return (
    <>
      <div className="profile-box">
        <div className="profile-image-wrapper">
          <img
            src={user.avatar_url}
            alt={`${user.name} 프로필 이미지`}
            className="profile-image"
          />
        </div>
        <h2 className="profile-username">{user.name}</h2>
        <p className="profile-user-login">{user.login}</p>
        <p className="profile-user-bio">{user.bio}</p>
      </div>
      <style jsx>{style}</style>
    </>
  );
};

export const getServerSideProps = async ({ query }) => {
  const { name } = query;

  try {
    const res = await fetch(`https://api.github.com/users/${name}`);

    if (res.status === 200) {
      const user = await res.json();
      return { props: { user } };
    }

    return { props: {} };
  } catch (error) {
    console.log(error);
    return { props: {} };
  }
};

export default name;

2. react-icons

react-icons를 설치하고 필요한 아이콘을 import해서 사용해 보도록 하겠습니다.

우선 view에 해당하는 Profile.jsx 컴포넌트를 생성해 코드를 분리하고 리액트 아이콘을 활용하여 출력을 개선해 보도록 하겠습니다.

> components/Profile.jsx

import css from "styled-jsx/css";
import { GoOrganization, GoLink, GoMail, GoLocation } from "react-icons/go";

const style = css`
  .profile-box {
    width: 25%;
    max-width: 272px;
    margin-right: 26px;
  }
  .profile-image-wrapper {
    width: 100%;
    border: 1px solid #e1e4e8;
  }
  .profile-image-wrapper .profile-image {
    display: block;
    width: 100%;
  }
  .profile-username {
    margin: 0;
    padding-top: 16px;
    font-size: 26px;
  }
  .profile-user-login {
    margin: 0;
    font-size: 20px;
  }
  .profile-user-bio {
    margin: 0;
    padding-top: 16px;
    font-size: 14px;
  }
  .profile-user-info {
    display: flex;
    align-items: center;
    margin: 4px 0 0;
  }
  .profile-user-info-text {
    margin-left: 6px;
  }
`;

const Profile = ({ user }) => {
  if (!user) return null;

  return (
    <>
      <div className="profile-box">
        <div className="profile-image-wrapper">
          <img
            src={user.avatar_url}
            alt={`${user.name} 프로필 이미지`}
            className="profile-image"
          />
        </div>
        <h2 className="profile-username">{user.name}</h2>
        <p className="profile-user-login">{user.login}</p>

        <p className="profile-user-bio">{user.bio}</p>
        <p className="prifile-user-info">
          <GoOrganization size={16} color="#6a737d" />
          <span className="profile-user-info-text">{user.company}</span>
        </p>
        <p className="prifile-user-info">
          <GoLocation size={16} color="#6a737d" />
          <span className="profile-user-info-text">{user.location}</span>
        </p>
        <p className="prifile-user-info">
          <GoMail size={16} color="#6a737d" />
          <span className="profile-user-info-text">stylenbs@gmail.com</span>
        </p>
        <p className="prifile-user-info">
          <GoLink size={16} color="#6a737d" />
          <span className="profile-user-info-text">
            devcarrot-skilltree.herokuapp.com
          </span>
        </p>
      </div>
      <style jsx>{style}</style>
    </>
  );
};

export default Profile;

github api로 데이터를 가져와 동적 컴포넌트에 props로 전달하는 로직은 [name].jsx 컴포넌트에 남겨두도록 합니다.

> pages/users/[namx].jsx

import Profile from "../../components/Profile";
import fetch from "isomorphic-unfetch";

const name = ({ user }) => {
  if (!user) return null;

  return (
    <>
      <Profile user={user} />
    </>
  );
};

export const getServerSideProps = async ({ query }) => {
  const { name } = query;

  try {
    const res = await fetch(`https://api.github.com/users/${name}`);

    if (res.status === 200) {
      const user = await res.json();
      return { props: { user } };
    }
    return { props: {} };
  } catch (error) {
    console.log(error);
    return { props: {} };
  }
};

export default name;

빌드 후 localhost에 접속해서 github 아이디를 검색하면 리액트 아이콘과 함께 출력되는 유저 정보를 확인할 수 있습니다.

3. github repositories 리스트 스타일링

Profile.jsx를 만들었던 방식과 동일하게 repositories 리스트를 만들어 봅시다. getServerSideProps에서 데이터를 요청하고 데이터를 이용하여 view를 만들도록 하겠습니다.
유저의 repositories를 불러오는 github api 경로는 아래와 같습니다.

https://api.github.com/users/${username}/repos?sort=updated&page=1&per_page=10

  • api는 여러가지 query를 받는데, sort는 정렬기준으로 업데이트 기준으로 불러옵니다.
  • per_page는 page당 받게 될 레파지토리의 개수이며, page는 per_page의 개수만큼 나누었을 때 불러올 그룹의 번호를 의미합니다.

> pages/users/[name].jsx

import Profile from "../../components/Profile";
import fetch from "isomorphic-unfetch";
import css from "styled-jsx/css";

const style = css`
  .user-contents-wrapper {
    display: flex;
    padding: 20px;
  }
  .repos-wrapper {
    width: 100%;
    height: 100vh;
    overflow: scroll;
    padding: 0px 16px;
  }
  .repos-header {
    padding: 16px 0;
    font-size: 14px;
    font-weight: 600;
    border-bottom: 1px solid #e1e4e8;
  }
  .repos-count {
    display: inline-block;
    padding: 2px 5px;
    margin-left: 6px;
    font-size: 12px;
    font-weight: 600;
    line-height: 1;
    color: #586069;
    background-color: rgba(27, 31, 35, 0.08);
    border-radius: 20px;
  }

  .repository-wrapper {
    width: 100%;
    border-bottom: 1px solid #e1e4e8;
    padding: 24px 0;
  }
  .repository-description {
    padding: 12px 0;
  }
  a {
    text-decoration: none;
  }
  .repository-name {
    margin: 0;
    color: #0366d6;
    font-size: 20px;
    display: inline-block;
    cursor: pointer;
  }
  .repository-name:hover {
    text-decoration: underline;
  }
  .repository-description {
    margin: 0;
    font-size: 14px;
  }
  .repository-language {
    margin: 0;
    font-size: 14px;
  }
  .repository-updated-at {
    margin-left: 20px;
  }
`;

const name = ({ user, repos }) => {
  if (!user) return null;

  return (
    <div className="user-contents-wrapper">
      <Profile user={user} />
      <div className="repos-wrapper">
        <div className="repos-header">
          Repositories
          <span className="repos-count">{user.public_repos}</span>
        </div>
        {user && repos ? (
          repos.map((repo) => (
            <div className="repository-wrapper" key={repo.id}>
              <a
                target="_blank"
                rel="noreferrer"
                href={`https://github.com/${user.login}/${repo.name}`}
              >
                <h2 className="repository-name">{repo.name}</h2>
              </a>
              <p className="repository-description">{repo.description}</p>
              <p className="repository-language">
                {repo.language}
                <span className="repository-updated-at"></span>
              </p>
            </div>
          ))
        ) : (
          <div>ropository 정보가 없습니다.</div>
        )}
      </div>
      <style jsx>{style}</style>
    </div>
  );
};

export const getServerSideProps = async ({ query }) => {
  const { name } = query;

  try {
    let user;
    let repos;

    const userRes = await fetch(`https://api.github.com/users/${name}`);
    if (userRes.status === 200) {
      user = await userRes.json();
    }

    const repoRes = await fetch(
      `https://api.github.com/users/${name}/repos?sort=updated&page=1&per_page=10`
    );
    if (repoRes.status === 200) {
      repos = await repoRes.json();
    }
    console.log(repos);
    return { props: { user, repos } };
  } catch (error) {
    console.log(error);
    return { props: {} };
  }
};

export default name;
  • repoRes는 배열값을 반환하므로 map 함수를 통해 repository 정보를 출력합니다.
  • a태그의 ref="noopener noreferrer"는 보안상의 이유로 위부링크 사용시 추가하는 어트리뷰트값입니다.

4. 날짜 출력 라이브러리 date-fns

date-fns는 날짜 및 시간에 관련된 api를 제공하는 라이브러리 입니다. monent가 가장 인기있는 라이브러리지만, 크기 때문에 비교적 가벼운 date-fns를 사용해 봅니다.

$ yarn add date-fns

date-fns를 설치한 뒤 적용해 봅시다.

> pages/users/[name].jsx

...
<p className="repository-language">
  {repo.language}
  <span className="repository-updated-at">
    {formatDistance(
      new Date(repo.updated_at),
      new Date(),
      { addSuffix: true, }
     )
    }
  </span>
</p>
...

공식문서를 확인하여 더 세부적인 date 설정을 할 수 있으니 참고 바랍니다.

date-fns

5. pagination

github repositories를 가져오는 api를 보면 query로 page와 per_page를 보내주고 있습니다. 이를 통해 페이지네이션을 구현해 보도록 하겠습니다.

Profile과 마찬가지로 repositories를 위한 컴포넌트를 분리하도록 하겠습니다.

> components/Repositories.jsx

import { useRouter } from "next/router";
import css from "styled-jsx/css";
import Link from "next/link";
import formatDistance from "date-fns/formatDistance";

const style = css`
  .repos-wrapper {
    width: 100%;
    height: 100vh;
    overflow: scroll;
    padding: 0px 16px;
  }
  .repos-header {
    padding: 16px 0;
    font-size: 14px;
    font-weight: 600;
    border-bottom: 1px solid #e1e4e8;
  }
  .repos-count {
    display: inline-block;
    padding: 2px 5px;
    margin-left: 6px;
    font-size: 12px;
    font-weight: 600;
    line-height: 1;
    color: #586069;
    background-color: rgba(27, 31, 35, 0.08);
    border-radius: 20px;
  }

  .repository-wrapper {
    width: 100%;
    border-bottom: 1px solid #e1e4e8;
    padding: 24px 0;
  }
  .repository-description {
    padding: 12px 0;
  }
  a {
    text-decoration: none;
  }
  .repository-name {
    margin: 0;
    color: #0366d6;
    font-size: 20px;
    display: inline-block;
    cursor: pointer;
  }
  .repository-name:hover {
    text-decoration: underline;
  }
  .repository-description {
    margin: 0;
    font-size: 14px;
  }
  .repository-language {
    margin: 0;
    font-size: 14px;
  }
  .repository-updated-at {
    margin-left: 20px;
  }
  .repository-pagination {
    border: 1px solid rgba(27, 31, 35, 0.15);
    border-radius: 3px;
    width: fit-content;
    margin: auto;
    margin-top: 20px;
  }
  .repository-pagination button {
    padding: 6px 12px;
    font-size: 14px;
    border: 0;
    color: #0366d6;
    background-color: white;
    font-weight: bold;
    cursor: pointer;
    outline: none;
  }
  .repository-pagination button:first-child {
    border-right: 1px solid rgba(27, 31, 35, 0.15);
  }
  .ropository-pagination button:hover:not([disabled]) {
    background-color: #0366d6;
    color: white;
  }
  .repository-pagination button:disabled {
    cursor: no-drop;
    color: rgba(27, 31, 35, 0.3);
  }
`;

const Repositories = ({ user, repos }) => {
  const router = useRouter();
  const { page = "1" } = router.query;

  if (!user || !repos) return null;

  return (
    <>
      <div className="repos-wrapper">
        <div className="repos-header">
          Repositories
          <span className="repos-count">{user.public_repos}</span>
        </div>
        {repos.map((repo) => (
          <div className="repository-wrapper" key={repo.id}>
            <a
              target="_blank"
              rel="noopener noreferrer"
              href={`https://github.com/${user.login}/${repo.name}`}
            >
              <h2 className="repository-name">{repo.name}</h2>
            </a>
            <p className="repository-description">{repo.description}</p>
            <p className="repository-language">
              {repo.language}
              <span className="repository-updated-at">
                {formatDistance(new Date(repo.updated_at), new Date(), {
                  addSuffix: true,
                })}
              </span>
            </p>
          </div>
        ))}
        <div className="repository-pagination">
          <Link href={`/users/${user.login}?page=${Number(page) - 1}`}>
            <a>
              <button type="button" disabled={page && page === "1"}>
                Previous
              </button>
            </a>
          </Link>
          <Link
            href={`/users/${user.login}?page=${!page ? "2" : Number(page) + 1}`}
          >
            <a>
              <button type="button" disabled={repos.length < 10}>
                Next
              </button>
            </a>
          </Link>
        </div>
      </div>
      <style jsx>{style}</style>
    </>
  );
};

export default Repositories;

pagination 로직

  • page가 1 이거나 없다면 Previous 버튼은 disabled가 되며, Next를 클릭하면 page가 2가 되어 라우팅을 합니다.
  • page가 2 라면 previous를 클릭하면 페이지는 1로 라우팅이 되고 Next를 클릭하면 3으로 라우팅 됩니다.
  • per_page를 10개씩 받아왔을 때 받은 repositories가 10보다 작다면 마지막 페이지임을 의미하여 Next 버튼이 disabled 됩니다.

작성된 Repositories.jsx를 import하여 [namx].jsx를 수정합니다.

import Profile from "../../components/Profile";
import Repositories from "../../components/Repositories";
import fetch from "isomorphic-unfetch";
import css from "styled-jsx/css";

const style = css`
  .user-contents-wrapper {
    display: flex;
    padding: 20px;
  }
`;

const name = ({ user, repos }) => {
  if (!user) return null;

  return (
    <div className="user-contents-wrapper">
      <Profile user={user} />
      <Repositories user={user} repos={repos} />
      <style jsx>{style}</style>
    </div>
  );
};

export const getServerSideProps = async ({ query }) => {
  const { name, page } = query;

  try {
    let user;
    let repos;

    const userRes = await fetch(`https://api.github.com/users/${name}`);
    if (userRes.status === 200) {
      user = await userRes.json();
    }

    const repoRes = await fetch(
      `https://api.github.com/users/${name}/repos?sort=updated&page=${page}&per_page=10`
    );
    if (repoRes.status === 200) {
      repos = await repoRes.json();
    }
    return { props: { user, repos } };
  } catch (error) {
    console.log(error);
    return { props: {} };
  }
};

export default name;

정리

라우팅과 서버에서 데이터를 패치하는 방법을 이용하여 pagination을 구현할 수 있습니다.

profile
당근같은사람

0개의 댓글