https://www.newline.co/@esausilva/adding-a-react-app-to-a-.net-core-mvc-app--63008d15

 


.Net 5.0을 사용해 React 프로젝트에서 Docker Container를 사용해 프로젝트를 배포하는 방법에 대해 알아봅니다.

 

 

 

0. Docker Desktop 설치.

 

Docker Container로 배포를 위해선 개발 환경에 docker가 설치되어 있어야 합니다. 공식 홈페이지로 이동해 Docker Desktop을 설치합니다: www.docker.com/products/docker-desktop

 

Docker Desktop for Mac and Windows | Docker

Learn why Docker Desktop is the preferred choice for millions of developers building containerized applications. Download for Mac or Windows.

www.docker.com

 

 

 

1. 프로젝트 생성

 

Visual studio를 켜고 React.js 프로젝트를 생성합니다.

 

 

프로젝트 이름은 적당히 짓고 대상 프레임워크는 .Net 5.0을 선택합니다.

 

 

* IDE는 Visual studio 2019 CE를 사용하였습니다.

 

 

 

2. Docker 지원 추가.

 

Visual studio 2019의 다른 여러가지 Web 프로젝트는 대부분 기본적으로 Docker Container 배포를 지원합니다. 하지만 안타깝게도 포스팅을 올리는 현재 Visual studio 2019에서 React.js 프로젝트는 Docker Container 배포를 기본적으론 지원하지 않습니다. 앞서 프로젝트를 만들 때 docker 지원을 보지 못하셨을 겁니다.

 

React.js 프로젝트에 수동으로 docker 지원을 추가해 봅시다. 먼저 프로젝트를 우클릭한 뒤 추가 -> Docker 지원을 클릭합니다.

 

 

대부분의 경우는 Linux container일겁니다. 자신이 원하는 대상 OS를 선택합니다.

 

 

이제 IDE가 자동으로 "Dockerfile"을 생성해 .Net5.0에 맞는 dockerfile을 작성해 줍니다. "Dockerfile"을 우클릭 해 "Docker 이미지 빌드"를 클릭해 도커 이미지를 생성해 봅시다.

 

 

빌드가 잘 되시나요? 아마 다음과 같은 오류와 함께 빌드가 실패했을 것입니다.

 

 

이제 빌드 출력창을 한번 뒤져봅시다. 아마 다음과 같은 에러 메시지를 마주 할 수 있을 겁니다.

 

1>#16 2.809 /bin/sh: 2: /tmp/tmp1d7d426dff82428db5f0845ee787658f.exec.cmd: npm: not found
1>#16 2.814 /src/DockerDeployExample/DockerDeployExample.csproj(37,5): error MSB3073: The command "npm install" exited with code 127.
1>#16 ERROR: executor failed running [/bin/sh -c dotnet publish "DockerDeployExample.csproj" -c Release -o /app/publish]: exit code: 1

 

누가 봐도 어디서 문제가 생긴건지 찾으실 수 있을 겁니다 npm: not found 우리 이미지엔 npm이 없었습니다.

 

 

 

3. .Net5.0 이미지에 nodejs 추가하기.

 

사실 MSDN을 애용하는 개발자라면 이런 문제를 마주치지 않을 수도 있었을 겁니다. MSDN의 다음 글을 확인해 주세요: Quickstart: Use Docker with a React Single-page App in Visual Studio

 

Visual Studio Container Tools with ASP.NET Core and React.js

Learn how to create a containerized React SPA app with Visual Studio Container Tools and Docker

docs.microsoft.com

 

리눅스 컨테이너 항목을 보면 수동으로 nodejs를 설치해야 하는 것을 알 수 있습니다. 이제 우리 "Dockerfile"에 다음과 같은 명령어를 추가해 주도록 합니다.

 

RUN curl -sL https://deb.nodesource.com/setup_15.x | bash -
RUN apt-get install -y nodejs

 

* 현재 포스팅 일자 기준으로 nodejs의 최신 버전은 15.x 버전이며 LTS는 14.x 버전입니다.

 

MSDN페이지에선 base와 build 모두에 설치했지만 .Net5.0에선 base 이미지에 nodejs를 설치하면 에러가 발생하며 build 이미지에만 nodejs를 설치해 줘도 정상적으로 빌드가 됩니다.

 

수정한 Dockerfile은 다음과 같습니다.

#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
RUN curl -sL https://deb.nodesource.com/setup_15.x | bash -
RUN apt-get install -y nodejs
WORKDIR /src
COPY ["DockerDeployExample/DockerDeployExample.csproj", "DockerDeployExample/"]
RUN dotnet restore "DockerDeployExample/DockerDeployExample.csproj"
COPY . .
WORKDIR "/src/DockerDeployExample"
RUN dotnet build "DockerDeployExample.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "DockerDeployExample.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "DockerDeployExample.dll"]

 

이제 다시 "Dockerfile"을 우클릭 해 "Docker 이미지 빌드"를 클릭해 도커 이미지를 생성해보면 정상적으로 이미지가 생성되는 것을 확인할 수 있습니다.

 

 

 

4. Docker image push

 

이제 빌드한 이미지를 "게시" 기능을 이용해 Docker Registry에 Push까지 해봅시다. Docker Registry는 DockerHub를 사용하거나 PrivateRegistry를 구축해 사용하시면 됩니다.

 

프로젝트를 우클릭 해 "게시"를 클릭합니다.

 

 

게시 대상을 선택합니다. "Docker 컨테이너 레지스트리"를 선택합니다.

 

 

원하는 컨테이너 레지스트리를 선택합니다. 저는 이전에 구축한 Harbor에 게시해 보도록 하겠습니다.

 

 

자격증명과 레지스트리 경로를 입력합니다.

 

 

"호스팅" 항목의 "..."을 클릭하면 "이미지 태그 편집"을 사용할 수 있습니다. 기본값은 latest인데 이 값을 0.0.1로 바꿔 게시해 보도록 하겠습니다.

 

 

잠시 기다리면 게시가 완료되었다는 메시지를 확인할 수 있습니다.

 

 

실제로 Harbor에 접속해보면 이미지가 정상적으로 push 되었음을 확인할 수 있습니다.

 

 

 

 

 

 

반응형

 

.Net 5.0의 WPF에서 WinForm을 이용해 OpenFileDialog를 사용하는 방법에 대해 알아봅니다.

 

 

 

1. 프로젝트 설정.

 

IDE는 VisualStudio 2019 CE를 사용했으며 프로젝트는 .Net 5.0을 사용한 WPF 프로젝트를 생성하였습니다.

 

 

 

2. OpenFileDialog 사용해보기.

 

먼저 적당히 xaml에 버튼을 추가한 후 이벤트를 생성합니다.

 

MSDN의 .Net5.0에 해당하는 OpenFileDialog 클래스를 보면 다음과 같이 사용할 수 있다고 나와있습니다.

 

// public sealed class OpenFileDialog : System.Windows.Forms.FileDialog
var fileContent = string.Empty;
var filePath = string.Empty;
using (OpenFileDialog openFileDialog = new OpenFileDialog())
{
    openFileDialog.InitialDirectory = "c:\\";
    openFileDialog.Filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*";
    openFileDialog.FilterIndex = 2;
    openFileDialog.RestoreDirectory = true;
    if (openFileDialog.ShowDialog() == DialogResult.OK)
    {
        //Get the path of specified file
        filePath = openFileDialog.FileName;
        //Read the contents of the file into a stream
        var fileStream = openFileDialog.OpenFile();
        using (StreamReader reader = new StreamReader(fileStream))
        {
            fileContent = reader.ReadToEnd();
        }
    }
}
MessageBox.Show(fileContent, "File Content at path: " + filePath, MessageBoxButtons.OK);

 

위와 같이 이벤트를 수정해 봅시다.

 

 

슬프게도 에러가 발생합니다. 심지어 "System.Windows.Forms.FileDialog"도 찾을 수 없습니다. 

 

 

 

. .Net 5.0 WPF 프로젝트에서 WinForm 사용하기.

 

사실 .Net5.0 WPF 프로젝트에서 WinForm은 사용할 수 없는 게 기본값입니다. WinForm을 사용하기 위해선 프로젝트 설정으로 가서 수동으로 WinForm을 사용하도록 설정해야 합니다.

 

프로젝트 파일을 열어 설정을 확인합니다.

 

<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<OutputType>WinExe</OutputType>
		<TargetFramework>net5.0-windows</TargetFramework>
		<UseWPF>true</UseWPF>
	</PropertyGroup>
</Project>

 

별다른 설정을 하지 않았다면 위와 같은 설정을 확인할 수 있습니다. 이제 여기 "PropertyGroup"에 "UseWindowsForms" 항목을 추가해 줍니다.

 

<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<OutputType>WinExe</OutputType>
		<TargetFramework>net5.0-windows</TargetFramework>
		<UseWPF>true</UseWPF>
		<UseWindowsForms>true</UseWindowsForms>
	</PropertyGroup>
</Project>

 

이제 다시 코드로 돌아가 봅시다.

 

 

이제 정상적으로 WinForm을 사용할 수 있습니다.

 

 

 

 

 

반응형

 

System.Drawing.Bitmap를 사용할 때 발생하는 "The type initializer for 'Gdip' threw an exception." 오류를 해결하는 방법에 대해 알아봅니다.

 

 

 

1. 상황

 

프로젝트에서 System.Drawing.Bitmap을 사용하려고 할 때 Exception이 발생하며 "The type initializer for 'Gdip' threw an exception."와 같은 메시지가 출력됩니다.

 

프로젝트는 Docker Desktop을 사용해 Docker를 지원하는 프로젝트였습니다.

 

 

 

2. 원인

 

MS에서 Docker 지원 프로젝트를 만들면 자동으로 생성해주는 Dockerfile에 의해 생성되는 컨테이너는 libgdiplus를 지원하지 않아 발생하는 문제였습니다.

 

 

 

 

3. 해결

 

Dockerfile에 libgidplus를 지원할 수 있는 코드를 추가해 주면 됩니다. 코드는 다음 글에서 참고하였습니다: Can't use System.Drawing.Common in microsoft/dotnet:runtime

 

Can't use System.Drawing.Common in microsoft/dotnet:runtime · Issue #618 · dotnet/dotnet-docker

Steps to reproduce the issue pull microsoft/dotnet:runtime add .net core app that is using System.Drawing.Common package try and run app Expected behavior Should be able to create thumbnail images ...

github.com

 

# Your Dockerfile
#...
# install System.Drawing native dependencies
RUN apt-get update \
    && apt-get install -y --allow-unauthenticated \
        libc6-dev \
        libgdiplus \
        libx11-dev \
     && rm -rf /var/lib/apt/lists/*
#...

 

위와 같이 Dockefile을 수정 후 코드를 실행하면 정상적으로 동작합니다.

 

 

 

 

 

반응형

 

fo-dicom을 사용해 dicom파일에서 이미지를 추출하는 방법에 대해 알아봅니다. 또한 해당 작업을 수행하며 겪은 "A generic error occurred in GDI+." 에러를 해결하는 방법에 대해서도 알아봅니다.

 

 

 

Git에 나온 방법을 그대로 사용할 수 없어서 이것저것 알아보다 사용하게 된 코드입니다.

 

// Dicom images
ImageManager.SetImplementation(new WinFormsImageManager());
DicomImage dcmImg = new DicomImage(dcmFile.Dataset);
filePath = string.Format("{0}/{1}.bmp", folderPath, fileNameWithoutExt);
System.Drawing.Bitmap dcmBitmap = dcmImg.RenderImage().AsClonedBitmap();
dcmBitmap.Save(folderPath, System.Drawing.Imaging.ImageFormat.Bmp);

 

해당 코드를 수행하면 Save에서 다음과 같은 오류를 마주하게 됩니다: A generic error occurred in GDI+.

 

위 오류를 해결하기 위해 fo-dicom 관련 글을 뒤져봤으나 해결 방법을 찾을 수 없었는데 fo-dicom이 아닌 다른 글에서 해결 방법을 찾을 수 있었습니다: A Generic error occurred in GDI+ in Bitmap.Save method

 

A Generic error occurred in GDI+ in Bitmap.Save method

I am working on to upload and save a thumbnail copy of that image in a thumbnail folder. I am using following link: http://weblogs.asp.net/markmcdonnell/archive/2008/03/09/resize-image-before-upl...

stackoverflow.com

 

결론부터 말하면 fo-dicom과 관련된 문제가 아닌 Bitmap과 관련된 문제였습니다. MSDN과 연결된 링크는 확인할 수 없었으나 발췌된 글은 다음과 같습니다.

 

When either a Bitmap object or an Image object is constructed from a file, the file remains locked for the lifetime of the object. As a result, you cannot change an image and save it back to the same file where it originated.
> Bitmap 객체 또는 Image 객체가 파일에서 생성되면 생성된 파일은 객체의 수명 동안 잠긴 상태로 유지됩니다. 따라서 이미지를 변경하거나 원본과 동일한 파일에 다시 저장할 수 없습니다.

 

따라서 문제 되는 부분은 Bitmap을 Save 하는 부분이었습니다. 이를 해결하기 위해 다음과 같이 코드를 수정해 해결할 수 있었습니다.

 

// Dicom images
ImageManager.SetImplementation(new WinFormsImageManager());
DicomImage dcmImg = new DicomImage(dcmFile.Dataset);
filePath = string.Format("{0}/{1}.jpg", folderPath, fileNameWithoutExt);
System.Drawing.Bitmap dcmBitmap = dcmImg.RenderImage().AsClonedBitmap();
using (MemoryStream ms = new MemoryStream())
{
	using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite))
	{
		dcmBitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
		byte[] bytesBitmap = ms.ToArray();
		fs.Write(bytesBitmap, 0, bytesBitmap.Length);
	}
}

 

정상적으로 코드가 수행되는 것을 확인할 수 있습니다.

 

 

 

 

 

 

반응형

 

String.IsNullOrEmpty와 String.IsNullOrWhiteSpace의 차이점에 대해서 알아봅니다.

 

 

 

String.IsNullOrEmpty와 String.IsNullOrWhiteSpace

 

두 메서드는 한글로 해석하면 비어있다와 공백이다 정도의 아주 미묘한 차이로 해석됩니다. 이게 한글로 해석하면 미묘한 것이 공백의 사전적 정의에 "아무것도 없이 비어 있음."이 포함되어 있습니다. 결국 똑같이 해석되는 것처럼 보입니다. 

 

하지만 C#에서 프로그래밍적으로 두 메서드는 엄연히 다른 메서드입니다. MSDN에 따르면 String.IsNullOrWhiteSpace에 대한 설명에서 다음과 같이 나와있습니다.

 

 

 

IsNullOrWhiteSpace는 우수한 성능을 제공한다는 점을 제외하면 다음 코드와 유사한 기능을 하는 편리한 메서드입니다.

return String.IsNullOrEmpty(value) || value.Trim().Length == 0;

공백 문자는 유니 코드 표준에 의해 정의됩니다. IsNullOrWhiteSpace 메서드는 Char.IsWhiteSpace 메서드에 의해 true 값을 반환하는 모든 문자를 공백 문자로 해석합니다.

 

결국 String.IsNullOrWhiteSpace 메서드는 매개변수에 유니코드에 표준에 의한 "공백 문자"가 있더라도 true를 반환합니다. 다음 예시를 보면 그 차이를 한눈에 이해할 수 있습니다.

 

string.IsNullOrWhiteSpace("\t"); //true
string.IsNullOrEmpty("\t"); //false

string.IsNullOrWhiteSpace(" "); //true
string.IsNullOrEmpty(" "); //false

string.IsNullOrWhiteSpace("\n"); //true
string.IsNullOrEmpty("\n"); //false

 

 

 

 

 

 

반응형

 

앞선 글

 

Entity Framework Core를 처음 사용하고자 했습니다. MS Docs를 읽으며 자습서를 따라 하는 도중 문득 의문이 생겼습니다. 내가 그동안 알고 있던 패턴은 Repository 패턴인데 이 익숙한 패턴을 그대로 사용할 수는 없을까? 하는 생각이 들었습니다.

 

MS Docs에서는 Repository 패턴과 관련된 부분을 찾을 수 없었습니다. 혹시 이런 고민을 하고 있는 사람이 저뿐 아니라 다른 분도 있는지 구글링을 해 본 결과 다음과 같은 글을 확인할 수 있었습니다.

 

 

 

EF에서 저장소 패턴을 사용하는 것이 좋지 않은 이유

 

왜 Entity Framework 에서 Repository 패턴을 사용하면 안 될까요: Why shouldn't I use the repository pattern with Entity Framework?

 

Why shouldn't I use the repository pattern with Entity Framework?

During a job interview, I was asked to explain why the repository pattern isn't a good pattern to work with ORMs like Entity Framework. Why is this the case?

softwareengineering.stackexchange.com

 

해당 글을 번역해보자면 다음과 같습니다.

 

면접에서 Repository 패턴(이하 저장소 패턴)이 Entity Frmawork(이하 EF)와 같은 ORM과 함께 사용하는데 좋은 패턴이 아닌 이유를 설명하라는 질문을 받았습니다. 왜 그런 걸까요?

 

EF에서 저장소 패턴을 사용하지 않는 가장 큰 이유는 바로 EF가 이미 저장소 패턴을 구현하고 있기 때문입니다. DbContext는 여러분의 작업 단위이고 DbSet은 저장소가 됩니다. 이 위해 저장소 패턴으로 다른 계층을 구현한다면 중복될 뿐 아니라 유지 관리가 더 어려워지게 됩니다.

 

사람들은 디자인 패턴의 목적을 알지 못한 채 패턴을 사용하곤 합니다 저장소 패턴의 목적은 낮은 수준의 데이터베이스 쿼리 로직을 추상화하는 것입니다. 코드에 실제 SQL문을 작성하던 예전에는 저장소 패턴이 코드 베이스 전체에 흩어져 있는 각각의 SQL을 개별 메서드에서 한 곳으로 지역화(Localize)하는 방법 중 하나였습니다. EF와 같은 ORM을 사용하면 이러한 코드 추상화를 대체할 수 있으므로 저장소 패턴이 필요하지 않습니다.

 

그렇다고 ORM위에 추상화를 만드는 것은 나쁜 생각은 아닙니다. 데이터가 EF나 또 다른 Web API에서 오는지 신경 쓰지 않고 애플리케이션에서 사용할 수 있는  API를 구성하는 서비스 패턴을 사용한다고 가정해 봅시다. 애플리케이션에 필요한 데이터를 반환하기 위해 서비스 클래스에 메서드를 추가하기면 하면 되므로 훨씬 간단해집니다. 

 

예를 들어 To-do앱을 만들고 있다면 이번 주에 만기 되고 아직 완료되지 않은 항목을 반환하는 서비스 호출이 있을 수도 있습니다. 앱이 알고 있는 것은 이 정보를 원한다면 해당 메서드를 호출한다는 것입니다. 매서드 내부에서 EF와 상호작용이 진행됩니다. 나중에 상호작용 대상이 변경되더라도 이 메서드 외의 나머지 코드는 문제없이 작동합니다.

 

잠재적으로는 저장소 패턴을 사용하는 것을 주장하는 것처럼 들릴 수 있지만 여기서 주요한 차이점은 서비스가 더 얇은 계층이며 리포지토리가 계속 쿼리 하는 것과 달리 서비스는 완전히 만들어진 데이터를 반환하도록 되어있다는 점입니다.

 

 

 

맺는 글

 

위 글에서 "디자인 패턴의 목적을 알지 못한 채 패턴을 사용하곤 한다"는 글이 크게 와 닿았습니다. 다양한 패턴을 사용하기 위해 알아보곤 했는데 이 패턴을 왜 쓰고 무엇을 위해 사용되는 패턴인지 한번 더 확인해야겠다는 생각이 드는 글이었습니다. 물론 EF를 저장소 패턴과 같이 안 쓰는 이유에 대해서도 명확히 이해하게 되었습니다

 

 

 

 

 

반응형

https://devblogs.microsoft.com/dotnet/introducing-net-5/

 

1. 현상

 

dotNet 5.0이 나왔습니다. 한번 써보려고 dotNet 5.0 SDK를 설치하고 VS2019를 업그레이드하고 16.8 이상의 버전을 확인 한 뒤 Solution을 만들고 빌드를 눌렀습니다만 저를 반기는 건 다음과 같은 에러였습니다.

 

컴퓨터에 Framework 어셈블리가 없습니다.

 

 

 

이게 대체 무슨 상황인가 하고 Solution 구성을 뒤져보니 종속성 항목이 좀 이상해 보이는 게 눈에 들어왔습니다.

 

무언가 잘못되었습니다. 좀 더 자세히 보려고 종속성 어셈블리 참조 리스트를 열어봤습니다.

 

슬슬 머리가 아파옵니다. 혹시 프로젝트의 프레임워크 설정이 잘못됐나 하고 프레임워크 설정으로 들어가 봅니다.

 

 

대상 프레임 워크가 없습니다. 여기부터 머리를 쥐어 잡고 해결책을 찾는 대장정이 시작되었습니다.

 

 

 

해결책 1 - SDK 재설치

 

가장 먼저 떠올릴 수 있는 해결책이었습니다. Microsoft 공식 홈페이지로 이동해 dotNet 5.0 SDK를 새로 다운로드하여 제거 후 다시 설치했습니다.

 

여기까지 하고 해결되신 분들은 축하합니다. 뭔가 설치가 잘못됬었나 보네요. 하지만 여기서 해결됐다면 대장정이란 말은 꺼내지도 않았을 겁니다. 제 에러는 여전했으니까요.

 

 

 

해결책 2 - 사용자 변수 확인

 

열심히 구글링을 하다가 저와 유사해 보이는 문제를 가진 분의 글을 찾았습니다: Target framework dropdown empty

 

Target framework dropdown empty

I have VS.NET 2010 Premium installed, but there seems to be an issue when trying to target existing .NET frameworks. When I open properties for a web application project, the only option I get is to

stackoverflow.com

제시된 해결 방법을 요약하면 "환경 변수의 TEMP와 TMP 경로를 같게 해 줘라"입니다. 후다닥 환경변수를 확인해보니 이미 TEMP와 TMP모두 "%USERPROFILE%\AppData\Local\Temp"로 동일했습니다.

 

또한 댓글로 남겨진 해결방법 중 하나인 "계정 이름에서 아포스트로피(Apostrophe, ')를 제거했더니 된다"라는 내용도 있었지만 제 계정명은 영문으로만 이뤄져 있었습니다. 

 

 

 

해결책 3 - global.json 설정

 

또다시 구글링을 하는 도중에 제 문제와 똑같은 문제를 겪은 분의 글을 찾았습니다:  .NET 5 cant be selected as Target Framework in VS2019, reference assemblies not found

 

.NET 5 cant be selected as Target Framework in VS2019, reference assemblies not found

I cant get Visual studio to use .NET 5. The following are my current configuration: Visual Studio Professional 2019 version: 16.7.6 .NET sdk: dotnet-sdk-5.0.100-preview.7.20366.6-win-x64 (see image...

stackoverflow.com

 

제시된 해결방법을 소개하자면 다음과 같습니다.

 

보기-> 터미널 혹은 Ctrl + `를 눌러 터미널을 열고 "dotnet new globaljson"을 입력합니다.

Solution의 루트 폴더로 이동해 global.json을 열어 버전을 추가합니다. 앞서 설치한 버전이 5.0.2 이므로 5.0.2를 추가합니다.

{
  "sdk": {
    "version": "2.2.207",
    "version": "5.0.2"
  }
}

 

그리고 VS2019를 재시작합니다. 해당 글쓴이는 이렇게 해결했지만 제 문제는 여전했습니다.

 

 

 

해결책 4 - 시스템 변수 확인

 

여기까지 해도 해결이 되지 않으신 분들은 저와 같은 현상으로 이번에는 해결하셨으면 좋겠습니다. 해결책 3을 시도하던 도중 이상한 것을 발견했습니다. 해결책 3에서 설명한 대로 터미널을 열고 dotnet 버전을 확인하기 위해 "dotnet --info"를 입력했습니다.

 

아무 생각 없이 넘어갔었는데 경로가 뭔가 이상했습니다. 해결책 1에서 dotNet 5.0을 재설치할 때 확인한 화면하고 뭔가 달랐습니다.

 

 

이제 원인이 명확해졌습니다. 왜 어째서인진 모르겠지만 dotnet 기본 경로가 "C:\Program Files (x86)\dotnet"로 잡혀있었습니다. 왜 이런가 해서 찾아보니 "사용자 변수의 Path"값은 "C:\Program Files\dotnet\"로, "시스템 변수"의 값은 "C:\Program Files (x86)\dotnet\"로 잡혀있는 상황을 확인했습니다.

 

 

시스템 변수를 수정하고 다시 터미널을 열고 dotnet 버전을 확인하기 위해 "dotnet --info"를 입력합니다.

 

그동안 해결하겠다며 삽질하면서 설치한 모든 SDK가 쭉 나오면서 경로가 제대로 잡힌 걸 확인할 수 있습니다. 이제 Solution을 새로 만들고 확인하니 모든 프레임워크가 정상적으로 동작하는 걸 확인할 수 있었습니다.

 

 

 

 

 

 

반응형

 

Object storage에 파일 조회하고 업로드하는 방법에 대해 알아봅니다.

 

 

 

0. 사전 준비.

 

AWS S3든 뭐든 오브젝트 스토리지를 준비합니다. 단, AWS SDK를 사용할 예정이기 때문에 S3 Compatible 한 오브젝트 스토리지를 준비해야 합니다. 

 

만약 준비된 오브젝트 스토리지가 없다면 다음 글을 참고해 Minio를 준비해 봅시다.

2020/10/21 - [Programming] - [MINIO] 시놀로지 NAS에서 MINIO를 이용해 오브젝트 스토리지 구성하기

2020/10/23 - [Programming] - [Minio] Minio Object Stroage에 Region 지정하기.

 

[Minio] Minio Object Stroage에 Region 지정하기.

Minio Object Stroage에 Region 지정하는 방법에 대해 알아봅니다. 0. 사전 준비. 미리 Minio를 준비합니다. Minio 설치 방법은 다음 글을 참고해 주세요: 2020/10/21 - [Programming] - [MINIO] 시놀로지 NAS에..

smoh.tistory.com

 

IDE는 Visual Studio 2019 CE를 사용하였으며 AST.NET Core 버전은 3.0을 사용했습니다.

 

 

 

1. 솔루션 및 프로젝트 생성.

 

VS를 열고 새 프로젝트를 생성합니다. C# - ASP.NET Core 웹 애플리케이션을 선택합니다.

 

 

프로젝트 탬플릿은 API를 선택합니다. 

 

 

디버그를 시작해 시작화면을 확인합니다. 기본적으로 자동 생성되는 컨트롤러인 WeatherForecast 컨트롤러에서 데이터를 보여주고 있을 겁니다.

 

 

 

2. Nuget 패키지 추가하기.

 

S3 compatible Object Storage를 사용하므로 우선 ASW SDK를 설치해 줍니다. Nuget을 통해 AWSSDK.Core와 AWSSDK.S3를 먼저 설치해 줍니다. 그리고 추후 구현할 파일 리스트 조회를 위해 Json 관련 패키지도 같이 설치해 줍니다.

 

 

 

 

3. 컨트롤러 추가 및 파일 업로드 구현.

 

프로젝트를 우클릭해 컨트롤러를 추가합니다. "읽기/쓰기 작업이 포함된 api 컨트롤러 클래스"를 선택하면 기본인 예시 GET/POST/DELETE 메서드가 추가되어 있습니다. 어차피 새로 코딩해야 하니 "API 컨트롤러 클래스 - 비어있음"을 선택해 컨트롤러를 생성해 줍니다.

 

ASP.NET Core 코드에 대해선 자세히 설명하지 않도록 하겠습니다. 우선 S3 Client를 생성하고 POST로 업로드를 하는 메서드를 만들어보겠습니다.

 

using Amazon;
using Amazon.S3;
using Amazon.S3.Transfer;
using Microsoft.AspNetCore.Mvc;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace S3StorageManager.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class S3FileController : ControllerBase
    {
        private const string _storageEndpoint = "https://your.host.here";
        private const string _accessKeyId = "youraccesskeyidhere";
        private const string _secretAccessKey = "yoursecretaccesskeyhere";
        private readonly AmazonS3Config _s3Config = new AmazonS3Config()
        {
            RegionEndpoint = RegionEndpoint.APNortheast2,
            ServiceURL = _storageEndpoint,
            ForcePathStyle = true
        };

        [HttpPost("upload")]
        public async Task<ActionResult> Upload()
        {
            var req = HttpContext.Request;
            var form = req.Form;
            string fileName = form["filename"];
            string bucketName = form["bucketname"];
            try
            {
                using (var client = new AmazonS3Client(_accessKeyId, _secretAccessKey, _s3Config))
                {
                    using (var ms = new MemoryStream())
                    {
                        form.Files.First().CopyTo(ms);
                        var uploadReq = new TransferUtilityUploadRequest();
                        uploadReq.InputStream = ms;
                        uploadReq.Key = fileName;
                        uploadReq.BucketName = bucketName;
                        uploadReq.CannedACL = S3CannedACL.PublicRead;
                        var fileTransferUtil = new TransferUtility(client);
                        await fileTransferUtil.UploadAsync(uploadReq);
                        return Ok();
                    }
                }
            }
            catch(Exception e)
            {
                return StatusCode(500, e.Message);
            }
        }
    }
}

 

소스코드를 위에서부터 찬찬히 봅시다. 먼저 Endpoint, AccessKeyId, SecretAccessKey를 지정해 줍니다.

그리고 S3 Client를 위한 Config를 생성합니다. _s3Config의 RegionEndpoint는 S3의 Region을 의미합니다. 앞서 지정한 Minio Storage의 Region을 입력해 주세요. ServiceURL은 Endpoint를 의미합니다. 우리는 아마존의 S3 경로를 사용하지 않기 때문에 Minio의 Endpoint값을 입력해 줍니다. ForcePathStyle은 AWS SDK로 Minio를 개발하기 위해 반드시 True값으로 설정되어 있어야 합니다.

 

POST로 upload요청이 오면 Upload가 호출됩니다. 먼저 요청의 form으로부터 저장될 파일 이름과 버킷 이름을 받아옵니다. 그 후 미리 정의한 키값들과 설정값으로 S3 Client를 만든 뒤 MemoryStream에 form내의 파일을 복사합니다. 그 후 업로드 요청에 파일, 파일 이름, 버킷 이름을 지정한 뒤 UploadAync로 파일 업로드를 시도합니다.

 

바로 테스트를 시도해 봅시다. 먼저 코드를 실행시킨 뒤 Postman과 같은 프로그램을 이용해서 POST 요청을 날려봅시다.

 

 

위와 같이 POST 요청을 준비합니다. 엔드포인트는 호스트, 디버그 포트, 컨트롤러를 이용해 자신의 환경에 맞게 수정해 줍니다. filename은 버킷에 저장될 파일의 이름이고 bucketname은 파일이 저장될 버킷의 이름입니다. file은 아무거나 업로드 할 파일을 골라주세요.

 

다 작성했다면 Send 버튼을 클릭해 파일 업로드를 시도합니다. 200OK와 함께 업로드에 성공한 것을 확인할 수 있습니다. 만약 HTTPS로 인해 문제가 생긴다면 Postman에서 https를 이용하지 않도록 하는 옵션이 있으니 참고해 주세요. 

 

 

이후 브라우저를 통해 정상적으로 업로드 한 파일을 확인할 수 있습니다.

 

 

 

4. 버켓 내 파일 조회하기.

 

이제 업로드를 하였으니 업로드 한 파일을 조회하는 방법에 대해 알아봅니다. 컨트롤러 클래스에 다음 코드를 추가해 주세요.

 

// 중간 생략...
using Newtonsoft.Json;
using System.Collections.Generic;
// 중간 생략...
        [HttpGet("GetFileList")]
        public async Task<ActionResult> GetFileList()
        {
            var req = HttpContext.Request;
            var form = req.Form;
            string bucketName = form["bucketname"];
            try
            {
                using (var client = new AmazonS3Client(_accessKeyId, _secretAccessKey, _s3Config))
                {
                    List<Dictionary<string, string>> listS3Objs = new List<Dictionary<string, string>>();
                    var listObjectsResponse = await client.ListObjectsAsync(bucketName);
                    foreach (var obj in listObjectsResponse.S3Objects)
                    {
                        Dictionary<string, string> dicObjInfo = new Dictionary<string, string>();
                        dicObjInfo["Bucket"] = obj.BucketName;
                        dicObjInfo["Key"] = obj.Key;
                        dicObjInfo["Size"] = Convert.ToString(obj.Size);
                        dicObjInfo["Tag"] = obj.ETag;
                        dicObjInfo["Modified"] = obj.LastModified.ToString("yyyyMMddHHmmss");
                        dicObjInfo["Owner"] = Convert.ToString(obj.Owner);
                        dicObjInfo["StorageClass"] = obj.StorageClass;
                        listS3Objs.Add(dicObjInfo);
                    }
                    string jsonResult = JsonConvert.SerializeObject(listS3Objs);
                    return Ok(jsonResult);
                }
            }
            catch (Exception e)
            {
                return StatusCode(500, e.Message);
            }
        }

 

그다지 어렵지 않습니다. 아까와 같이 S3 클라이언트를 생성하고 bucketName을 이용해 오브젝트 리스트를 가져옵니다. 가져온 정보는 Dictionary를 이용해 구조화하고 List에 넣어둡니다. 그 후 모든 파일을  JSON으로 변경해 리턴하는 게 전부입니다.

 

만약 버킷 이름이 공백이면 다음과 같은 에러 메시지를 리턴합니다.

 

BucketName is a required property and must be set before making this call. (Parameter 'ListObjectsRequest.BucketName')

 

또한 존재하지 않는 버킷 이름을 전송한다면 다음과 같은 에러 메시지를 리턴합니다.

 

The specified bucket does not exist

 

이제 테스트를 해봅시다. 다시 Postman을 열고 다음과 같이 GET 요청을 작성합니다.

 

 

요청 주소를 자신의 환경에 맞게 수정한 뒤 bucketname을 파일을 업로드한 버킷 이름으로 설정해 준 뒤 Send 버튼을 클릭합니다.

 

[{"Bucket":"test-0","Key":"uploadtest.txt","Size":"11","Tag":"\"06e0e6637d27b2622ab52022db713ce2\"","Modified":"20201023135946","Owner":"Amazon.S3.Model.Owner","StorageClass":"STANDARD"}]

 

JSON 포맷으로 결과를 잘 받아오는 걸 확인할 수 있습니다.

 

 

 

 

반응형

ASP.Net Core 3.0 MVC 프로젝트에서 Material Design Compoent를 사용하는 방법에 대해 알아봅니다.

기본적인 솔루션 및 패키지 구성은 이 글을 참고해 주세요.

 

 

 

1. package.json 수정

 

Material degisn을 사용하기 위해 NPM에서 "material-components-web"을 설치해야 합니다.

pakckage.json의 내용을 다음과 같이 변경합니다.

{
  "version": "1.0.0",
  "name": "asp.net",
  "private": true,
  "devDependencies": {
    "gulp": "4.0.2",
    "del": "5.1.0",
    "material-components-web": "4.0.0"
  }
}

이후 package.json 파일을 우클릭 후 패키지 복원을 클릭해 주세요.

 

 

 

2. css 생성

 

MDC을 사용할 CSS파일을 만듭니다.

 

"Styles"폴더를 생성하고 그 안에 "Home.css"파일을 생성한 후 다음과 같이 코딩합니다.

@import "../node_modules/material-components-web/dist/material-components-web.css";
body {
    margin: 0px;
}

 

 

 

3. gulpfile.js 수정

 

설치한 MDCWeb 패키지와 생성한 CSS파일을 빌드시 wwwroot 폴더로 복사해주는 코드를 작성합니다.

 

gulp.file.js를 다음과 같이 수정합니다.

/// <binding AfterBuild='default' Clean='clean' />
var gulp = require('gulp');
var del = require('del');
var paths = {
    scripts: ['scripts/**/*.js'],
    styles: ['styles/**/*'],
    MDCWeb: ['./node_modules/material-components-web/**/*'],
};
gulp.task('clean', function () {
    return del(['wwwroot/scripts/**/*', 'wwwroot/node_modules/**/*']);
});
gulp.task('default', function () {
    gulp.src(paths.scripts).pipe(gulp.dest('wwwroot/scripts'));
    gulp.src(paths.styles).pipe(gulp.dest('wwwroot/styles'));
    gulp.src(paths.MDCWeb).pipe(gulp.dest('wwwroot/node_modules/material-components-web'));
});

 

 

 

4. 페이지 수정.

 

예시에서는 기본 페이지를 사용합니다.

 

index.cshtml을 다음과 같이 수정합니다.

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
    <link rel="stylesheet" href="~/styles/home.css">
    <title>Index</title>
</head>
<body>
    <header class=" mdc-top-app-bar">
        <div class="mdc-top-app-bar__row">
            <section class="mdc-top-app-bar__section mdc-top-app-bar__section--align-start">
                <button class="mdc-icon-button material-icons mdc-top-app-bar__navigation-icon--unbounded">menu</button><span class="mdc-top-app-bar__title">San Francisco, CA</span>
            </section>
            <section class="mdc-top-app-bar__section mdc-top-app-bar__section--align-end">
                <button class="mdc-icon-button material-icons mdc-top-app-bar__action-item--unbounded" aria-label="Download">file_download</button>
                <button class="mdc-icon-button material-icons mdc-top-app-bar__action-item--unbounded" aria-label="Print this page">print</button>
                <button class="mdc-icon-button material-icons mdc-top-app-bar__action-item--unbounded" aria-label="Bookmark this page">bookmark</button>
            </section>
        </div>
    </header>
    <script src="~/scripts/home.js"></script>
</body>
</html>

 

 

 

 

5. 빌드

 

이제 빌드 후 실행해봅시다.

 

다음과 같은 화면을 확인할 수 있습니다.

 

 

 

 

 

반응형

ASP.NET Core에서 TypeScript 사용하기

 

ASP.NET Core를 사용하는 솔루션에서 TypeScript를 사용하는 방법에 대해 알아봅니다.

 

 

 

0. 솔루션 및 프로젝트 생성.

 

TypeScript를 사용할 솔루션 및 프로젝트를 생성합니다.

예시에 사용된 솔루션은 ASP.Net Core 3.0을 사용하였으며 ASP.Net Core 웹 응용 프로그램 프로젝트중 웹 응용프로그램(Razor)을 사용하였습니다.

 

 

 

1. NuGet 패키지 설치

 

"프로젝트 -> NuGet 패키지 관리자"를 클릭해 NuGet 패키지 관리자를 엽니다.

 

"StaticFiles"와 "TypeScript.MsBuild"를 검색해 설치합니다.

 

 

 

 

2. Node.js 설치

 

TypeScript를 빌드하기 위해 gulp를 사용합니다. 여기에서 Nodejs를 설치하세요.

 

 

 

 

3. 외부 웹 도구 설정 변경.

 

PATH 환경 변수에서 npm을 찾도록 Visual Studio를 구성합니다. 기본적으로 Visual Studio는 설치 디렉터리에 있는 npm의 버전을 사용합니다.

 

도구 > 옵션 > 프로젝트 및 솔루션 > 웹 패키지 관리 > 외부 웹 도구로 이동한 후 목록에서 $(PATH) 항목을 선택합니다. 위쪽 화살표를 클릭하여 이 항목을 목록의 두 번째 위치로 이동합니다.

 

 

 

 

4. ts 파일 생성.

 

다음과 같이 프로젝트에 "scripts" 폴더를 추가한 후 "app.ts" 파일을 생성합니다.

 

그리고 다음과 같이 코딩합니다.

 

function sayHello() { 
    const compiler = (document.getElementById("compiler") as HTMLInputElement).value; 
    const framework = (document.getElementById("framework") as HTMLInputElement).value; 
    return `Hello from ${compiler} and ${framework}!`; 
}

 

 

 

5. TypeScript 구성 파일 생성.

 

이제 TypeScript 컴파일러 구성 파일을 생성합니다. "script" 폴더 내에 "tsconfig.json" 파일을 생성합니다.

 

새 항목 추가를 이용해 "TypeScript JSON 구성 파일"을 선택해서 생성하면 기본 설정을 자동으로 입력해 줍니다.

 

"tsconfig.json"의 설정을 다음과 같이 수정합니다.

 

{ 
  "compilerOptions": { 
    "noImplicitAny": true, 
    "noEmitOnError": true, 
    "removeComments": true, 
    "sourceMap": true, 
    "target": "es6" 
  }, 
  "exclude": [ 
    "wwwroot" 
  ], 
  "files": [ 
    "./app.ts" 
  ], 
  "compileOnSave": true 
} 

 

 

6. NPM 패키지 설치

 

이제 TypeScript 컴파일을 위한 npm 패키지를 설치해야 합니다.

 

프로젝트에 "package.json" 파일을 추가합니다. 새 항목 추가를 이용해 "NPM 구성 파일"을 선택해서 생성하면 기본 설정을 자동으로 입력해 줍니다.

 

"package.json"파일을 다음과 같이 수정해줍니다.

 

{ 
  "version": "1.0.0", 
  "name": "asp.net", 
  "private": true, 
  "devDependencies": { 
    "gulp": "4.0.2", 
    "del": "5.1.0" 
  } 
}

 

** "package.json" 파일을 수동으로 만들지 않고 npm cmd를 이용해서 생성해주셔도 됩니다.

 

수정이 끝났다면 "package.json" 파일을 우클릭 해 "패키지 복원"을 클릭합니다.

 

복원이 완료되면 프로젝트의 종속성에서 추가한 "del" 및 "gulp"를 확인할 수 있습니다.

 

 

 

 

 

 

 

7. gulp 설정

 

이제 설치한 gulp가 TypeScript를 빌드하도록 일을 시켜야 합니다.

 

프로젝트에 "gulpfile.js"를 추가한 후 다음과 같이 코딩합니다.

 

/// <binding AfterBuild='default' Clean='clean' />
var gulp = require('gulp');
var del = require('del');
var paths = {
    scripts: ['scripts/**/*.js', 'scripts/**/*.ts', 'scripts/**/*.map'],
};
gulp.task('clean', function () {
    return del(['wwwroot/scripts/**/*']);
});
gulp.task('default', function () {
    gulp.src(paths.scripts).pipe(gulp.dest('wwwroot/scripts'));
});

 

수정이 끝났다면 "gulpfile.js" 파일을 우클릭 해 "작업 러너 탐색기"를 클릭합니다.

 

 

 

 

 

 

 

 

 

 

정상적으로 적용되었다면 좌측 화면과 같은 내용이 보입니다.

만약 반영이 안 되었다면 하늘색 화살표를 눌러 새로고침을 해주세요.

 

 

 

 

 

 

 

 

 

8. HTML 작성

 

"Pages -> index.cshtml"파일을 수정합니다.

 

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <div id="message"></div>
    <div>
        Compiler: <input id="compiler" value="TypeScript" onkeyup="document.getElementById('message').innerText = sayHello()" /><br />
        Framework: <input id="framework" value="ASP.NET" onkeyup="document.getElementById('message').innerText = sayHello()" />
    </div>
</div>
<script src="~/scripts/app.js"></script>

 

 

 

9. 빌드 및 결과 확인.

 

이제 빌드 및 디버그를 수행합니다.

 

빌드가 완료되면 "webroot"에 "scripts"폴더가 생성되고 "app.js"파일이 생성된 것을 확인할 수 있습니다.

 

 

 

 

 

 

 

컴파일러 내의 문자열을 수정하면 위에 텍스트가 변경됩니다.

 

 

 

 

 

 

반응형

+ Recent posts